7#include <initializer_list>
11#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
23 const std::vector<size_t>& editor_selected_indices) {
30 const std::vector<size_t> target_indices = editor_selected_indices;
40 const char* label =
"";
47 switch (
object.GetLayerValue()) {
58 switch (
object.GetLayerValue()) {
60 return "Upper layer (BG1)";
62 return "Lower layer (BG2)";
83 if (viewer ==
nullptr || viewer->
rooms() ==
nullptr) {
91 if (!selected_index.has_value()) {
96 auto& sprites = room->GetSprites();
97 if (*selected_index >= sprites.size()) {
101 if (
auto* ctx = handler.context()) {
104 mutator(sprites[*selected_index]);
105 room->MarkSpritesDirty();
106 if (
auto* ctx = handler.context()) {
108 ctx->NotifyEntityChanged();
115 if (viewer ==
nullptr || viewer->
rooms() ==
nullptr) {
123 if (!selected_index.has_value()) {
128 auto& items = room->GetPotItems();
129 if (*selected_index >= items.size()) {
133 if (
auto* ctx = handler.context()) {
136 mutator(items[*selected_index]);
137 room->MarkPotItemsDirty();
138 if (
auto* ctx = handler.context()) {
140 ctx->NotifyEntityChanged();
146 std::initializer_list<InspectorStat> stats) {
147 if (stats.size() == 0) {
151 if (!ImGui::BeginTable(
153 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
158 for (
const auto& [label, value] : stats) {
159 ImGui::TableNextColumn();
161 ImGui::TextColored(theme.text_secondary_gray,
"%s", label);
162 ImGui::TextWrapped(
"%s", value.c_str());
169 std::initializer_list<InspectorAction> actions) {
170 if (actions.size() == 0) {
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;
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);
188 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
189 if (next_x <= line_right) {
190 ImGui::SameLine(0.0f, spacing);
195 if (!action.enabled) {
196 ImGui::BeginDisabled();
198 if (ImGui::Button(action.label, ImVec2(button_width, 0.0f)) &&
199 action.enabled && action.action) {
202 if (!action.enabled) {
203 ImGui::EndDisabled();
227 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
228 : object_editor_(std::move(object_editor)) {}
279 for (
size_t idx : indices) {
302 ImGui::AlignTextToFramePadding();
303 ImGui::TextColored(theme.text_info,
ICON_MD_TUNE " Selection Inspector");
311 ImGui::TextDisabled(tr(
"Object editor unavailable"));
353 " Inspecting selected room object");
356 ImGui::TextColored(theme.status_success,
358 " Inspecting %zu selected room objects",
362 ImGui::TextColored(theme.status_success,
366 ImGui::TextColored(theme.status_success,
370 ImGui::TextColored(theme.status_success,
381 ImGui::TextColored(theme.text_secondary_gray,
399 DrawWrappedInspectorActions(
407 DrawWrappedInspectorActions(
416 DrawWrappedInspectorActions(
423 DrawWrappedInspectorActions(
436 if (!viewer || !viewer->
HasRooms()) {
442 if (selected.empty()) {
446 if (selected.size() == 1) {
448 if (selected[0] < objects.size()) {
449 const auto& obj = objects[selected[0]];
451 ImGui::TextColored(theme.status_success, tr(
"Object #%zu · 0x%03X %s"),
452 selected[0], obj.id_,
454 DrawInspectorSummaryGrid(
455 "##SelectedObjectInfo",
456 {{
"Position", absl::StrFormat(
"(%d, %d)", obj.x_, obj.y_)},
459 GetStoredPlacementLabel(obj)},
460 {
"Size", absl::StrFormat(
"0x%02X", obj.size_)},
464 :
"Special-table layer selector"}});
470 ImGui::TextColored(theme.status_success, tr(
"%zu objects selected"),
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"}});
483 if (!viewer || !viewer->
HasRooms()) {
495 const auto& doors = room.GetDoors();
496 if (entity.index >= doors.size()) {
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();
504 ImGui::TextColored(theme.status_success, tr(
"Door #%zu · %s"), entity.index,
506 DrawInspectorSummaryGrid(
507 "##SelectedDoorInfo",
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)}});
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),
522 if (ImGui::Selectable(entry_label.c_str(), is_current) && !is_current) {
529 ImGui::SetItemDefaultFocus();
535 const int neighbor_id =
537 std::optional<size_t> reciprocal_index;
538 if (neighbor_id >= 0 &&
539 neighbor_id <
static_cast<int>(viewer->
rooms()->
size())) {
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;
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;
560 const bool can_jump = reciprocal_index.has_value() &&
563 ImGui::BeginDisabled();
571 ImGui::EndDisabled();
575 ImGui::TextColored(theme.text_secondary_gray,
576 tr(
"Browse and place additional doors from Door Editor."));
581 if (!viewer || !viewer->
HasRooms()) {
593 if (entity.index >= sprites.size()) {
597 const auto& sprite = sprites[entity.index];
598 ImGui::TextColored(theme.status_success, tr(
"Sprite #%zu · 0x%02X %s"),
599 entity.index, sprite.id(),
601 if (sprite.IsOverlord()) {
603 ImGui::TextColored(theme.status_warning,
ICON_MD_STAR " OVERLORD");
606 const char* key_drop_label = sprite.key_drop() == 1 ?
"Small Key"
607 : sprite.key_drop() == 2 ?
"Big Key"
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}});
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,
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) {
635 int key_drop = sprite.key_drop();
636 ImGui::Text(tr(
"Key Drop:"));
638 if (ImGui::RadioButton(tr(
"None##SelectionKeyNone"), key_drop == 0)) {
639 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
644 if (ImGui::RadioButton(
ICON_MD_KEY " Small##SelectionKeySmall",
646 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
653 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
660 theme.text_secondary_gray,
661 tr(
"Drag in the canvas to reposition. Browse and place more "
662 "sprites from Sprite Editor."));
667 if (!viewer || !viewer->
HasRooms()) {
679 if (entity.index >= items.size()) {
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",
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)},
702 {
"Value", absl::StrFormat(
"0x%02X", item.item)}});
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) {
715 mutable_item.
item =
static_cast<uint8_t
>(i);
719 ImGui::SetItemDefaultFocus();
727 theme.text_secondary_gray,
728 tr(
"Drag in the canvas to reposition. Browse and place more "
729 "items from Item Editor."));
737 " Click any room object, door, sprite, or item in the "
738 "canvas to inspect it here.");
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.");
750 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
751 if (ImGui::Begin(
"Keyboard Shortcuts##DungeonSelectionInspector",
754 auto shortcut_row = [&](
const char* keys,
const char* desc) {
755 ImGui::TextColored(theme.status_warning,
"%-18s", keys);
757 ImGui::TextUnformatted(desc);
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");
768 ImGui::TextColored(theme.status_success,
ICON_MD_EDIT " Editing");
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");
780 shortcut_row(
"Arrow Keys",
"Nudge selected (1px)");
786 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
790 const ImGuiIO& io = ImGui::GetIO();
792 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
795 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
798 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
803 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
810 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
815 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
818 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
821 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
822 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
829 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
832 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
835 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
838 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
846 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
850 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
854 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
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);
901 if (selected.empty()) {
905 std::vector<size_t> new_indices;
906 for (
size_t idx : selected) {
908 if (new_idx.has_value()) {
909 new_indices.push_back(*new_idx);
912 interaction.SetSelectedObjects(new_indices);
933 if (!viewer || !viewer->
HasRooms()) {
943 coordinator.sprite_handler().DeleteAll();
946 coordinator.item_handler().DeleteAll();
955 if (!viewer || !viewer->
HasRooms()) {
968 auto& sprites = room.GetSprites();
969 if (*selected_index >= sprites.size()) {
973 if (
auto* ctx = handler.context()) {
976 sprites.push_back(sprites[*selected_index]);
977 room.MarkSpritesDirty();
978 if (
auto* ctx = handler.context()) {
981 handler.SelectSprite(sprites.size() - 1);
1019 const size_t total_objects = objects.size();
1020 if (total_objects == 0) {
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});
1038 if (index >= objects.size()) {
1042 const auto& obj = objects[index];
bool MutateDoorType(size_t index, zelda3::DoorType new_type)
Change the type of a door in place, re-encoding ROM bytes.
DungeonRoomStore * rooms() const
int current_room_id() const
DungeonObjectInteraction & object_interaction()
void ScrollToTile(int tile_x, int tile_y)
Handles object selection, placement, and interaction within the dungeon canvas.
void HandleDeleteSelected()
void SetSelectedObjects(const std::vector< size_t > &indices)
SelectedEntity GetSelectedEntity() const
std::vector< size_t > GetSelectedObjectIndices() const
void SetSelectionChangeCallback(std::function< void()> callback)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
bool NudgeSelected(int delta_x, int delta_y)
void ClearEntitySelection()
void HandlePasteObjects()
void HandleCopySelected()
ItemInteractionHandler & item_handler()
SpriteInteractionHandler & sprite_handler()
void DeleteSelectedEntity()
Delete currently selected entity.
DoorInteractionHandler & door_handler()
std::optional< size_t > GetSelectedIndex() const
Get selected item index.
void Draw(bool *p_open) override
Draw the panel content.
DungeonCanvasViewer * ResolveCanvasViewer()
void HandleKeyboardShortcuts()
std::function< DungeonCanvasViewer *()> canvas_viewer_provider_
void RefreshSelectionSnapshot()
void DuplicateSelectedObjects()
void DeleteCurrentSelection()
void DeselectAllObjects()
void DrawSelectionActions()
void DeleteAllSelectedTypeInRoom()
void CycleObjectSelection(int direction)
void DrawKeyboardShortcutHelp()
void DrawSelectedSpriteInfo()
void DrawSelectedDoorInfo()
ObjectEditorContent(std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr)
void DrawSelectionSummary()
DungeonSelectionSnapshot selection_snapshot_
void DrawSelectedItemInfo()
void DuplicateSelectedSprite()
void ScrollToObject(size_t index)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void DeleteSelectedEntity()
void NudgeCurrentSelection(int dx, int dy)
size_t cached_selection_count_
DungeonCanvasViewer * canvas_viewer_
bool selection_callbacks_setup_
void SetupSelectionCallbacks()
void OnSelectionChanged()
std::function< void(int, size_t)> on_jump_to_reciprocal_door_
void DeleteSelectedObjects()
void CopySelectedObjects()
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
void DrawSelectedObjectInfo()
std::optional< size_t > GetSelectedIndex() const
Get selected sprite index.
A class for managing sprites in the overworld and underworld.
void set_subtype(int subtype)
void set_layer(int layer)
auto set_key_drop(int key)
#define ICON_MD_ARROW_FORWARD
#define ICON_MD_OPEN_WITH
#define ICON_MD_FILTER_NONE
#define ICON_MD_CONTENT_PASTE
#define ICON_MD_INVENTORY
#define ICON_MD_DOOR_FRONT
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_HELP_OUTLINE
#define ICON_MD_SELECT_ALL
#define ICON_MD_CONTENT_COPY
#define ICON_MD_DELETE_SWEEP
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
std::pair< const char *, std::string > InspectorStat
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.
@ DoubleSidedShutter
Double sided shutter door.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
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.
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
const char * ResolveSpriteName(uint16_t id)
bool HasSelection() const
DungeonSelectionKind kind
bool HasObjectSelection() const
std::function< void()> action