7#include <initializer_list>
11#include "absl/strings/str_format.h"
14#include "imgui/imgui.h"
24 const std::vector<size_t>& editor_selected_indices) {
31 const std::vector<size_t> target_indices = editor_selected_indices;
41 const char* label =
"";
48 switch (
object.GetLayerValue()) {
59 switch (
object.GetLayerValue()) {
61 return "Upper layer (BG1)";
63 return "Lower layer (BG2)";
73 if (viewer ==
nullptr || viewer->
rooms() ==
nullptr) {
81 if (!selected_index.has_value()) {
86 auto& sprites = room->GetSprites();
87 if (*selected_index >= sprites.size()) {
91 if (
auto* ctx = handler.context()) {
94 mutator(sprites[*selected_index]);
95 room->MarkSpritesDirty();
96 if (
auto* ctx = handler.context()) {
98 ctx->NotifyEntityChanged();
105 if (viewer ==
nullptr || viewer->
rooms() ==
nullptr) {
113 if (!selected_index.has_value()) {
118 auto& items = room->GetPotItems();
119 if (*selected_index >= items.size()) {
123 if (
auto* ctx = handler.context()) {
126 mutator(items[*selected_index]);
127 room->MarkPotItemsDirty();
128 if (
auto* ctx = handler.context()) {
130 ctx->NotifyEntityChanged();
136 std::initializer_list<InspectorStat> stats) {
137 if (stats.size() == 0) {
141 if (!ImGui::BeginTable(
143 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
148 for (
const auto& [label, value] : stats) {
149 ImGui::TableNextColumn();
151 ImGui::TextColored(theme.text_secondary_gray,
"%s", label);
152 ImGui::TextWrapped(
"%s", value.c_str());
159 std::initializer_list<InspectorAction> actions) {
160 if (actions.size() == 0) {
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;
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);
178 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
179 if (next_x <= line_right) {
180 ImGui::SameLine(0.0f, spacing);
185 if (!action.enabled) {
186 ImGui::BeginDisabled();
188 if (ImGui::Button(action.label, ImVec2(button_width, 0.0f)) &&
189 action.enabled && action.action) {
192 if (!action.enabled) {
193 ImGui::EndDisabled();
217 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor)
218 : object_editor_(std::move(object_editor)) {}
269 for (
size_t idx : indices) {
292 ImGui::AlignTextToFramePadding();
293 ImGui::TextColored(theme.text_info,
ICON_MD_TUNE " Selection Inspector");
301 ImGui::TextDisabled(tr(
"Object editor unavailable"));
343 " Inspecting selected room object");
346 ImGui::TextColored(theme.status_success,
348 " Inspecting %zu selected room objects",
352 ImGui::TextColored(theme.status_success,
356 ImGui::TextColored(theme.status_success,
360 ImGui::TextColored(theme.status_success,
371 ImGui::TextColored(theme.text_secondary_gray,
389 DrawWrappedInspectorActions(
397 DrawWrappedInspectorActions(
406 DrawWrappedInspectorActions(
413 DrawWrappedInspectorActions(
426 if (!viewer || !viewer->
HasRooms()) {
432 if (selected.empty()) {
436 if (selected.size() == 1) {
438 if (selected[0] < objects.size()) {
439 const auto& obj = objects[selected[0]];
441 ImGui::TextColored(theme.status_success, tr(
"Object #%zu · 0x%03X %s"),
442 selected[0], obj.id_,
444 DrawInspectorSummaryGrid(
445 "##SelectedObjectInfo",
446 {{
"Position", absl::StrFormat(
"(%d, %d)", obj.x_, obj.y_)},
449 GetStoredPlacementLabel(obj)},
450 {
"Size", absl::StrFormat(
"0x%02X", obj.size_)},
454 :
"Special-table layer selector"}});
460 ImGui::TextColored(theme.status_success, tr(
"%zu objects selected"),
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"}});
473 if (!viewer || !viewer->
HasRooms()) {
485 const auto& doors = room.GetDoors();
486 if (entity.index >= doors.size()) {
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();
494 ImGui::TextColored(theme.status_success, tr(
"Door #%zu · %s"), entity.index,
496 DrawInspectorSummaryGrid(
497 "##SelectedDoorInfo",
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)}});
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),
512 if (ImGui::Selectable(entry_label.c_str(), is_current) && !is_current) {
519 ImGui::SetItemDefaultFocus();
525 const int neighbor_id =
527 std::optional<size_t> reciprocal_index;
529 neighbor_id <
static_cast<int>(viewer->
rooms()->
size())) {
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];
535 neighbor_door.direction == opposite &&
536 neighbor_door.position == door.position) {
537 reciprocal_index = i;
541 if (!reciprocal_index) {
542 for (
size_t i = 0; i < neighbor_doors.size(); ++i) {
544 neighbor_doors[i].direction == opposite) {
545 reciprocal_index = i;
552 const bool can_jump = reciprocal_index.has_value() &&
555 ImGui::BeginDisabled();
563 ImGui::EndDisabled();
567 ImGui::TextColored(theme.text_secondary_gray,
568 tr(
"Browse and place additional doors from Door Editor."));
573 if (!viewer || !viewer->
HasRooms()) {
585 if (entity.index >= sprites.size()) {
589 const auto& sprite = sprites[entity.index];
590 ImGui::TextColored(theme.status_success, tr(
"Sprite #%zu · 0x%02X %s"),
591 entity.index, sprite.id(),
593 if (sprite.IsOverlord()) {
595 ImGui::TextColored(theme.status_warning,
ICON_MD_STAR " OVERLORD");
598 const char* key_drop_label = sprite.key_drop() == 1 ?
"Small Key"
599 : sprite.key_drop() == 2 ?
"Big Key"
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}});
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,
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) {
627 int key_drop = sprite.key_drop();
628 ImGui::Text(tr(
"Key Drop:"));
630 if (ImGui::RadioButton(tr(
"None##SelectionKeyNone"), key_drop == 0)) {
631 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
636 if (ImGui::RadioButton(
ICON_MD_KEY " Small##SelectionKeySmall",
638 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
645 (void)MutateSelectedSprite(viewer, [](
zelda3::Sprite& mutable_sprite) {
652 theme.text_secondary_gray,
653 tr(
"Drag in the canvas to reposition. Browse and place more "
654 "sprites from Sprite Editor."));
659 if (!viewer || !viewer->
HasRooms()) {
671 if (entity.index >= items.size()) {
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",
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)},
694 {
"Value", absl::StrFormat(
"0x%02X", item.item)}});
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) {
707 mutable_item.
item =
static_cast<uint8_t
>(i);
711 ImGui::SetItemDefaultFocus();
719 theme.text_secondary_gray,
720 tr(
"Drag in the canvas to reposition. Browse and place more "
721 "items from Item Editor."));
733 ImGui::SetNextWindowSize(ImVec2(340, 0), ImGuiCond_Appearing);
734 if (ImGui::Begin(
"Keyboard Shortcuts##DungeonSelectionInspector",
737 auto shortcut_row = [&](
const char* keys,
const char* desc) {
738 ImGui::TextColored(theme.status_warning,
"%-18s", keys);
740 ImGui::TextUnformatted(desc);
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");
751 ImGui::TextColored(theme.status_success,
ICON_MD_EDIT " Editing");
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");
763 shortcut_row(
"Arrow Keys",
"Nudge selected (1px)");
769 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
773 const ImGuiIO& io = ImGui::GetIO();
774 if (io.WantTextInput) {
778 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && !io.KeyShift) {
781 if (ImGui::IsKeyPressed(ImGuiKey_A) && io.KeyCtrl && io.KeyShift) {
784 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
791 if (ImGui::IsKeyPressed(ImGuiKey_D) && io.KeyCtrl) {
798 if (ImGui::IsKeyPressed(ImGuiKey_C) && io.KeyCtrl) {
803 if (ImGui::IsKeyPressed(ImGuiKey_V) && io.KeyCtrl) {
806 if (ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && !io.KeyShift) {
809 if ((ImGui::IsKeyPressed(ImGuiKey_Z) && io.KeyCtrl && io.KeyShift) ||
810 (ImGui::IsKeyPressed(ImGuiKey_Y) && io.KeyCtrl)) {
817 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
820 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
823 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
826 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
834 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && !io.KeyCtrl) {
838 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
842 if (ImGui::IsKeyPressed(ImGuiKey_Slash) && io.KeyShift) {
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);
889 if (selected.empty()) {
893 std::vector<size_t> new_indices;
894 for (
size_t idx : selected) {
896 if (new_idx.has_value()) {
897 new_indices.push_back(*new_idx);
900 interaction.SetSelectedObjects(new_indices);
921 if (!viewer || !viewer->
HasRooms()) {
931 coordinator.sprite_handler().DeleteAll();
934 coordinator.item_handler().DeleteAll();
943 if (!viewer || !viewer->
HasRooms()) {
956 auto& sprites = room.GetSprites();
957 if (*selected_index >= sprites.size()) {
961 if (
auto* ctx = handler.context()) {
964 sprites.push_back(sprites[*selected_index]);
965 room.MarkSpritesDirty();
966 if (
auto* ctx = handler.context()) {
969 handler.SelectSprite(sprites.size() - 1);
1007 const size_t total_objects = objects.size();
1008 if (total_objects == 0) {
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});
1026 if (index >= objects.size()) {
1030 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)
bool CanHandleRoomCanvasShortcut() const
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)
std::pair< const char *, std::string > InspectorStat
constexpr auto 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)
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.
constexpr bool IsRoomConnectionDoorType(DoorType type)
Return true when a door can represent an adjacent-room connection.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
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.
const char * ResolveSpriteName(uint16_t id)
bool HasSelection() const
DungeonSelectionKind kind
bool HasObjectSelection() const
std::function< void()> action