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",
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 if (io.WantTextInput) {
792 return;
793 }
794
795 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
797 }
798 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
800 }
801 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
804 }
805 }
806 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
811 }
812 }
813 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
816 }
817 }
818 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
819 PasteObjects();
820 }
821 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
822 object_editor_->Undo();
823 }
824 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
825 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
826 object_editor_->Redo();
827 }
828
829 if (!io.KeyCtrl) {
830 int dx = 0;
831 int dy = 0;
832 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
833 dx = -1;
834 }
835 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
836 dx = 1;
837 }
838 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
839 dy = -1;
840 }
841 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
842 dy = 1;
843 }
844 if ((dx != 0 || dy != 0) && selection_snapshot_.HasSelection()) {
845 NudgeCurrentSelection(dx, dy);
846 }
847 }
848
849 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
850 CycleObjectSelection(io.KeyShift ? -1 : 1);
851 }
852
853 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
855 }
856
857 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
859 }
860}
861
863 auto* viewer = ResolveCanvasViewer();
864 if (!viewer || !object_editor_) {
865 return;
866 }
867
868 auto& interaction = viewer->object_interaction();
869 const auto& objects = object_editor_->GetObjects();
870 std::vector<size_t> all_indices;
871 all_indices.reserve(objects.size());
872 for (size_t i = 0; i < objects.size(); ++i) {
873 all_indices.push_back(i);
874 }
875 interaction.SetSelectedObjects(all_indices);
876}
877
879 auto* viewer = ResolveCanvasViewer();
880 if (!viewer) {
881 return;
882 }
885}
886
888 auto* viewer = ResolveCanvasViewer();
889 if (!viewer) {
890 return;
891 }
892
894}
895
897 auto* viewer = ResolveCanvasViewer();
898 if (!object_editor_ || !viewer) {
899 return;
900 }
901
902 auto& interaction = viewer->object_interaction();
903 const auto& selected = interaction.GetSelectedObjectIndices();
904 if (selected.empty()) {
905 return;
906 }
907
908 std::vector<size_t> new_indices;
909 for (size_t idx : selected) {
910 auto new_idx = object_editor_->DuplicateObject(idx, 1, 1);
911 if (new_idx.has_value()) {
912 new_indices.push_back(*new_idx);
913 }
914 }
915 interaction.SetSelectedObjects(new_indices);
916}
917
919 auto* viewer = ResolveCanvasViewer();
920 if (!viewer) {
921 return;
922 }
924}
925
927 auto* viewer = ResolveCanvasViewer();
928 if (!viewer) {
929 return;
930 }
932}
933
935 auto* viewer = ResolveCanvasViewer();
936 if (!viewer || !viewer->HasRooms()) {
937 return;
938 }
939
940 auto& coordinator = viewer->object_interaction().entity_coordinator();
941 switch (selection_snapshot_.kind) {
943 coordinator.door_handler().DeleteAll();
944 break;
946 coordinator.sprite_handler().DeleteAll();
947 break;
949 coordinator.item_handler().DeleteAll();
950 break;
951 default:
952 break;
953 }
954}
955
957 auto* viewer = ResolveCanvasViewer();
958 if (!viewer || !viewer->HasRooms()) {
959 return;
960 }
961
962 auto& interaction = viewer->object_interaction();
963 auto& handler = interaction.entity_coordinator().sprite_handler();
964 const auto selected_index = handler.GetSelectedIndex();
965 if (!selected_index.has_value() || viewer->current_room_id() < 0 ||
966 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
967 return;
968 }
969
970 auto& room = (*viewer->rooms())[viewer->current_room_id()];
971 auto& sprites = room.GetSprites();
972 if (*selected_index >= sprites.size()) {
973 return;
974 }
975
976 if (auto* ctx = handler.context()) {
977 ctx->NotifyMutation(MutationDomain::kSprites);
978 }
979 sprites.push_back(sprites[*selected_index]);
980 room.MarkSpritesDirty();
981 if (auto* ctx = handler.context()) {
982 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
983 }
984 handler.SelectSprite(sprites.size() - 1);
985}
986
988 auto* viewer = ResolveCanvasViewer();
989 if (!viewer) {
990 return;
991 }
993}
994
996 auto* viewer = ResolveCanvasViewer();
997 if (!viewer) {
998 return;
999 }
1000
1002}
1003
1005 auto* viewer = ResolveCanvasViewer();
1006 if (!viewer) {
1007 return;
1008 }
1009
1010 viewer->object_interaction().NudgeSelected(dx, dy);
1011}
1012
1014 auto* viewer = ResolveCanvasViewer();
1015 if (!viewer || !object_editor_) {
1016 return;
1017 }
1018
1019 auto& interaction = viewer->object_interaction();
1020 const auto& selected = interaction.GetSelectedObjectIndices();
1021 const auto& objects = object_editor_->GetObjects();
1022 const size_t total_objects = objects.size();
1023 if (total_objects == 0) {
1024 return;
1025 }
1026
1027 const size_t current_idx = selected.empty() ? 0 : selected.front();
1028 const size_t next_idx =
1029 (current_idx + direction + total_objects) % total_objects;
1030 interaction.SetSelectedObjects({next_idx});
1031 ScrollToObject(next_idx);
1032}
1033
1035 auto* viewer = ResolveCanvasViewer();
1036 if (!viewer || !object_editor_) {
1037 return;
1038 }
1039
1040 const auto& objects = object_editor_->GetObjects();
1041 if (index >= objects.size()) {
1042 return;
1043 }
1044
1045 const auto& obj = objects[index];
1046 viewer->ScrollToTile(obj.x(), obj.y());
1047}
1048
1049} // 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
const char * ObjectRenderRoutingDisplayLabel(const ObjectLayerSemantics &semantics)
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 * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284