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"
14#include "imgui/imgui.h"
19
20namespace yaze::editor {
21
23 DungeonObjectInteraction& interaction,
24 const std::vector<size_t>& editor_selected_indices) {
25 if (interaction.GetSelectedObjectIndices() == editor_selected_indices) {
26 return false;
27 }
28 // Canvas selection callbacks mirror back into DungeonObjectEditor. Copy the
29 // target first so those callbacks cannot invalidate the source vector while
30 // ObjectSelection is being rebuilt.
31 const std::vector<size_t> target_indices = editor_selected_indices;
32 interaction.SetSelectedObjects(target_indices);
33 return true;
34}
35
36namespace {
37
38using InspectorStat = std::pair<const char*, std::string>;
39
41 const char* label = "";
42 std::function<void()> action;
43 bool enabled = true;
44};
45
46const char* GetStoredPlacementLabel(const zelda3::RoomObject& object) {
47 if (zelda3::UsesRoomObjectStream(object)) {
48 switch (object.GetLayerValue()) {
49 case 0:
50 return "Primary";
51 case 1:
52 return "BG2 overlay";
53 case 2:
54 return "BG1 overlay";
55 default:
56 return "Unknown";
57 }
58 }
59 switch (object.GetLayerValue()) {
60 case 0:
61 return "Upper layer (BG1)";
62 case 1:
63 return "Lower layer (BG2)";
64 default:
65 return "Unknown";
66 }
67}
68
70
72 std::function<void(zelda3::Sprite&)> mutator) {
73 if (viewer == nullptr || viewer->rooms() == nullptr) {
74 return false;
75 }
76
77 auto& interaction = viewer->object_interaction();
78 auto& coordinator = interaction.entity_coordinator();
79 auto& handler = coordinator.sprite_handler();
80 const auto selected_index = handler.GetSelectedIndex();
81 if (!selected_index.has_value()) {
82 return false;
83 }
84
85 auto* room = &(*viewer->rooms())[viewer->current_room_id()];
86 auto& sprites = room->GetSprites();
87 if (*selected_index >= sprites.size()) {
88 return false;
89 }
90
91 if (auto* ctx = handler.context()) {
92 ctx->NotifyMutation(MutationDomain::kSprites);
93 }
94 mutator(sprites[*selected_index]);
95 room->MarkSpritesDirty();
96 if (auto* ctx = handler.context()) {
97 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
98 ctx->NotifyEntityChanged();
99 }
100 return true;
101}
102
104 std::function<void(zelda3::PotItem&)> mutator) {
105 if (viewer == nullptr || viewer->rooms() == nullptr) {
106 return false;
107 }
108
109 auto& interaction = viewer->object_interaction();
110 auto& coordinator = interaction.entity_coordinator();
111 auto& handler = coordinator.item_handler();
112 const auto selected_index = handler.GetSelectedIndex();
113 if (!selected_index.has_value()) {
114 return false;
115 }
116
117 auto* room = &(*viewer->rooms())[viewer->current_room_id()];
118 auto& items = room->GetPotItems();
119 if (*selected_index >= items.size()) {
120 return false;
121 }
122
123 if (auto* ctx = handler.context()) {
124 ctx->NotifyMutation(MutationDomain::kItems);
125 }
126 mutator(items[*selected_index]);
127 room->MarkPotItemsDirty();
128 if (auto* ctx = handler.context()) {
129 ctx->NotifyInvalidateCache(MutationDomain::kItems);
130 ctx->NotifyEntityChanged();
131 }
132 return true;
133}
134
135void DrawInspectorSummaryGrid(const char* table_id,
136 std::initializer_list<InspectorStat> stats) {
137 if (stats.size() == 0) {
138 return;
139 }
140
141 if (!ImGui::BeginTable(
142 table_id, 2,
143 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
144 return;
145 }
146
147 const auto& theme = AgentUI::GetTheme();
148 for (const auto& [label, value] : stats) {
149 ImGui::TableNextColumn();
150 ImGui::BeginGroup();
151 ImGui::TextColored(theme.text_secondary_gray, "%s", label);
152 ImGui::TextWrapped("%s", value.c_str());
153 ImGui::EndGroup();
154 }
155 ImGui::EndTable();
156}
157
159 std::initializer_list<InspectorAction> actions) {
160 if (actions.size() == 0) {
161 return;
162 }
163
164 const ImGuiStyle& style = ImGui::GetStyle();
165 const float spacing = style.ItemSpacing.x;
166 const float content_width = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
167 const float line_right =
168 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
169 bool first = true;
170
171 for (const InspectorAction& action : actions) {
172 const float desired_width = ImGui::CalcTextSize(action.label).x +
173 style.FramePadding.x * 2.0f + 10.0f;
174 const float button_width =
175 std::min(std::max(84.0f, desired_width), content_width);
176
177 if (!first) {
178 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
179 if (next_x <= line_right) {
180 ImGui::SameLine(0.0f, spacing);
181 }
182 }
183 first = false;
184
185 if (!action.enabled) {
186 ImGui::BeginDisabled();
187 }
188 if (ImGui::Button(action.label, ImVec2(button_width, 0.0f)) &&
189 action.enabled && action.action) {
190 action.action();
191 }
192 if (!action.enabled) {
193 ImGui::EndDisabled();
194 }
195 }
196}
197
199 switch (kind) {
201 return ICON_MD_DELETE_SWEEP " Delete All Doors";
203 return ICON_MD_DELETE_SWEEP " Delete All Sprites";
205 return ICON_MD_DELETE_SWEEP " Delete All Items";
208 return ICON_MD_DELETE_SWEEP " Delete All";
209 default:
210 return ICON_MD_DELETE_SWEEP " Delete All";
211 }
212}
213
214} // namespace
215
217 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
218 : object_editor_(std::move(object_editor)) {}
219
227
230 return;
231 }
232
233 auto& interaction = canvas_viewer_->object_interaction();
234 interaction.SetSelectionChangeCallback([this]() { OnSelectionChanged(); });
235 interaction.SetEntityChangedCallback([this]() { OnSelectionChanged(); });
236
239}
240
252
254 auto* viewer = ResolveCanvasViewer();
255 if (!viewer) {
258 return;
259 }
260
262
263 if (!object_editor_) {
264 return;
265 }
266
267 auto indices = viewer->object_interaction().GetSelectedObjectIndices();
268 (void)object_editor_->ClearSelection();
269 for (size_t idx : indices) {
270 (void)object_editor_->AddToSelection(idx);
271 }
272}
273
286
287void ObjectEditorContent::Draw(bool* p_open) {
288 (void)p_open;
289 auto* viewer = ResolveCanvasViewer();
290 const auto& theme = AgentUI::GetTheme();
291
292 ImGui::AlignTextToFramePadding();
293 ImGui::TextColored(theme.text_info, ICON_MD_TUNE " Selection Inspector");
294 ImGui::SameLine();
295 if (ImGui::SmallButton(ICON_MD_HELP_OUTLINE " Shortcuts")) {
296 show_shortcut_help_ = true;
297 }
298 ImGui::Separator();
299
300 if (!viewer || !object_editor_) {
301 ImGui::TextDisabled(tr("Object editor unavailable"));
302 return;
303 }
304
308
311 object_editor_->DrawPropertyUI();
313 viewer->object_interaction(),
314 object_editor_->GetSelection().selected_objects);
323 ImGui::TextDisabled(
325 } else {
327 }
328
331}
332
334 const auto& theme = AgentUI::GetTheme();
335 auto* viewer = ResolveCanvasViewer();
336 if (!viewer) {
337 return;
338 }
339
340 switch (selection_snapshot_.kind) {
342 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE
343 " Inspecting selected room object");
344 break;
346 ImGui::TextColored(theme.status_success,
348 " Inspecting %zu selected room objects",
350 break;
352 ImGui::TextColored(theme.status_success,
353 ICON_MD_DOOR_FRONT " Inspecting selected door");
354 break;
356 ImGui::TextColored(theme.status_success,
357 ICON_MD_PERSON " Inspecting selected sprite");
358 break;
360 ImGui::TextColored(theme.status_success,
361 ICON_MD_INVENTORY " Inspecting selected item");
362 break;
365 ImGui::TextColored(
366 theme.status_success, ICON_MD_SELECT_ALL " %s",
368 break;
370 default:
371 ImGui::TextColored(theme.text_secondary_gray,
372 ICON_MD_TUNE " Waiting for selection");
373 break;
374 }
375}
376
378 auto* viewer = ResolveCanvasViewer();
379 if (!viewer || !selection_snapshot_.HasSelection()) {
380 return;
381 }
382
383 ImGui::Spacing();
384 const bool can_copy_selection = selection_snapshot_.object_count > 0 ||
387
389 DrawWrappedInspectorActions(
390 {{ICON_MD_CONTENT_COPY " Copy", [this]() { CopySelectedObjects(); }},
391 {ICON_MD_CONTENT_PASTE " Paste", [this]() { PasteObjects(); }},
392 {ICON_MD_FILTER_NONE " Duplicate",
393 [this]() { DuplicateSelectedObjects(); }},
394 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
395 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }}});
397 DrawWrappedInspectorActions(
398 {{ICON_MD_FILTER_NONE " Duplicate",
399 [this]() { DuplicateSelectedSprite(); }},
400 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
401 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }},
402 {GetDeleteAllSelectedTypeLabel(selection_snapshot_.kind),
403 [this]() { DeleteAllSelectedTypeInRoom(); }}});
406 DrawWrappedInspectorActions(
407 {{ICON_MD_CONTENT_COPY " Copy", [this]() { CopySelectedObjects(); },
408 can_copy_selection},
409 {ICON_MD_CONTENT_PASTE " Paste", [this]() { PasteObjects(); }},
410 {ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
411 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }}});
412 } else {
413 DrawWrappedInspectorActions(
414 {{ICON_MD_CLEAR " Clear", [this]() { DeselectAllObjects(); }},
415 {ICON_MD_DELETE " Delete", [this]() { DeleteCurrentSelection(); }},
416 {GetDeleteAllSelectedTypeLabel(selection_snapshot_.kind),
417 [this]() { DeleteAllSelectedTypeInRoom(); }}});
418 }
419
420 ImGui::Separator();
421}
422
424 const auto& theme = AgentUI::GetTheme();
425 auto* viewer = ResolveCanvasViewer();
426 if (!viewer || !viewer->HasRooms()) {
427 return;
428 }
429
430 auto& interaction = viewer->object_interaction();
431 auto selected = interaction.GetSelectedObjectIndices();
432 if (selected.empty()) {
433 return;
434 }
435
436 if (selected.size() == 1) {
437 const auto& objects = object_editor_->GetObjects();
438 if (selected[0] < objects.size()) {
439 const auto& obj = objects[selected[0]];
440 const auto semantics = zelda3::GetObjectLayerSemantics(obj);
441 ImGui::TextColored(theme.status_success, tr("Object #%zu · 0x%03X %s"),
442 selected[0], obj.id_,
443 zelda3::GetObjectName(obj.id_).c_str());
444 DrawInspectorSummaryGrid(
445 "##SelectedObjectInfo",
446 {{"Position", absl::StrFormat("(%d, %d)", obj.x_, obj.y_)},
447 {zelda3::UsesRoomObjectStream(obj) ? "Object stream"
448 : "Special layer",
449 GetStoredPlacementLabel(obj)},
450 {"Size", absl::StrFormat("0x%02X", obj.size_)},
451 {zelda3::UsesRoomObjectStream(obj) ? "Draws" : "Role",
454 : "Special-table layer selector"}});
455 ImGui::Spacing();
456 }
457 return;
458 }
459
460 ImGui::TextColored(theme.status_success, tr("%zu objects selected"),
461 selected.size());
462 DrawInspectorSummaryGrid(
463 "##SelectedObjectMultiInfo",
464 {{"Selection", absl::StrFormat("%zu objects", selected.size())},
465 {"Scope", "Bulk object actions and property edits"},
466 {"Movement", "Use Arrow Keys to nudge all selected objects"},
467 {"Refine", "Shift-click or drag in the room canvas"}});
468 ImGui::Spacing();
469}
470
472 auto* viewer = ResolveCanvasViewer();
473 if (!viewer || !viewer->HasRooms()) {
474 return;
475 }
476
477 const auto& theme = AgentUI::GetTheme();
478 const auto entity = viewer->object_interaction().GetSelectedEntity();
479 if (entity.type != EntityType::Door || viewer->current_room_id() < 0 ||
480 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
481 return;
482 }
483
484 const auto& room = (*viewer->rooms())[viewer->current_room_id()];
485 const auto& doors = room.GetDoors();
486 if (entity.index >= doors.size()) {
487 return;
488 }
489
490 const auto& door = doors[entity.index];
491 const auto [tile_x, tile_y] = door.GetTileCoords();
492 const auto [pixel_x, pixel_y] = door.GetPixelCoords();
493
494 ImGui::TextColored(theme.status_success, tr("Door #%zu · %s"), entity.index,
495 std::string(zelda3::GetDoorTypeName(door.type)).c_str());
496 DrawInspectorSummaryGrid(
497 "##SelectedDoorInfo",
498 {{"Direction", std::string(zelda3::GetDoorDirectionName(door.direction))},
499 {"Position", absl::StrFormat("0x%02X", door.position)},
500 {"Tile", absl::StrFormat("(%d, %d)", tile_x, tile_y)},
501 {"Pixel", absl::StrFormat("(%d, %d)", pixel_x, pixel_y)}});
502
503 const std::string current_type_name(zelda3::GetDoorTypeName(door.type));
504 ImGui::SetNextItemWidth(-1);
505 if (ImGui::BeginCombo(tr("Door Type##SelectionDoorType"),
506 current_type_name.c_str())) {
507 for (const auto door_type : kInspectorDoorTypes) {
508 const bool is_current = door.type == door_type;
509 const std::string entry_label = absl::StrFormat(
510 "0x%02X %s", static_cast<int>(door_type),
511 std::string(zelda3::GetDoorTypeName(door_type)).c_str());
512 if (ImGui::Selectable(entry_label.c_str(), is_current) && !is_current) {
513 viewer->object_interaction()
515 .door_handler()
516 .MutateDoorType(entity.index, door_type);
517 }
518 if (is_current) {
519 ImGui::SetItemDefaultFocus();
520 }
521 }
522 ImGui::EndCombo();
523 }
524
525 const int neighbor_id =
526 NeighborRoomId(viewer->current_room_id(), door.direction);
527 std::optional<size_t> reciprocal_index;
528 if (zelda3::IsRoomConnectionDoorType(door.type) && neighbor_id >= 0 &&
529 neighbor_id < static_cast<int>(viewer->rooms()->size())) {
530 const auto opposite = OppositeDir(door.direction);
531 const auto& neighbor_doors = (*viewer->rooms())[neighbor_id].GetDoors();
532 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
533 const auto& neighbor_door = neighbor_doors[i];
534 if (zelda3::IsRoomConnectionDoorType(neighbor_door.type) &&
535 neighbor_door.direction == opposite &&
536 neighbor_door.position == door.position) {
537 reciprocal_index = i;
538 break;
539 }
540 }
541 if (!reciprocal_index) {
542 for (size_t i = 0; i < neighbor_doors.size(); ++i) {
543 if (zelda3::IsRoomConnectionDoorType(neighbor_doors[i].type) &&
544 neighbor_doors[i].direction == opposite) {
545 reciprocal_index = i;
546 break;
547 }
548 }
549 }
550 }
551
552 const bool can_jump = reciprocal_index.has_value() &&
553 static_cast<bool>(on_jump_to_reciprocal_door_);
554 if (!can_jump) {
555 ImGui::BeginDisabled();
556 }
557 if (ImGui::Button(ICON_MD_ARROW_FORWARD " Jump to Reciprocal",
558 ImVec2(-1, 0)) &&
559 can_jump) {
560 on_jump_to_reciprocal_door_(neighbor_id, *reciprocal_index);
561 }
562 if (!can_jump) {
563 ImGui::EndDisabled();
564 }
565
566 ImGui::Spacing();
567 ImGui::TextColored(theme.text_secondary_gray,
568 tr("Browse and place additional doors from Door Editor."));
569}
570
572 auto* viewer = ResolveCanvasViewer();
573 if (!viewer || !viewer->HasRooms()) {
574 return;
575 }
576
577 const auto& theme = AgentUI::GetTheme();
578 const auto entity = viewer->object_interaction().GetSelectedEntity();
579 if (entity.type != EntityType::Sprite || viewer->current_room_id() < 0 ||
580 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
581 return;
582 }
583
584 auto& sprites = (*viewer->rooms())[viewer->current_room_id()].GetSprites();
585 if (entity.index >= sprites.size()) {
586 return;
587 }
588
589 const auto& sprite = sprites[entity.index];
590 ImGui::TextColored(theme.status_success, tr("Sprite #%zu · 0x%02X %s"),
591 entity.index, sprite.id(),
592 zelda3::ResolveSpriteName(sprite.id()));
593 if (sprite.IsOverlord()) {
594 ImGui::SameLine();
595 ImGui::TextColored(theme.status_warning, ICON_MD_STAR " OVERLORD");
596 }
597
598 const char* key_drop_label = sprite.key_drop() == 1 ? "Small Key"
599 : sprite.key_drop() == 2 ? "Big Key"
600 : "None";
601 DrawInspectorSummaryGrid(
602 "##SelectedSpriteInfo",
603 {{"Position", absl::StrFormat("(%d, %d)", sprite.x(), sprite.y())},
604 {"Layer", absl::StrFormat("%d", sprite.layer())},
605 {"Subtype", absl::StrFormat("%d", sprite.subtype())},
606 {"Key Drop", key_drop_label}});
607
608 int subtype = sprite.subtype();
609 ImGui::SetNextItemWidth(120.0f);
610 if (ImGui::Combo(tr("Subtype##SelectionSpriteSubtype"), &subtype,
611 "0\0001\0002\0003\0004\0005\0006\0007\0")) {
612 (void)MutateSelectedSprite(viewer,
613 [subtype](zelda3::Sprite& mutable_sprite) {
614 mutable_sprite.set_subtype(subtype);
615 });
616 }
617
618 int layer = sprite.layer();
619 ImGui::SetNextItemWidth(140.0f);
620 if (ImGui::Combo(tr("Layer##SelectionSpriteLayer"), &layer,
621 "Upper (0)\0Lower (1)\0Both (2)\0")) {
622 (void)MutateSelectedSprite(viewer, [layer](zelda3::Sprite& mutable_sprite) {
623 mutable_sprite.set_layer(layer);
624 });
625 }
626
627 int key_drop = sprite.key_drop();
628 ImGui::Text(tr("Key Drop:"));
629 ImGui::SameLine();
630 if (ImGui::RadioButton(tr("None##SelectionKeyNone"), key_drop == 0)) {
631 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
632 mutable_sprite.set_key_drop(0);
633 });
634 }
635 ImGui::SameLine();
636 if (ImGui::RadioButton(ICON_MD_KEY " Small##SelectionKeySmall",
637 key_drop == 1)) {
638 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
639 mutable_sprite.set_key_drop(1);
640 });
641 }
642 ImGui::SameLine();
643 if (ImGui::RadioButton(ICON_MD_VPN_KEY " Big##SelectionKeyBig",
644 key_drop == 2)) {
645 (void)MutateSelectedSprite(viewer, [](zelda3::Sprite& mutable_sprite) {
646 mutable_sprite.set_key_drop(2);
647 });
648 }
649
650 ImGui::Spacing();
651 ImGui::TextColored(
652 theme.text_secondary_gray,
653 tr("Drag in the canvas to reposition. Browse and place more "
654 "sprites from Sprite Editor."));
655}
656
658 auto* viewer = ResolveCanvasViewer();
659 if (!viewer || !viewer->HasRooms()) {
660 return;
661 }
662
663 const auto& theme = AgentUI::GetTheme();
664 const auto entity = viewer->object_interaction().GetSelectedEntity();
665 if (entity.type != EntityType::Item || viewer->current_room_id() < 0 ||
666 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
667 return;
668 }
669
670 auto& items = (*viewer->rooms())[viewer->current_room_id()].GetPotItems();
671 if (entity.index >= items.size()) {
672 return;
673 }
674
675 static constexpr std::array<const char*, 28> kPotItemNames = {{
676 "Nothing", "Green Rupee", "Rock", "Bee", "Health",
677 "Bomb", "Heart", "Blue Rupee", "Key", "Arrow",
678 "Bomb", "Heart", "Magic", "Full Magic", "Cucco",
679 "Green Soldier", "Bush Stal", "Blue Soldier", "Landmine", "Heart",
680 "Fairy", "Heart", "Nothing", "Hole", "Warp",
681 "Staircase", "Bombable", "Switch",
682 }};
683
684 const auto& item = items[entity.index];
685 const char* item_name =
686 item.item < kPotItemNames.size() ? kPotItemNames[item.item] : "Unknown";
687 ImGui::TextColored(theme.status_success, tr("Item #%zu · 0x%02X %s"),
688 entity.index, item.item, item_name);
689 DrawInspectorSummaryGrid(
690 "##SelectedItemInfo",
691 {{"Tile", absl::StrFormat("(%d, %d)", item.GetTileX(), item.GetTileY())},
692 {"Raw", absl::StrFormat("0x%04X", item.position)},
693 {"Kind", item_name},
694 {"Value", absl::StrFormat("0x%02X", item.item)}});
695
696 int item_type = item.item;
697 ImGui::SetNextItemWidth(-1);
698 if (ImGui::BeginCombo(
699 tr("Item Type##SelectionItemType"),
700 absl::StrFormat("0x%02X %s", item.item, item_name).c_str())) {
701 for (size_t i = 0; i < kPotItemNames.size(); ++i) {
702 const bool is_current = item.item == static_cast<uint8_t>(i);
703 const std::string label =
704 absl::StrFormat("0x%02zX %s", i, kPotItemNames[i]);
705 if (ImGui::Selectable(label.c_str(), is_current) && !is_current) {
706 (void)MutateSelectedItem(viewer, [i](zelda3::PotItem& mutable_item) {
707 mutable_item.item = static_cast<uint8_t>(i);
708 });
709 }
710 if (is_current) {
711 ImGui::SetItemDefaultFocus();
712 }
713 }
714 ImGui::EndCombo();
715 }
716
717 ImGui::Spacing();
718 ImGui::TextColored(
719 theme.text_secondary_gray,
720 tr("Drag in the canvas to reposition. Browse and place more "
721 "items from Item Editor."));
722}
723
727
729 if (!show_shortcut_help_) {
730 return;
731 }
732
733 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
734 if (ImGui::Begin("Keyboard Shortcuts##DungeonSelectionInspector",
735 &show_shortcut_help_, ImGuiWindowFlags_NoCollapse)) {
736 const auto& theme = AgentUI::GetTheme();
737 auto shortcut_row = [&](const char* keys, const char* desc) {
738 ImGui::TextColored(theme.status_warning, "%-18s", keys);
739 ImGui::SameLine();
740 ImGui::TextUnformatted(desc);
741 };
742
743 ImGui::TextColored(theme.status_success, ICON_MD_KEYBOARD " Selection");
744 ImGui::Separator();
745 shortcut_row("Ctrl+A", "Select all objects");
746 shortcut_row("Ctrl+Shift+A", "Deselect all");
747 shortcut_row("Tab / Shift+Tab", "Cycle selection");
748 shortcut_row("Escape", "Clear selection");
749
750 ImGui::Spacing();
751 ImGui::TextColored(theme.status_success, ICON_MD_EDIT " Editing");
752 ImGui::Separator();
753 shortcut_row("Delete", "Remove selected");
754 shortcut_row("Ctrl+D", "Duplicate selected");
755 shortcut_row("Ctrl+C", "Copy selected");
756 shortcut_row("Ctrl+V", "Paste");
757 shortcut_row("Ctrl+Z", "Undo");
758 shortcut_row("Ctrl+Shift+Z", "Redo");
759
760 ImGui::Spacing();
761 ImGui::TextColored(theme.status_success, ICON_MD_OPEN_WITH " Movement");
762 ImGui::Separator();
763 shortcut_row("Arrow Keys", "Nudge selected (1px)");
764 }
765 ImGui::End();
766}
767
769 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
770 return;
771 }
772
773 const ImGuiIO& io = ImGui::GetIO();
774 if (io.WantTextInput) {
775 return;
776 }
777
778 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
780 }
781 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
783 }
784 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
785 auto* viewer = ResolveCanvasViewer();
786 if (selection_snapshot_.HasSelection() && viewer != nullptr &&
787 !ImGui::IsAnyItemActive() && viewer->CanHandleRoomCanvasShortcut()) {
789 }
790 }
791 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
796 }
797 }
798 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
801 }
802 }
803 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
804 PasteObjects();
805 }
806 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
807 object_editor_->Undo();
808 }
809 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
810 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
811 object_editor_->Redo();
812 }
813
814 if (!io.KeyCtrl) {
815 int dx = 0;
816 int dy = 0;
817 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
818 dx = -1;
819 }
820 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
821 dx = 1;
822 }
823 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
824 dy = -1;
825 }
826 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
827 dy = 1;
828 }
829 if ((dx != 0 || dy != 0) && selection_snapshot_.HasSelection()) {
830 NudgeCurrentSelection(dx, dy);
831 }
832 }
833
834 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
835 CycleObjectSelection(io.KeyShift ? -1 : 1);
836 }
837
838 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
840 }
841
842 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
844 }
845}
846
848 auto* viewer = ResolveCanvasViewer();
849 if (!viewer || !object_editor_) {
850 return;
851 }
852
853 auto& interaction = viewer->object_interaction();
854 const auto& objects = object_editor_->GetObjects();
855 std::vector<size_t> all_indices;
856 all_indices.reserve(objects.size());
857 for (size_t i = 0; i < objects.size(); ++i) {
858 all_indices.push_back(i);
859 }
860 interaction.SetSelectedObjects(all_indices);
861}
862
864 auto* viewer = ResolveCanvasViewer();
865 if (!viewer) {
866 return;
867 }
870}
871
873 auto* viewer = ResolveCanvasViewer();
874 if (!viewer) {
875 return;
876 }
877
879}
880
882 auto* viewer = ResolveCanvasViewer();
883 if (!object_editor_ || !viewer) {
884 return;
885 }
886
887 auto& interaction = viewer->object_interaction();
888 const auto& selected = interaction.GetSelectedObjectIndices();
889 if (selected.empty()) {
890 return;
891 }
892
893 std::vector<size_t> new_indices;
894 for (size_t idx : selected) {
895 auto new_idx = object_editor_->DuplicateObject(idx, 1, 1);
896 if (new_idx.has_value()) {
897 new_indices.push_back(*new_idx);
898 }
899 }
900 interaction.SetSelectedObjects(new_indices);
901}
902
904 auto* viewer = ResolveCanvasViewer();
905 if (!viewer) {
906 return;
907 }
909}
910
912 auto* viewer = ResolveCanvasViewer();
913 if (!viewer) {
914 return;
915 }
917}
918
920 auto* viewer = ResolveCanvasViewer();
921 if (!viewer || !viewer->HasRooms()) {
922 return;
923 }
924
925 auto& coordinator = viewer->object_interaction().entity_coordinator();
926 switch (selection_snapshot_.kind) {
928 coordinator.door_handler().DeleteAll();
929 break;
931 coordinator.sprite_handler().DeleteAll();
932 break;
934 coordinator.item_handler().DeleteAll();
935 break;
936 default:
937 break;
938 }
939}
940
942 auto* viewer = ResolveCanvasViewer();
943 if (!viewer || !viewer->HasRooms()) {
944 return;
945 }
946
947 auto& interaction = viewer->object_interaction();
948 auto& handler = interaction.entity_coordinator().sprite_handler();
949 const auto selected_index = handler.GetSelectedIndex();
950 if (!selected_index.has_value() || viewer->current_room_id() < 0 ||
951 viewer->current_room_id() >= static_cast<int>(viewer->rooms()->size())) {
952 return;
953 }
954
955 auto& room = (*viewer->rooms())[viewer->current_room_id()];
956 auto& sprites = room.GetSprites();
957 if (*selected_index >= sprites.size()) {
958 return;
959 }
960
961 if (auto* ctx = handler.context()) {
962 ctx->NotifyMutation(MutationDomain::kSprites);
963 }
964 sprites.push_back(sprites[*selected_index]);
965 room.MarkSpritesDirty();
966 if (auto* ctx = handler.context()) {
967 ctx->NotifyInvalidateCache(MutationDomain::kSprites);
968 }
969 handler.SelectSprite(sprites.size() - 1);
970}
971
973 auto* viewer = ResolveCanvasViewer();
974 if (!viewer) {
975 return;
976 }
978}
979
981 auto* viewer = ResolveCanvasViewer();
982 if (!viewer) {
983 return;
984 }
985
987}
988
990 auto* viewer = ResolveCanvasViewer();
991 if (!viewer) {
992 return;
993 }
994
995 viewer->object_interaction().NudgeSelected(dx, dy);
996}
997
999 auto* viewer = ResolveCanvasViewer();
1000 if (!viewer || !object_editor_) {
1001 return;
1002 }
1003
1004 auto& interaction = viewer->object_interaction();
1005 const auto& selected = interaction.GetSelectedObjectIndices();
1006 const auto& objects = object_editor_->GetObjects();
1007 const size_t total_objects = objects.size();
1008 if (total_objects == 0) {
1009 return;
1010 }
1011
1012 const size_t current_idx = selected.empty() ? 0 : selected.front();
1013 const size_t next_idx =
1014 (current_idx + direction + total_objects) % total_objects;
1015 interaction.SetSelectedObjects({next_idx});
1016 ScrollToObject(next_idx);
1017}
1018
1020 auto* viewer = ResolveCanvasViewer();
1021 if (!viewer || !object_editor_) {
1022 return;
1023 }
1024
1025 const auto& objects = object_editor_->GetObjects();
1026 if (index >= objects.size()) {
1027 return;
1028 }
1029
1030 const auto& obj = objects[index];
1031 viewer->ScrollToTile(obj.x(), obj.y());
1032}
1033
1034} // 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:39
void set_subtype(int subtype)
Definition sprite.h:133
void set_layer(int layer)
Definition sprite.h:134
auto set_key_drop(int key)
Definition sprite.h:126
#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_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)
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)
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
EmptyStateOptions EmptySelectInCanvas(bool compact)
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
constexpr std::array< DoorType, 32 > GetPlaceableDoorTypes()
Get supported door types suitable for editor placement controls.
Definition door_types.h:364
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
Definition door_types.h:333
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:287