feat: Implement Jump-to Functionality for Cross-Editor Navigation

- Added JumpToDungeonRoom and JumpToOverworldMap methods to facilitate quick navigation between dungeon and overworld editors.
- Introduced SwitchToEditor method to manage editor tab activation based on the selected editor type.
- Enhanced DungeonEditorV2 with room and entrance selection capabilities, improving user experience and workflow efficiency.
- Updated header files to declare new methods and ensure proper integration within the editor management system.
This commit is contained in:
scawful
2025-10-07 12:31:26 -04:00
parent 33f64f38a5
commit 5e4e6f5d45
10 changed files with 365 additions and 32 deletions

View File

@@ -1,5 +1,6 @@
#include "dungeon_editor_v2.h" #include "dungeon_editor_v2.h"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include "absl/strings/str_format.h" #include "absl/strings/str_format.h"
@@ -55,7 +56,7 @@ absl::Status DungeonEditorV2::Load() {
palette_editor_.Initialize(rom_); palette_editor_.Initialize(rom_);
// Wire palette changes to trigger room re-renders // Wire palette changes to trigger room re-renders
palette_editor_.SetOnPaletteChanged([this](int palette_id) { palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
// Re-render all active rooms when palette changes // Re-render all active rooms when palette changes
for (int i = 0; i < active_rooms_.Size; i++) { for (int i = 0; i < active_rooms_.Size; i++) {
int room_id = active_rooms_[i]; int room_id = active_rooms_[i];
@@ -118,6 +119,30 @@ void DungeonEditorV2::DrawToolset() {
if (toolbar.AddAction(ICON_MD_ADD, "Open Room")) { if (toolbar.AddAction(ICON_MD_ADD, "Open Room")) {
OnRoomSelected(room_selector_.current_room_id()); OnRoomSelected(room_selector_.current_room_id());
} }
toolbar.AddSeparator();
if (toolbar.AddToggle(ICON_MD_LIST, &show_room_selector_, "Toggle Room Selector")) {
// Toggled
}
if (toolbar.AddToggle(ICON_MD_GRID_VIEW, &show_room_matrix_, "Toggle Room Matrix")) {
// Toggled
}
if (toolbar.AddToggle(ICON_MD_DOOR_FRONT, &show_entrances_list_, "Toggle Entrances List")) {
// Toggled
}
toolbar.AddSeparator();
if (toolbar.AddToggle(ICON_MD_CATEGORY, &show_object_selector_, "Toggle Object Selector")) {
// Toggled
}
if (toolbar.AddToggle(ICON_MD_PALETTE, &show_palette_editor_, "Toggle Palette Editor")) {
// Toggled
}
toolbar.End(); toolbar.End();
} }
@@ -126,42 +151,43 @@ void DungeonEditorV2::DrawLayout() {
// NO TABLE LAYOUT - All independent dockable EditorCards // NO TABLE LAYOUT - All independent dockable EditorCards
// 1. Room Selector Card (independent, dockable) // 1. Room Selector Card (independent, dockable)
{ if (show_room_selector_) {
static bool show_room_selector = true; DrawRoomsListCard();
gui::EditorCard selector_card( }
MakeCardTitle("Room Selector").c_str(),
ICON_MD_LIST, &show_room_selector); // 2. Room Matrix Card (visual navigation)
if (selector_card.Begin()) { if (show_room_matrix_) {
room_selector_.Draw(); DrawRoomMatrixCard();
} }
selector_card.End();
// 3. Entrances List Card
if (show_entrances_list_) {
DrawEntrancesListCard();
} }
// 2. Object Selector/Manager Card (independent, dockable) // 4. Object Selector/Manager Card (independent, dockable)
{ if (show_object_selector_) {
static bool show_object_selector = true;
gui::EditorCard object_card( gui::EditorCard object_card(
MakeCardTitle("Object Selector").c_str(), MakeCardTitle("Object Selector").c_str(),
ICON_MD_CATEGORY, &show_object_selector); ICON_MD_CATEGORY, &show_object_selector_);
if (object_card.Begin()) { if (object_card.Begin()) {
object_selector_.Draw(); object_selector_.Draw();
} }
object_card.End(); object_card.End();
} }
// 3. Palette Editor Card (independent, dockable) // 5. Palette Editor Card (independent, dockable)
{ if (show_palette_editor_) {
static bool show_palette_editor = true;
gui::EditorCard palette_card( gui::EditorCard palette_card(
MakeCardTitle("Palette Editor").c_str(), MakeCardTitle("Palette Editor").c_str(),
ICON_MD_PALETTE, &show_palette_editor); ICON_MD_PALETTE, &show_palette_editor_);
if (palette_card.Begin()) { if (palette_card.Begin()) {
palette_editor_.Draw(); palette_editor_.Draw();
} }
palette_card.End(); palette_card.End();
} }
// 4. Active Room Cards (independent, dockable, no inheritance) // 6. Active Room Cards (independent, dockable, tracked for jump-to)
for (int i = 0; i < active_rooms_.Size; i++) { for (int i = 0; i < active_rooms_.Size; i++) {
int room_id = active_rooms_[i]; int room_id = active_rooms_[i];
bool open = true; bool open = true;
@@ -177,14 +203,20 @@ void DungeonEditorV2::DrawLayout() {
std::string card_name_str = absl::StrFormat("%s###RoomCard%d", std::string card_name_str = absl::StrFormat("%s###RoomCard%d",
MakeCardTitle(base_name).c_str(), room_id); MakeCardTitle(base_name).c_str(), room_id);
// Each room card is COMPLETELY independent - no parent windows // Track or create card for jump-to functionality
gui::EditorCard room_card(card_name_str.c_str(), ICON_MD_GRID_ON, &open); if (room_cards_.find(room_id) == room_cards_.end()) {
if (room_card.Begin()) { room_cards_[room_id] = std::make_shared<gui::EditorCard>(
card_name_str.c_str(), ICON_MD_GRID_ON, &open);
}
auto& room_card = room_cards_[room_id];
if (room_card->Begin(&open)) {
DrawRoomTab(room_id); DrawRoomTab(room_id);
} }
room_card.End(); room_card->End();
if (!open) { if (!open) {
room_cards_.erase(room_id);
active_rooms_.erase(active_rooms_.Data + i); active_rooms_.erase(active_rooms_.Data + i);
i--; i--;
} }
@@ -235,7 +267,8 @@ void DungeonEditorV2::OnRoomSelected(int room_id) {
// Check if already open // Check if already open
for (int i = 0; i < active_rooms_.Size; i++) { for (int i = 0; i < active_rooms_.Size; i++) {
if (active_rooms_[i] == room_id) { if (active_rooms_[i] == room_id) {
// Optional: Focus the existing window if possible. For now, do nothing. // Focus the existing room card
FocusRoom(room_id);
return; return;
} }
} }
@@ -245,5 +278,212 @@ void DungeonEditorV2::OnRoomSelected(int room_id) {
room_selector_.set_active_rooms(active_rooms_); room_selector_.set_active_rooms(active_rooms_);
} }
void DungeonEditorV2::OnEntranceSelected(int entrance_id) {
if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
return;
}
// Get the room ID associated with this entrance
int room_id = entrances_[entrance_id].room_;
// Open and focus the room
OnRoomSelected(room_id);
}
void DungeonEditorV2::add_room(int room_id) {
OnRoomSelected(room_id);
}
void DungeonEditorV2::FocusRoom(int room_id) {
// Focus the room card if it exists
auto it = room_cards_.find(room_id);
if (it != room_cards_.end()) {
it->second->Focus();
}
}
void DungeonEditorV2::DrawRoomsListCard() {
gui::EditorCard selector_card(
MakeCardTitle("Rooms List").c_str(),
ICON_MD_LIST, &show_room_selector_);
if (selector_card.Begin()) {
// Add text filter
static char room_filter[256] = "";
ImGui::SetNextItemWidth(-1);
if (ImGui::InputTextWithHint("##RoomFilter", ICON_MD_SEARCH " Filter rooms...",
room_filter, sizeof(room_filter))) {
// Filter updated
}
ImGui::Separator();
// Scrollable room list with resource labels
if (ImGui::BeginChild("##RoomsList", ImVec2(0, 0), true)) {
std::string filter_str = room_filter;
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
for (int i = 0; i < 0x128; i++) {
std::string room_name;
if (i < static_cast<int>(std::size(zelda3::kRoomNames))) {
room_name = absl::StrFormat("%03X - %s", i, zelda3::kRoomNames[i].data());
} else {
room_name = absl::StrFormat("%03X - Room %d", i, i);
}
// Apply filter
if (!filter_str.empty()) {
std::string room_name_lower = room_name;
std::transform(room_name_lower.begin(), room_name_lower.end(),
room_name_lower.begin(), ::tolower);
if (room_name_lower.find(filter_str) == std::string::npos) {
continue;
}
}
bool is_selected = (current_room_id_ == i);
if (ImGui::Selectable(room_name.c_str(), is_selected)) {
OnRoomSelected(i);
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
OnRoomSelected(i);
}
}
ImGui::EndChild();
}
}
selector_card.End();
}
void DungeonEditorV2::DrawEntrancesListCard() {
gui::EditorCard entrances_card(
MakeCardTitle("Entrances List").c_str(),
ICON_MD_DOOR_FRONT, &show_entrances_list_);
if (entrances_card.Begin()) {
// Add text filter
static char entrance_filter[256] = "";
ImGui::SetNextItemWidth(-1);
if (ImGui::InputTextWithHint("##EntranceFilter", ICON_MD_SEARCH " Filter entrances...",
entrance_filter, sizeof(entrance_filter))) {
// Filter updated
}
ImGui::Separator();
// Scrollable entrance list with associated room names
if (ImGui::BeginChild("##EntrancesList", ImVec2(0, 0), true)) {
std::string filter_str = entrance_filter;
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
for (int i = 0; i < static_cast<int>(entrances_.size()); i++) {
int room_id = entrances_[i].room_;
std::string room_name = "Unknown";
if (room_id >= 0 && room_id < static_cast<int>(std::size(zelda3::kRoomNames))) {
room_name = zelda3::kRoomNames[room_id].data();
}
std::string entrance_label = absl::StrFormat("%02X - %s (Room %03X)",
i, room_name.c_str(), room_id);
// Apply filter
if (!filter_str.empty()) {
std::string entrance_label_lower = entrance_label;
std::transform(entrance_label_lower.begin(), entrance_label_lower.end(),
entrance_label_lower.begin(), ::tolower);
if (entrance_label_lower.find(filter_str) == std::string::npos) {
continue;
}
}
if (ImGui::Selectable(entrance_label.c_str())) {
OnEntranceSelected(i);
}
}
ImGui::EndChild();
}
}
entrances_card.End();
}
void DungeonEditorV2::DrawRoomMatrixCard() {
gui::EditorCard matrix_card(
MakeCardTitle("Room Matrix").c_str(),
ICON_MD_GRID_VIEW, &show_room_matrix_);
matrix_card.SetDefaultSize(600, 600);
if (matrix_card.Begin()) {
// Draw 8x8 grid of rooms (first 64 rooms)
constexpr int kRoomsPerRow = 8;
constexpr int kRoomsPerCol = 8;
constexpr float kRoomCellSize = 64.0f;
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
for (int row = 0; row < kRoomsPerCol; row++) {
for (int col = 0; col < kRoomsPerRow; col++) {
int room_id = row * kRoomsPerRow + col;
ImVec2 cell_min = ImVec2(canvas_pos.x + col * kRoomCellSize,
canvas_pos.y + row * kRoomCellSize);
ImVec2 cell_max = ImVec2(cell_min.x + kRoomCellSize,
cell_min.y + kRoomCellSize);
// Check if room is active
bool is_active = false;
for (int i = 0; i < active_rooms_.Size; i++) {
if (active_rooms_[i] == room_id) {
is_active = true;
break;
}
}
// Draw cell background
ImU32 bg_color = is_active ? IM_COL32(100, 150, 255, 255)
: IM_COL32(50, 50, 50, 255);
draw_list->AddRectFilled(cell_min, cell_max, bg_color);
// Draw cell border
draw_list->AddRect(cell_min, cell_max, IM_COL32(150, 150, 150, 255));
// Draw room ID
std::string room_label = absl::StrFormat("%02X", room_id);
ImVec2 text_size = ImGui::CalcTextSize(room_label.c_str());
ImVec2 text_pos = ImVec2(cell_min.x + (kRoomCellSize - text_size.x) * 0.5f,
cell_min.y + (kRoomCellSize - text_size.y) * 0.5f);
draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), room_label.c_str());
// Handle clicks
ImGui::SetCursorScreenPos(cell_min);
ImGui::InvisibleButton(absl::StrFormat("##room%d", room_id).c_str(),
ImVec2(kRoomCellSize, kRoomCellSize));
if (ImGui::IsItemClicked()) {
OnRoomSelected(room_id);
}
// Hover preview (TODO: implement room bitmap preview)
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (room_id < static_cast<int>(std::size(zelda3::kRoomNames))) {
ImGui::Text("%s", zelda3::kRoomNames[room_id].data());
} else {
ImGui::Text("Room %03X", room_id);
}
ImGui::EndTooltip();
}
}
}
// Advance cursor past the grid
ImGui::Dummy(ImVec2(kRoomsPerRow * kRoomCellSize, kRoomsPerCol * kRoomCellSize));
}
matrix_card.End();
}
} // namespace yaze::editor } // namespace yaze::editor

View File

@@ -1,6 +1,9 @@
#ifndef YAZE_APP_EDITOR_DUNGEON_EDITOR_V2_H #ifndef YAZE_APP_EDITOR_DUNGEON_EDITOR_V2_H
#define YAZE_APP_EDITOR_DUNGEON_EDITOR_V2_H #define YAZE_APP_EDITOR_DUNGEON_EDITOR_V2_H
#include <memory>
#include <unordered_map>
#include "absl/status/status.h" #include "absl/status/status.h"
#include "absl/strings/str_format.h" #include "absl/strings/str_format.h"
#include "app/editor/editor.h" #include "app/editor/editor.h"
@@ -70,7 +73,8 @@ class DungeonEditorV2 : public Editor {
Rom* rom() const { return rom_; } Rom* rom() const { return rom_; }
// Room management // Room management
void add_room(int room_id) { active_rooms_.push_back(room_id); } void add_room(int room_id);
void FocusRoom(int room_id);
// ROM state // ROM state
bool IsRomLoaded() const override { return rom_ && rom_->is_loaded(); } bool IsRomLoaded() const override { return rom_ && rom_->is_loaded(); }
@@ -85,19 +89,31 @@ class DungeonEditorV2 : public Editor {
void DrawLayout(); void DrawLayout();
void DrawRoomTab(int room_id); void DrawRoomTab(int room_id);
void DrawToolset(); void DrawToolset();
void DrawRoomMatrixCard();
void DrawRoomsListCard();
void DrawEntrancesListCard();
// Room selection callback // Room selection callback
void OnRoomSelected(int room_id); void OnRoomSelected(int room_id);
void OnEntranceSelected(int entrance_id);
// Data // Data
Rom* rom_; Rom* rom_;
std::array<zelda3::Room, 0x128> rooms_; std::array<zelda3::Room, 0x128> rooms_;
std::array<zelda3::RoomEntrance, 0x8C> entrances_; std::array<zelda3::RoomEntrance, 0x8C> entrances_;
// Active room tabs // Active room tabs and card tracking for jump-to
ImVector<int> active_rooms_; ImVector<int> active_rooms_;
std::unordered_map<int, std::shared_ptr<gui::EditorCard>> room_cards_;
int current_room_id_ = 0; int current_room_id_ = 0;
// Card visibility flags
bool show_room_selector_ = true;
bool show_room_matrix_ = false;
bool show_entrances_list_ = false;
bool show_object_selector_ = true;
bool show_palette_editor_ = true;
// Palette management // Palette management
gfx::SnesPalette current_palette_; gfx::SnesPalette current_palette_;
gfx::PaletteGroup current_palette_group_; gfx::PaletteGroup current_palette_group_;

View File

@@ -3267,5 +3267,42 @@ void EditorManager::DrawWelcomeScreen() {
} }
} }
// ============================================================================
// Jump-to Functionality for Cross-Editor Navigation
// ============================================================================
void EditorManager::JumpToDungeonRoom(int room_id) {
if (!current_editor_set_) return;
// Switch to dungeon editor
SwitchToEditor(EditorType::kDungeon);
// Open the room in the dungeon editor
current_editor_set_->dungeon_editor_.add_room(room_id);
}
void EditorManager::JumpToOverworldMap(int map_id) {
if (!current_editor_set_) return;
// Switch to overworld editor
SwitchToEditor(EditorType::kOverworld);
// Set the current map in the overworld editor
current_editor_set_->overworld_editor_.set_current_map(map_id);
}
void EditorManager::SwitchToEditor(EditorType editor_type) {
// Find the editor tab and activate it
for (size_t i = 0; i < current_editor_set_->active_editors_.size(); ++i) {
if (current_editor_set_->active_editors_[i]->type() == editor_type) {
current_editor_set_->active_editors_[i]->set_active(true);
// Set editor as the current/focused one
// This will make it visible when tabs are rendered
break;
}
}
}
} // namespace editor } // namespace editor
} // namespace yaze } // namespace yaze

View File

@@ -134,6 +134,11 @@ class EditorManager {
} }
void BuildModernMenu(); void BuildModernMenu();
// Jump-to functionality for cross-editor navigation
void JumpToDungeonRoom(int room_id);
void JumpToOverworldMap(int map_id);
void SwitchToEditor(EditorType editor_type);
private: private:
void DrawWelcomeScreen(); void DrawWelcomeScreen();

View File

@@ -101,6 +101,14 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
if (!rom_->is_loaded()) return "ROM failed to load"; if (!rom_->is_loaded()) return "ROM failed to load";
return absl::StrFormat("ROM loaded: %s", rom_->title()); return absl::StrFormat("ROM loaded: %s", rom_->title());
} }
// Jump-to functionality
void set_current_map(int map_id) {
if (map_id >= 0 && map_id < zelda3::kNumOverworldMaps) {
current_map_ = map_id;
current_world_ = map_id / 0x40; // Calculate which world the map belongs to
}
}
/** /**
* @brief Load the Bitmap objects for each OverworldMap. * @brief Load the Bitmap objects for each OverworldMap.

View File

@@ -15,10 +15,10 @@ using namespace ImGui;
// Entity colors - solid with good visibility // Entity colors - solid with good visibility
namespace { namespace {
ImVec4 GetEntranceColor() { return ImVec4(0.0f, 255.0f, 0.0f, 255.0f); } // Solid green ImVec4 GetEntranceColor() { return ImVec4{1.0f, 1.0f, 0.0f, 1.0f}; } // Solid yellow (#FFFF00FF, fully opaque)
ImVec4 GetExitColor() { return ImVec4(255.0f, 0.0f, 0.0f, 255.0f); } // Solid red ImVec4 GetExitColor() { return ImVec4{1.0f, 1.0f, 1.0f, 1.0f}; } // Solid white (#FFFFFFFF, fully opaque)
ImVec4 GetItemColor() { return ImVec4(255.0f, 255.0f, 0.0f, 255.0f); } // Solid yellow ImVec4 GetItemColor() { return ImVec4{1.0f, 0.0f, 0.0f, 1.0f}; } // Solid red (#FF0000FF, fully opaque)
ImVec4 GetSpriteColor() { return ImVec4(255.0f, 128.0f, 0.0f, 255.0f); } // Solid orange ImVec4 GetSpriteColor() { return ImVec4{1.0f, 0.0f, 1.0f, 1.0f}; } // Solid magenta (#FF00FFFF, fully opaque)
} // namespace } // namespace
void OverworldEntityRenderer::DrawEntrances(ImVec2 canvas_p0, ImVec2 scrolling, void OverworldEntityRenderer::DrawEntrances(ImVec2 canvas_p0, ImVec2 scrolling,

View File

@@ -141,7 +141,7 @@ void DrawCanvasRect(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling,
ImVec2 size(canvas_p0.x + scrolling.x + scaled_x + scaled_w, ImVec2 size(canvas_p0.x + scrolling.x + scaled_x + scaled_w,
canvas_p0.y + scrolling.y + scaled_y + scaled_h); canvas_p0.y + scrolling.y + scaled_y + scaled_h);
uint32_t color_u32 = IM_COL32(color.x, color.y, color.z, color.w); uint32_t color_u32 = IM_COL32(color.x * 255, color.y * 255, color.z * 255, color.w * 255);
draw_list->AddRectFilled(origin, size, color_u32); draw_list->AddRectFilled(origin, size, color_u32);
// Add a black outline // Add a black outline

View File

@@ -229,11 +229,14 @@ bool Toolset::AddUsageStatsButton(const char* tooltip) {
// ============================================================================ // ============================================================================
EditorCard::EditorCard(const char* title, const char* icon) EditorCard::EditorCard(const char* title, const char* icon)
: title_(title), icon_(icon ? icon : ""), default_size_(400, 300) {} : title_(title), icon_(icon ? icon : ""), default_size_(400, 300) {
window_name_ = icon_.empty() ? title_ : icon_ + " " + title_;
}
EditorCard::EditorCard(const char* title, const char* icon, bool* p_open) EditorCard::EditorCard(const char* title, const char* icon, bool* p_open)
: title_(title), icon_(icon ? icon : ""), default_size_(400, 300) { : title_(title), icon_(icon ? icon : ""), default_size_(400, 300) {
p_open_ = p_open; p_open_ = p_open;
window_name_ = icon_.empty() ? title_ : icon_ + " " + title_;
} }
void EditorCard::SetDefaultSize(float width, float height) { void EditorCard::SetDefaultSize(float width, float height) {
@@ -301,11 +304,20 @@ bool EditorCard::Begin(bool* p_open) {
} }
void EditorCard::End() { void EditorCard::End() {
// Check if window was focused this frame
focused_ = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows);
ImGui::End(); ImGui::End();
ImGui::PopStyleColor(2); ImGui::PopStyleColor(2);
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
} }
void EditorCard::Focus() {
// Set window focus using ImGui's focus system
ImGui::SetWindowFocus(window_name_.c_str());
focused_ = true;
}
// ============================================================================ // ============================================================================
// EditorLayout Implementation // EditorLayout Implementation
// ============================================================================ // ============================================================================

View File

@@ -132,15 +132,24 @@ class EditorCard {
void SetMinimized(bool minimized) { minimized_ = minimized; } void SetMinimized(bool minimized) { minimized_ = minimized; }
bool IsMinimized() const { return minimized_; } bool IsMinimized() const { return minimized_; }
// Focus the card window (bring to front and set focused)
void Focus();
bool IsFocused() const { return focused_; }
// Get the window name for ImGui operations
const char* GetWindowName() const { return window_name_.c_str(); }
private: private:
std::string title_; std::string title_;
std::string icon_; std::string icon_;
std::string window_name_; // Full window name with icon
ImVec2 default_size_; ImVec2 default_size_;
Position position_ = Position::Free; Position position_ = Position::Free;
bool minimizable_ = true; bool minimizable_ = true;
bool closable_ = true; bool closable_ = true;
bool minimized_ = false; bool minimized_ = false;
bool first_draw_ = true; bool first_draw_ = true;
bool focused_ = false;
bool* p_open_ = nullptr; bool* p_open_ = nullptr;
}; };

View File

@@ -133,6 +133,12 @@ struct EnhancedTheme {
Color editor_grid; // Grid lines in editors Color editor_grid; // Grid lines in editors
Color editor_cursor; // Cursor/selection in editors Color editor_cursor; // Cursor/selection in editors
Color editor_selection; // Selected area in editors Color editor_selection; // Selected area in editors
Color entrance_color;
Color hole_color;
Color exit_color;
Color item_color;
Color sprite_color;
// Style parameters // Style parameters
float window_rounding = 0.0f; float window_rounding = 0.0f;