yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_editor_content.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <functional>
7#include <initializer_list>
8#include <string>
9#include <vector>
10
11#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
18
19namespace yaze::editor {
20
22 DungeonObjectInteraction& interaction,
23 const std::vector<size_t>& editor_selected_indices) {
24 if (interaction.GetSelectedObjectIndices() == editor_selected_indices) {
25 return false;
26 }
27 // Canvas selection callbacks mirror back into DungeonObjectEditor. Copy the
28 // target first so those callbacks cannot invalidate the source vector while
29 // ObjectSelection is being rebuilt.
30 const std::vector<size_t> target_indices = editor_selected_indices;
31 interaction.SetSelectedObjects(target_indices);
32 return true;
33}
34
35namespace {
36
37using InspectorStat = std::pair<const char*, std::string>;
38
40 const char* label = "";
41 std::function<void()> action;
42 bool enabled = true;
43};
44
45const char* GetStoredPlacementLabel(const zelda3::RoomObject& object) {
46 if (zelda3::UsesRoomObjectStream(object)) {
47 switch (object.GetLayerValue()) {
48 case 0:
49 return "Primary";
50 case 1:
51 return "BG2 overlay";
52 case 2:
53 return "BG1 overlay";
54 default:
55 return "Unknown";
56 }
57 }
58 switch (object.GetLayerValue()) {
59 case 0:
60 return "Upper layer (BG1)";
61 case 1:
62 return "Lower layer (BG2)";
63 default:
64 return "Unknown";
65 }
66}
67
80
82 std::function<void(zelda3::Sprite&)> mutator) {
83 if (viewer == nullptr || viewer->rooms() == nullptr) {
84 return false;
85 }
86
87 auto& interaction = viewer->object_interaction();
88 auto& coordinator = interaction.entity_coordinator();
89 auto& handler = coordinator.sprite_handler();
90 const auto selected_index = handler.GetSelectedIndex();
91 if (!selected_index.has_value()) {
92 return false;
93 }
94
95 auto* room = &(*viewer->rooms())[viewer->current_room_id()];
96 auto& sprites = room->GetSprites();
97 if (*selected_index >= sprites.size()) {
98 return false;
99 }
100
101 if (auto* ctx = handler.context()) {
102 ctx->NotifyMutation(MutationDomain::kSprites);
103 }
104 mutator(sprites[*selected_index]);
105 room->MarkSpritesDirty();
106 if (auto* ctx = handler.context()) {
107 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
108 ctx->NotifyEntityChanged();
109 }
110 return true;
111}
112
114 std::function<void(zelda3::PotItem&)> mutator) {
115 if (viewer == nullptr || viewer->rooms() == nullptr) {
116 return false;
117 }
118
119 auto& interaction = viewer->object_interaction();
120 auto& coordinator = interaction.entity_coordinator();
121 auto& handler = coordinator.item_handler();
122 const auto selected_index = handler.GetSelectedIndex();
123 if (!selected_index.has_value()) {
124 return false;
125 }
126
127 auto* room = &(*viewer->rooms())[viewer->current_room_id()];
128 auto& items = room->GetPotItems();
129 if (*selected_index >= items.size()) {
130 return false;
131 }
132
133 if (auto* ctx = handler.context()) {
134 ctx->NotifyMutation(MutationDomain::kItems);
135 }
136 mutator(items[*selected_index]);
137 room->MarkPotItemsDirty();
138 if (auto* ctx = handler.context()) {
139 ctx->NotifyInvalidateCache(MutationDomain::kItems);
140 ctx->NotifyEntityChanged();
141 }
142 return true;
143}
144
145void DrawInspectorSummaryGrid(const char* table_id,
146 std::initializer_list<InspectorStat> stats) {
147 if (stats.size() == 0) {
148 return;
149 }
150
151 if (!ImGui::BeginTable(
152 table_id, 2,
153 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
154 return;
155 }
156
157 const auto& theme = AgentUI::GetTheme();
158 for (const auto& [label, value] : stats) {
159 ImGui::TableNextColumn();
160 ImGui::BeginGroup();
161 ImGui::TextColored(theme.text_secondary_gray, "%s", label);
162 ImGui::TextWrapped("%s", value.c_str());
163 ImGui::EndGroup();
164 }
165 ImGui::EndTable();
166}
167
169 std::initializer_list<InspectorAction> actions) {
170 if (actions.size() == 0) {
171 return;
172 }
173
174 const ImGuiStyle& style = ImGui::GetStyle();
175 const float spacing = style.ItemSpacing.x;
176 const float content_width = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
177 const float line_right =
178 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
179 bool first = true;
180
181 for (const InspectorAction& action : actions) {
182 const float desired_width = ImGui::CalcTextSize(action.label).x +
183 style.FramePadding.x * 2.0f + 10.0f;
184 const float button_width =
185 std::min(std::max(84.0f, desired_width), content_width);
186
187 if (!first) {
188 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
189 if (next_x <= line_right) {
190 ImGui::SameLine(0.0f, spacing);
191 }
192 }
193 first = false;
194
195 if (!action.enabled) {
196 ImGui::BeginDisabled();
197 }
198 if (ImGui::Button(action.label, ImVec2(button_width, 0.0f)) &&
199 action.enabled && action.action) {
200 action.action();
201 }
202 if (!action.enabled) {
203 ImGui::EndDisabled();
204 }
205 }
206}
207
209 switch (kind) {
211 return ICON_MD_DELETE_SWEEP " Delete All Doors";
213 return ICON_MD_DELETE_SWEEP " Delete All Sprites";
215 return ICON_MD_DELETE_SWEEP " Delete All Items";
218 return ICON_MD_DELETE_SWEEP " Delete All";
219 default:
220 return ICON_MD_DELETE_SWEEP " Delete All";
221 }
222}
223
224} // namespace
225
227 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
228 : object_editor_(std::move(object_editor)) {}
229
237
240 return;
241 }
242
243 auto& interaction = canvas_viewer_->object_interaction();
244 interaction.SetSelectionChangeCallback([this]() { OnSelectionChanged(); });
245 interaction.SetEntityChangedCallback([this]() { OnSelectionChanged(); });
246
249}
250
262
264 auto* viewer = ResolveCanvasViewer();
265 if (!viewer) {
268 return;
269 }
270
272
273 if (!object_editor_) {
274 return;
275 }
276
277 auto indices = viewer->object_interaction().GetSelectedObjectIndices();
278 (void)object_editor_->ClearSelection();
279 for (size_t idx : indices) {
280 (void)object_editor_->AddToSelection(idx);
281 }
282}
283
296
297void ObjectEditorContent::Draw(bool* p_open) {
298 (void)p_open;
299 auto* viewer = ResolveCanvasViewer();
300 const auto& theme = AgentUI::GetTheme();
301
302 ImGui::AlignTextToFramePadding();
303 ImGui::TextColored(theme.text_info, ICON_MD_TUNE " Selection Inspector");
304 ImGui::SameLine();
305 if (ImGui::SmallButton(ICON_MD_HELP_OUTLINE " Shortcuts")) {
306 show_shortcut_help_ = true;
307 }
308 ImGui::Separator();
309
310 if (!viewer || !object_editor_) {
311 ImGui::TextDisabled(tr("Object editor unavailable"));
312 return;
313 }
314
318
321 object_editor_->DrawPropertyUI();
323 viewer->object_interaction(),
324 object_editor_->GetSelection().selected_objects);
333 ImGui::TextDisabled(
335 } else {
337 }
338
341}
342
344 const auto& theme = AgentUI::GetTheme();
345 auto* viewer = ResolveCanvasViewer();
346 if (!viewer) {
347 return;
348 }
349
350 switch (selection_snapshot_.kind) {
352 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE
353 " Inspecting selected room object");
354 break;
356 ImGui::TextColored(theme.status_success,
358 " Inspecting %zu selected room objects",
360 break;
362 ImGui::TextColored(theme.status_success,
363 ICON_MD_DOOR_FRONT " Inspecting selected door");
364 break;
366 ImGui::TextColored(theme.status_success,
367 ICON_MD_PERSON " Inspecting selected sprite");
368 break;
370 ImGui::TextColored(theme.status_success,
371 ICON_MD_INVENTORY " Inspecting selected item");
372 break;
375 ImGui::TextColored(
376 theme.status_success, ICON_MD_SELECT_ALL " %s",
378 break;
380 default:
381 ImGui::TextColored(theme.text_secondary_gray,
382 ICON_MD_TUNE " Waiting for selection");
383 break;
384 }
385}
386
388 auto* viewer = ResolveCanvasViewer();
389 if (!viewer || !selection_snapshot_.HasSelection()) {
390 return;
391 }
392
393 ImGui::Spacing();
394 const bool can_copy_selection = selection_snapshot_.object_count > 0 ||
397
399 DrawWrappedInspectorActions(
400 {{ICON_MD_CONTENT_COPY " Copy", [this]() { CopySelectedObjects(); }},
401 {ICON_MD_CONTENT_PASTE " Paste", [this]() { PasteObjects(); }},
402 {ICON_MD_FILTER_NONE " Duplicate",
403 [this]() { DuplicateSelectedObjects(); }},
404 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
405 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }}});
407 DrawWrappedInspectorActions(
408 {{ICON_MD_FILTER_NONE " Duplicate",
409 [this]() { DuplicateSelectedSprite(); }},
410 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
411 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }},
412 {GetDeleteAllSelectedTypeLabel(selection_snapshot_.kind),
413 [this]() { DeleteAllSelectedTypeInRoom(); }}});
416 DrawWrappedInspectorActions(
417 {{ICON_MD_CONTENT_COPY " Copy", [this]() { CopySelectedObjects(); },
418 can_copy_selection},
419 {ICON_MD_CONTENT_PASTE " Paste", [this]() { PasteObjects(); }},
420 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
421 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }}});
422 } else {
423 DrawWrappedInspectorActions(
424 {{ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
425 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }},
426 {GetDeleteAllSelectedTypeLabel(selection_snapshot_.kind),
427 [this]() { DeleteAllSelectedTypeInRoom(); }}});
428 }
429
430 ImGui::Separator();
431}
432
434 const auto& theme = AgentUI::GetTheme();
435 auto* viewer = ResolveCanvasViewer();
436 if (!viewer || !viewer->HasRooms()) {
437 return;
438 }
439
440 auto& interaction = viewer->object_interaction();
441 auto selected = interaction.GetSelectedObjectIndices();
442 if (selected.empty()) {
443 return;
444 }
445
446 if (selected.size() == 1) {
447 const auto& objects = object_editor_->GetObjects();
448 if (selected[0] < objects.size()) {
449 const auto& obj = objects[selected[0]];
450 const auto semantics = zelda3::GetObjectLayerSemantics(obj);
451 ImGui::TextColored(theme.status_success, tr("Object #%zu · 0x%03X %s"),
452 selected[0], obj.id_,
453 zelda3::GetObjectName(obj.id_).c_str());
454 DrawInspectorSummaryGrid(
455 "##SelectedObjectInfo",
456 {{"Position", absl::StrFormat("(%d, %d)", obj.x_, obj.y_)},
457 {zelda3::UsesRoomObjectStream(obj) ? "Object stream"
458 : "Special layer",
459 GetStoredPlacementLabel(obj)},
460 {"Size", absl::StrFormat("0x%02X", obj.size_)},
461 {zelda3::UsesRoomObjectStream(obj) ? "Draws" : "Role",
463 ? zelda3::EffectiveBgLayerLabel(semantics.effective_bg_layer)
464 : "Special-table layer selector"}});
465 ImGui::Spacing();
466 }
467 return;
468 }
469
470 ImGui::TextColored(theme.status_success, tr("%zu objects selected"),
471 selected.size());
472 DrawInspectorSummaryGrid(
473 "##SelectedObjectMultiInfo",
474 {{"Selection", absl::StrFormat("%zu objects", selected.size())},
475 {"Scope", "Bulk object actions and property edits"},
476 {"Movement", "Use Arrow Keys to nudge all selected objects"},
477 {"Refine", "Shift-click or drag in the room canvas"}});
478 ImGui::Spacing();
479}
480
482 auto* viewer = ResolveCanvasViewer();
483 if (!viewer || !viewer->HasRooms()) {
484 return;
485 }
486
487 const auto& theme = AgentUI::GetTheme();
488 const auto entity = viewer->object_interaction().GetSelectedEntity();
489 if (entity.type != EntityType::Door || viewer->current_room_id() < 0 ||
490 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
491 return;
492 }
493
494 const auto& room = (*viewer->rooms())[viewer->current_room_id()];
495 const auto& doors = room.GetDoors();
496 if (entity.index >= doors.size()) {
497 return;
498 }
499
500 const auto& door = doors[entity.index];
501 const auto [tile_x, tile_y] = door.GetTileCoords();
502 const auto [pixel_x, pixel_y] = door.GetPixelCoords();
503
504 ImGui::TextColored(theme.status_success, tr("Door #%zu · %s"), entity.index,
505 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
506 DrawInspectorSummaryGrid(
507 "##SelectedDoorInfo",
508 {{"Direction", std::string(zelda3::GetDoorDirectionName(door.direction))},
509 {"Position", absl::StrFormat("0x%02X", door.position)},
510 {"Tile", absl::StrFormat("(%d, %d)", tile_x, tile_y)},
511 {"Pixel", absl::StrFormat("(%d, %d)", pixel_x, pixel_y)}});
512
513 const std::string current_type_name(zelda3::GetDoorTypeName(door.type));
514 ImGui::SetNextItemWidth(-1);
515 if (ImGui::BeginCombo(tr("Door Type##SelectionDoorType"),
516 current_type_name.c_str())) {
517 for (const auto door_type : kInspectorDoorTypes) {
518 const bool is_current = door.type == door_type;
519 const std::string entry_label = absl::StrFormat(
520 "0x%02X %s", static_cast<int>(door_type),
521 std::string(zelda3::GetDoorTypeName(door_type)).c_str());
522 if (ImGui::Selectable(entry_label.c_str(), is_current) && !is_current) {
523 viewer->object_interaction()
525 .door_handler()
526 .MutateDoorType(entity.index, door_type);
527 }
528 if (is_current) {
529 ImGui::SetItemDefaultFocus();
530 }
531 }
532 ImGui::EndCombo();
533 }
534
535 const int neighbor_id =
536 NeighborRoomId(viewer->current_room_id(), door.direction);
537 std::optional<size_t> reciprocal_index;
538 if (neighbor_id >= 0 &&
539 neighbor_id < static_cast<int>(viewer->rooms()->size())) {
540 const auto opposite = OppositeDir(door.direction);
541 const auto& neighbor_doors = (*viewer->rooms())[neighbor_id].GetDoors();
542 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
543 const auto& neighbor_door = neighbor_doors[i];
544 if (neighbor_door.direction == opposite &&
545 neighbor_door.position == door.position) {
546 reciprocal_index = i;
547 break;
548 }
549 }
550 if (!reciprocal_index) {
551 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
552 if (neighbor_doors[i].direction == opposite) {
553 reciprocal_index = i;
554 break;
555 }
556 }
557 }
558 }
559
560 const bool can_jump = reciprocal_index.has_value() &&
561 static_cast<bool>(on_jump_to_reciprocal_door_);
562 if (!can_jump) {
563 ImGui::BeginDisabled();
564 }
565 if (ImGui::Button(ICON_MD_ARROW_FORWARD " Jump to Reciprocal",
566 ImVec2(-1, 0)) &&
567 can_jump) {
568 on_jump_to_reciprocal_door_(neighbor_id, *reciprocal_index);
569 }
570 if (!can_jump) {
571 ImGui::EndDisabled();
572 }
573
574 ImGui::Spacing();
575 ImGui::TextColored(theme.text_secondary_gray,
576 tr("Browse and place additional doors from Door Editor."));
577}
578
580 auto* viewer = ResolveCanvasViewer();
581 if (!viewer || !viewer->HasRooms()) {
582 return;
583 }
584
585 const auto& theme = AgentUI::GetTheme();
586 const auto entity = viewer->object_interaction().GetSelectedEntity();
587 if (entity.type != EntityType::Sprite || viewer->current_room_id() < 0 ||
588 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
589 return;
590 }
591
592 auto& sprites = (*viewer->rooms())[viewer->current_room_id()].GetSprites();
593 if (entity.index >= sprites.size()) {
594 return;
595 }
596
597 const auto& sprite = sprites[entity.index];
598 ImGui::TextColored(theme.status_success, tr("Sprite #%zu · 0x%02X %s"),
599 entity.index, sprite.id(),
600 zelda3::ResolveSpriteName(sprite.id()));
601 if (sprite.IsOverlord()) {
602 ImGui::SameLine();
603 ImGui::TextColored(theme.status_warning, ICON_MD_STAR " OVERLORD");
604 }
605
606 const char* key_drop_label = sprite.key_drop() == 1 ? "Small Key"
607 : sprite.key_drop() == 2 ? "Big Key"
608 : "None";
609 DrawInspectorSummaryGrid(
610 "##SelectedSpriteInfo",
611 {{"Position", absl::StrFormat("(%d, %d)", sprite.x(), sprite.y())},
612 {"Layer", absl::StrFormat("%d", sprite.layer())},
613 {"Subtype", absl::StrFormat("%d", sprite.subtype())},
614 {"Key Drop", key_drop_label}});
615
616 int subtype = sprite.subtype();
617 ImGui::SetNextItemWidth(120.0f);
618 if (ImGui::Combo(tr("Subtype##SelectionSpriteSubtype"), &subtype,
619 "0\0001\0002\0003\0004\0005\0006\0007\0")) {
620 (void)MutateSelectedSprite(viewer,
621 [subtype](zelda3::Sprite& mutable_sprite) {
622 mutable_sprite.set_subtype(subtype);
623 });
624 }
625
626 int layer = sprite.layer();
627 ImGui::SetNextItemWidth(140.0f);
628 if (ImGui::Combo(tr("Layer##SelectionSpriteLayer"), &layer,
629 "Upper (0)\0Lower (1)\0Both (2)\0")) {
630 (void)MutateSelectedSprite(viewer, [layer](zelda3::Sprite& mutable_sprite) {
631 mutable_sprite.set_layer(layer);
632 });
633 }
634
635 int key_drop = sprite.key_drop();
636 ImGui::Text(tr("Key Drop:"));
637 ImGui::SameLine();
638 if (ImGui::RadioButton(tr("None##SelectionKeyNone"), key_drop == 0)) {
639 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
640 mutable_sprite.set_key_drop(0);
641 });
642 }
643 ImGui::SameLine();
644 if (ImGui::RadioButton(ICON_MD_KEY " Small##SelectionKeySmall",
645 key_drop == 1)) {
646 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
647 mutable_sprite.set_key_drop(1);
648 });
649 }
650 ImGui::SameLine();
651 if (ImGui::RadioButton(ICON_MD_VPN_KEY " Big##SelectionKeyBig",
652 key_drop == 2)) {
653 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
654 mutable_sprite.set_key_drop(2);
655 });
656 }
657
658 ImGui::Spacing();
659 ImGui::TextColored(
660 theme.text_secondary_gray,
661 tr("Drag in the canvas to reposition. Browse and place more "
662 "sprites from Sprite Editor."));
663}
664
666 auto* viewer = ResolveCanvasViewer();
667 if (!viewer || !viewer->HasRooms()) {
668 return;
669 }
670
671 const auto& theme = AgentUI::GetTheme();
672 const auto entity = viewer->object_interaction().GetSelectedEntity();
673 if (entity.type != EntityType::Item || viewer->current_room_id() < 0 ||
674 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
675 return;
676 }
677
678 auto& items = (*viewer->rooms())[viewer->current_room_id()].GetPotItems();
679 if (entity.index >= items.size()) {
680 return;
681 }
682
683 static constexpr std::array<const char*, 28> kPotItemNames = {{
684 "Nothing", "Green Rupee", "Rock", "Bee", "Health",
685 "Bomb", "Heart", "Blue Rupee", "Key", "Arrow",
686 "Bomb", "Heart", "Magic", "Full Magic", "Cucco",
687 "Green Soldier", "Bush Stal", "Blue Soldier", "Landmine", "Heart",
688 "Fairy", "Heart", "Nothing", "Hole", "Warp",
689 "Staircase", "Bombable", "Switch",
690 }};
691
692 const auto& item = items[entity.index];
693 const char* item_name =
694 item.item < kPotItemNames.size() ? kPotItemNames[item.item] : "Unknown";
695 ImGui::TextColored(theme.status_success, tr("Item #%zu · 0x%02X %s"),
696 entity.index, item.item, item_name);
697 DrawInspectorSummaryGrid(
698 "##SelectedItemInfo",
699 {{"Tile", absl::StrFormat("(%d, %d)", item.GetTileX(), item.GetTileY())},
700 {"Raw", absl::StrFormat("0x%04X", item.position)},
701 {"Kind", item_name},
702 {"Value", absl::StrFormat("0x%02X", item.item)}});
703
704 int item_type = item.item;
705 ImGui::SetNextItemWidth(-1);
706 if (ImGui::BeginCombo(
707 tr("Item Type##SelectionItemType"),
708 absl::StrFormat("0x%02X %s", item.item, item_name).c_str())) {
709 for (size_t i = 0; i < kPotItemNames.size(); ++i) {
710 const bool is_current = item.item == static_cast<uint8_t>(i);
711 const std::string label =
712 absl::StrFormat("0x%02zX %s", i, kPotItemNames[i]);
713 if (ImGui::Selectable(label.c_str(), is_current) && !is_current) {
714 (void)MutateSelectedItem(viewer, [i](zelda3::PotItem& mutable_item) {
715 mutable_item.item = static_cast<uint8_t>(i);
716 });
717 }
718 if (is_current) {
719 ImGui::SetItemDefaultFocus();
720 }
721 }
722 ImGui::EndCombo();
723 }
724
725 ImGui::Spacing();
726 ImGui::TextColored(
727 theme.text_secondary_gray,
728 tr("Drag in the canvas to reposition. Browse and place more "
729 "items from Item Editor."));
730}
731
733 const auto& theme = AgentUI::GetTheme();
734
735 ImGui::Spacing();
736 ImGui::TextColored(theme.text_secondary_gray, ICON_MD_MOUSE
737 " Click any room object, door, sprite, or item in the "
738 "canvas to inspect it here.");
739 ImGui::TextColored(theme.text_secondary_gray, ICON_MD_OPEN_WITH
740 " Use Shift-click and drag in the room to edit multiple "
741 "objects together. Use the placement panels to browse "
742 "new objects, doors, sprites, and items.");
743}
744
746 if (!show_shortcut_help_) {
747 return;
748 }
749
750 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
751 if (ImGui::Begin("Keyboard Shortcuts##DungeonSelectionInspector",
752 &show_shortcut_help_, ImGuiWindowFlags_NoCollapse)) {
753 const auto& theme = AgentUI::GetTheme();
754 auto shortcut_row = [&](const char* keys, const char* desc) {
755 ImGui::TextColored(theme.status_warning, "%-18s", keys);
756 ImGui::SameLine();
757 ImGui::TextUnformatted(desc);
758 };
759
760 ImGui::TextColored(theme.status_success, ICON_MD_KEYBOARD " Selection");
761 ImGui::Separator();
762 shortcut_row("Ctrl+A", "Select all objects");
763 shortcut_row("Ctrl+Shift+A", "Deselect all");
764 shortcut_row("Tab / Shift+Tab", "Cycle selection");
765 shortcut_row("Escape", "Clear selection");
766
767 ImGui::Spacing();
768 ImGui::TextColored(theme.status_success, ICON_MD_EDIT " Editing");
769 ImGui::Separator();
770 shortcut_row("Delete", "Remove selected");
771 shortcut_row("Ctrl+D", "Duplicate selected");
772 shortcut_row("Ctrl+C", "Copy selected");
773 shortcut_row("Ctrl+V", "Paste");
774 shortcut_row("Ctrl+Z", "Undo");
775 shortcut_row("Ctrl+Shift+Z", "Redo");
776
777 ImGui::Spacing();
778 ImGui::TextColored(theme.status_success, ICON_MD_OPEN_WITH " Movement");
779 ImGui::Separator();
780 shortcut_row("Arrow Keys", "Nudge selected (1px)");
781 }
782 ImGui::End();
783}
784
786 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
787 return;
788 }
789
790 const ImGuiIO& io = ImGui::GetIO();
791
792 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
794 }
795 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
797 }
798 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
801 }
802 }
803 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
808 }
809 }
810 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
813 }
814 }
815 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
816 PasteObjects();
817 }
818 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
819 object_editor_->Undo();
820 }
821 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
822 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
823 object_editor_->Redo();
824 }
825
826 if (!io.KeyCtrl) {
827 int dx = 0;
828 int dy = 0;
829 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
830 dx = -1;
831 }
832 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
833 dx = 1;
834 }
835 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
836 dy = -1;
837 }
838 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
839 dy = 1;
840 }
841 if ((dx != 0 || dy != 0) && selection_snapshot_.HasSelection()) {
842 NudgeCurrentSelection(dx, dy);
843 }
844 }
845
846 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
847 CycleObjectSelection(io.KeyShift ? -1 : 1);
848 }
849
850 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
852 }
853
854 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
856 }
857}
858
860 auto* viewer = ResolveCanvasViewer();
861 if (!viewer || !object_editor_) {
862 return;
863 }
864
865 auto& interaction = viewer->object_interaction();
866 const auto& objects = object_editor_->GetObjects();
867 std::vector<size_t> all_indices;
868 all_indices.reserve(objects.size());
869 for (size_t i = 0; i < objects.size(); ++i) {
870 all_indices.push_back(i);
871 }
872 interaction.SetSelectedObjects(all_indices);
873}
874
876 auto* viewer = ResolveCanvasViewer();
877 if (!viewer) {
878 return;
879 }
882}
883
885 auto* viewer = ResolveCanvasViewer();
886 if (!viewer) {
887 return;
888 }
889
891}
892
894 auto* viewer = ResolveCanvasViewer();
895 if (!object_editor_ || !viewer) {
896 return;
897 }
898
899 auto& interaction = viewer->object_interaction();
900 const auto& selected = interaction.GetSelectedObjectIndices();
901 if (selected.empty()) {
902 return;
903 }
904
905 std::vector<size_t> new_indices;
906 for (size_t idx : selected) {
907 auto new_idx = object_editor_->DuplicateObject(idx, 1, 1);
908 if (new_idx.has_value()) {
909 new_indices.push_back(*new_idx);
910 }
911 }
912 interaction.SetSelectedObjects(new_indices);
913}
914
916 auto* viewer = ResolveCanvasViewer();
917 if (!viewer) {
918 return;
919 }
921}
922
924 auto* viewer = ResolveCanvasViewer();
925 if (!viewer) {
926 return;
927 }
929}
930
932 auto* viewer = ResolveCanvasViewer();
933 if (!viewer || !viewer->HasRooms()) {
934 return;
935 }
936
937 auto& coordinator = viewer->object_interaction().entity_coordinator();
938 switch (selection_snapshot_.kind) {
940 coordinator.door_handler().DeleteAll();
941 break;
943 coordinator.sprite_handler().DeleteAll();
944 break;
946 coordinator.item_handler().DeleteAll();
947 break;
948 default:
949 break;
950 }
951}
952
954 auto* viewer = ResolveCanvasViewer();
955 if (!viewer || !viewer->HasRooms()) {
956 return;
957 }
958
959 auto& interaction = viewer->object_interaction();
960 auto& handler = interaction.entity_coordinator().sprite_handler();
961 const auto selected_index = handler.GetSelectedIndex();
962 if (!selected_index.has_value() || viewer->current_room_id() < 0 ||
963 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
964 return;
965 }
966
967 auto& room = (*viewer->rooms())[viewer->current_room_id()];
968 auto& sprites = room.GetSprites();
969 if (*selected_index >= sprites.size()) {
970 return;
971 }
972
973 if (auto* ctx = handler.context()) {
974 ctx->NotifyMutation(MutationDomain::kSprites);
975 }
976 sprites.push_back(sprites[*selected_index]);
977 room.MarkSpritesDirty();
978 if (auto* ctx = handler.context()) {
979 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
980 }
981 handler.SelectSprite(sprites.size() - 1);
982}
983
985 auto* viewer = ResolveCanvasViewer();
986 if (!viewer) {
987 return;
988 }
990}
991
993 auto* viewer = ResolveCanvasViewer();
994 if (!viewer) {
995 return;
996 }
997
999}
1000
1002 auto* viewer = ResolveCanvasViewer();
1003 if (!viewer) {
1004 return;
1005 }
1006
1007 viewer->object_interaction().NudgeSelected(dx, dy);
1008}
1009
1011 auto* viewer = ResolveCanvasViewer();
1012 if (!viewer || !object_editor_) {
1013 return;
1014 }
1015
1016 auto& interaction = viewer->object_interaction();
1017 const auto& selected = interaction.GetSelectedObjectIndices();
1018 const auto& objects = object_editor_->GetObjects();
1019 const size_t total_objects = objects.size();
1020 if (total_objects == 0) {
1021 return;
1022 }
1023
1024 const size_t current_idx = selected.empty() ? 0 : selected.front();
1025 const size_t next_idx =
1026 (current_idx + direction + total_objects) % total_objects;
1027 interaction.SetSelectedObjects({next_idx});
1028 ScrollToObject(next_idx);
1029}
1030
1032 auto* viewer = ResolveCanvasViewer();
1033 if (!viewer || !object_editor_) {
1034 return;
1035 }
1036
1037 const auto& objects = object_editor_->GetObjects();
1038 if (index >= objects.size()) {
1039 return;
1040 }
1041
1042 const auto& obj = objects[index];
1043 viewer->ScrollToTile(obj.x(), obj.y());
1044}
1045
1046} // namespace yaze::editor
bool MutateDoorType(size_t index, zelda3::DoorType new_type)
Change the type of a door in place, re-encoding ROM bytes.
DungeonObjectInteraction & object_interaction()
void ScrollToTile(int tile_x, int tile_y)
Handles object selection, placement, and interaction within the dungeon canvas.
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
void SetSelectionChangeCallback(std::function< void()> callback)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
SpriteInteractionHandler & sprite_handler()
void DeleteSelectedEntity()
Delete currently selected entity.
std::optional< size_t > GetSelectedIndex() const
Get selected item index.
void Draw(bool *p_open) override
Draw the panel content.
std::function< DungeonCanvasViewer *()> canvas_viewer_provider_
ObjectEditorContent(std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr)
DungeonSelectionSnapshot selection_snapshot_
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::function< void(int, size_t)> on_jump_to_reciprocal_door_
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
std::optional< size_t > GetSelectedIndex() const
Get selected sprite index.
A class for managing sprites in the overworld and underworld.
Definition sprite.h:37
void set_subtype(int subtype)
Definition sprite.h:125
void set_layer(int layer)
Definition sprite.h:126
auto set_key_drop(int key)
Definition sprite.h:118
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_FILTER_NONE
Definition icons.h:771
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_INVENTORY
Definition icons.h:1011
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_HELP_OUTLINE
Definition icons.h:935
#define ICON_MD_MOUSE
Definition icons.h:1251
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_KEY
Definition icons.h:1026
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_VPN_KEY
Definition icons.h:2113
#define ICON_MD_DELETE_SWEEP
Definition icons.h:533
const AgentUITheme & GetTheme()
void DrawWrappedInspectorActions(std::initializer_list< InspectorAction > actions)
const char * GetDeleteAllSelectedTypeLabel(DungeonSelectionKind kind)
bool MutateSelectedItem(DungeonCanvasViewer *viewer, std::function< void(zelda3::PotItem &)> mutator)
bool MutateSelectedSprite(DungeonCanvasViewer *viewer, std::function< void(zelda3::Sprite &)> mutator)
constexpr std::array< zelda3::DoorType, 20 > kInspectorDoorTypes
void DrawInspectorSummaryGrid(const char *table_id, std::initializer_list< InspectorStat > stats)
Editors are the view controllers for the application.
bool SyncObjectEditorSelectionToCanvas(DungeonObjectInteraction &interaction, const std::vector< size_t > &editor_selected_indices)
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
zelda3::DoorDirection OppositeDir(zelda3::DoorDirection dir)
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ SmallKeyStairsUp
Small key stairs (upwards)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ ExitMarker
Exit marker.
@ DoubleSidedShutter
Double sided shutter door.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284