diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index 91f380f4..cca59107 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -46,6 +46,10 @@ void DungeonEditor::Initialize() { } absl::Status DungeonEditor::Load() { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + auto dungeon_man_pal_group = rom()->palette_group().dungeon_main; // Use room loader component for loading rooms diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index 58b0075f..2e31edac 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -83,6 +83,14 @@ class DungeonEditor : public Editor { } Rom* rom() const { return rom_; } + // ROM state methods (from Editor base class) + bool IsRomLoaded() const override { return rom_ && rom_->is_loaded(); } + std::string GetRomStatus() const override { + if (!rom_) return "No ROM loaded"; + if (!rom_->is_loaded()) return "ROM failed to load"; + return absl::StrFormat("ROM loaded: %s", rom_->title()); + } + private: absl::Status RefreshGraphics(); diff --git a/src/app/editor/editor.h b/src/app/editor/editor.h index 211b904f..9c8e17e8 100644 --- a/src/app/editor/editor.h +++ b/src/app/editor/editor.h @@ -3,8 +3,11 @@ #include #include +#include #include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" #include "app/editor/system/command_manager.h" #include "app/editor/system/extension_manager.h" #include "app/editor/system/history_manager.h" @@ -101,10 +104,30 @@ class Editor { bool* active() { return &active_; } void set_active(bool active) { active_ = active; } + // ROM loading state helpers (default implementations) + virtual bool IsRomLoaded() const { return false; } + virtual std::string GetRomStatus() const { return "ROM state not implemented"; } + protected: bool active_ = false; EditorType type_; EditorContext* context_ = nullptr; + + // Helper method for ROM access with safety check + template + absl::StatusOr SafeRomAccess(std::function accessor, const std::string& operation = "") const { + if (!IsRomLoaded()) { + return absl::FailedPreconditionError( + operation.empty() ? "ROM not loaded" : + absl::StrFormat("%s: ROM not loaded", operation)); + } + try { + return accessor(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrFormat( + "%s: %s", operation.empty() ? "ROM access failed" : operation, e.what())); + } + } }; } // namespace editor diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 44bc3654..1366ba8f 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -423,8 +423,6 @@ void EditorManager::Initialize(const std::string &filename) { {}, {}, { - {absl::StrCat(ICON_MD_HOME, " Home"), "", - [&]() { show_homepage_ = true; }}, {kAssemblyEditorName, "", [&]() { show_asm_editor_ = true; }, [&]() { return show_asm_editor_; }}, {kDungeonEditorName, "", @@ -734,37 +732,78 @@ absl::Status EditorManager::Update() { autosave_timer_ = 0.0f; } - if (show_homepage_) { - ImGui::Begin("Home", &show_homepage_); - DrawHomepage(); - ImGui::End(); - } + // Check if ROM is loaded before allowing editor updates if (!current_editor_set_) { + // Show welcome screen when no session is active + if (sessions_.empty()) { + DrawWelcomeScreen(); + } return absl::OkStatus(); } - for (auto editor : current_editor_set_->active_editors_) { - if (*editor->active()) { - if (editor->type() == EditorType::kOverworld) { - auto &overworld_editor = static_cast(*editor); - if (overworld_editor.jump_to_tab() != -1) { - current_editor_set_->dungeon_editor_.set_active(true); - // Set the dungeon editor to the jump to tab - current_editor_set_->dungeon_editor_.add_room( - overworld_editor.jump_to_tab()); - overworld_editor.jump_to_tab_ = -1; + + // Check if current ROM is valid + if (!current_rom_) { + DrawWelcomeScreen(); + return absl::OkStatus(); + } + + // Check if any editors are active across ALL sessions + bool any_editor_active = false; + for (const auto& session : sessions_) { + if (!session.rom.is_loaded()) continue; + for (auto editor : session.editors.active_editors_) { + if (*editor->active()) { + any_editor_active = true; + break; + } + } + if (any_editor_active) break; + } + + // Show welcome screen if no editors are active (ROM loaded but editors not opened) + if (!any_editor_active) { + DrawWelcomeScreen(); + return absl::OkStatus(); + } + + // Iterate through ALL sessions to support multi-session docking + for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) { + auto& session = sessions_[session_idx]; + if (!session.rom.is_loaded()) continue; // Skip sessions with invalid ROMs + + for (auto editor : session.editors.active_editors_) { + if (*editor->active()) { + if (editor->type() == EditorType::kOverworld) { + auto &overworld_editor = static_cast(*editor); + if (overworld_editor.jump_to_tab() != -1) { + session.editors.dungeon_editor_.set_active(true); + // Set the dungeon editor to the jump to tab + session.editors.dungeon_editor_.add_room(overworld_editor.jump_to_tab()); + overworld_editor.jump_to_tab_ = -1; + } } - } - // Generate unique window titles for multi-session support - size_t session_index = GetCurrentSessionIndex(); - std::string window_title = GenerateUniqueEditorTitle(editor->type(), session_index); - - if (ImGui::Begin(window_title.c_str(), editor->active())) { - current_editor_ = editor; - status_ = editor->Update(); + // Generate unique window titles for multi-session support + std::string window_title = GenerateUniqueEditorTitle(editor->type(), session_idx); + + if (ImGui::Begin(window_title.c_str(), editor->active())) { + // Temporarily switch context for this editor's update + Rom* prev_rom = current_rom_; + EditorSet* prev_editor_set = current_editor_set_; + + current_rom_ = &session.rom; + current_editor_set_ = &session.editors; + current_editor_ = editor; + + status_ = editor->Update(); + + // Restore context + current_rom_ = prev_rom; + current_editor_set_ = prev_editor_set; + } + ImGui::End(); } - ImGui::End(); } } return absl::OkStatus(); @@ -1424,6 +1463,26 @@ size_t EditorManager::GetCurrentSessionIndex() const { return 0; // Default to first session if not found } +std::string EditorManager::GenerateUniqueEditorTitle(EditorType type, size_t session_index) const { + const char* base_name = kEditorNames[static_cast(type)]; + + if (sessions_.size() <= 1) { + // Single session - use simple name + return std::string(base_name); + } + + // Multi-session - include session identifier + const auto& session = sessions_[session_index]; + std::string session_name = session.GetDisplayName(); + + // Truncate long session names + if (session_name.length() > 20) { + session_name = session_name.substr(0, 17) + "..."; + } + + return absl::StrFormat("%s - %s##session_%zu", base_name, session_name, session_index); +} + // Layout Management Functions void EditorManager::ResetWorkspaceLayout() { // Show confirmation popup first @@ -1820,5 +1879,172 @@ void EditorManager::DrawSessionRenameDialog() { ImGui::End(); } +void EditorManager::DrawWelcomeScreen() { + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(680, 500), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar; + + if (ImGui::Begin("Welcome to Yaze", nullptr, flags)) { + // Header (reuse homepage style) + TextWrapped("Welcome to the Yet Another Zelda3 Editor (yaze)!"); + TextWrapped("The Legend of Zelda: A Link to the Past."); + + // Show different messages based on state + if (!sessions_.empty() && !current_rom_) { + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), ICON_MD_WARNING " ROM Loading Required"); + TextWrapped("A session exists but no ROM is loaded. Please load a ROM file to continue editing."); + ImGui::Text("Active Sessions: %zu", sessions_.size()); + } else { + ImGui::Separator(); + ImGui::Spacing(); + TextWrapped("No ROM loaded."); + } + + ImGui::Spacing(); + + // Primary actions with material design icons + ImGui::Text("Get Started:"); + ImGui::Spacing(); + + if (ImGui::Button(ICON_MD_FILE_OPEN " Open ROM File", ImVec2(180, 35))) { + status_ = LoadRom(); + if (!status_.ok()) { + toast_manager_.Show(std::string(status_.message()), editor::ToastType::kError); + } + } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open Project", ImVec2(180, 35))) { + auto file_name = core::FileDialogWrapper::ShowOpenFileDialog(); + if (!file_name.empty()) { + status_ = OpenRomOrProject(file_name); + if (!status_.ok()) { + toast_manager_.Show(std::string(status_.message()), editor::ToastType::kError); + } + } + } + + ImGui::Spacing(); + + // Feature flags section (per-session) + ImGui::Text("Options:"); + auto* flags = GetCurrentFeatureFlags(); + Checkbox("Load custom overworld features", &flags->overworld.kLoadCustomOverworld); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Recent files section (reuse homepage logic) + ImGui::Text("Recent Files:"); + ImGui::BeginChild("RecentFiles", ImVec2(0, 100), true); + static RecentFilesManager manager("recent_files.txt"); + manager.Load(); + for (const auto &file : manager.GetRecentFiles()) { + if (gui::ClickableText(file.c_str())) { + status_ = OpenRomOrProject(file); + if (!status_.ok()) { + toast_manager_.Show(std::string(status_.message()), editor::ToastType::kError); + } + } + } + ImGui::EndChild(); + + ImGui::Spacing(); + + // Show editor access buttons for loaded sessions + bool has_loaded_sessions = false; + for (const auto& session : sessions_) { + if (session.rom.is_loaded()) { + has_loaded_sessions = true; + break; + } + } + + if (has_loaded_sessions) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Available Editors:"); + ImGui::Text("Click to open editor windows that can be docked side by side"); + ImGui::Spacing(); + + // Show sessions and their editors + for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) { + const auto& session = sessions_[session_idx]; + if (!session.rom.is_loaded()) continue; + + ImGui::Text("Session: %s", session.GetDisplayName().c_str()); + + // Editor buttons in a grid layout for this session + if (ImGui::BeginTable(absl::StrFormat("EditorsTable##%zu", session_idx).c_str(), 4, + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX)) { + + // Row 1: Primary editors + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_MAP " Overworld##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.overworld_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_DOMAIN " Dungeon##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.dungeon_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_IMAGE " Graphics##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.graphics_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_PALETTE " Palette##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.palette_editor_.set_active(true); + } + + // Row 2: Secondary editors + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_MESSAGE " Message##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.message_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_PERSON " Sprite##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.sprite_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_MUSIC_NOTE " Music##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.music_editor_.set_active(true); + } + ImGui::TableNextColumn(); + if (ImGui::Button(absl::StrFormat(ICON_MD_MONITOR " Screen##%zu", session_idx).c_str(), ImVec2(120, 30))) { + sessions_[session_idx].editors.screen_editor_.set_active(true); + } + + ImGui::EndTable(); + } + + if (session_idx < sessions_.size() - 1) { + ImGui::Spacing(); + } + } + } + + // Links section + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Help & Support:"); + if (gui::ClickableText(ICON_MD_HELP " Getting Started Guide")) { + gui::OpenUrl("https://github.com/scawful/yaze/blob/master/docs/01-getting-started.md"); + } + if (gui::ClickableText(ICON_MD_BUG_REPORT " Report Issues")) { + gui::OpenUrl("https://github.com/scawful/yaze/issues"); + } + + // Show tip about drag and drop + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), ICON_MD_TIPS_AND_UPDATES " Tip: Drag and drop ROM files onto the window"); + } + ImGui::End(); +} + + } // namespace editor } // namespace yaze diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index aadf0f79..7016d59c 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -22,6 +22,7 @@ #include "app/editor/system/toast_manager.h" #include "app/editor/system/settings_editor.h" #include "app/emu/emulator.h" +#include "app/core/features.h" #include "app/rom.h" #include "yaze_config.h" @@ -99,9 +100,19 @@ class EditorManager { absl::Status SetCurrentRom(Rom* rom); auto GetCurrentRom() -> Rom* { return current_rom_; } auto GetCurrentEditorSet() -> EditorSet* { return current_editor_set_; } + + // Get current session's feature flags (falls back to global if no session) + core::FeatureFlags::Flags* GetCurrentFeatureFlags() { + size_t current_index = GetCurrentSessionIndex(); + if (current_index < sessions_.size()) { + return &sessions_[current_index].feature_flags; + } + return &core::FeatureFlags::get(); // Fallback to global + } private: void DrawHomepage(); + void DrawWelcomeScreen(); absl::Status DrawRomSelector(); absl::Status LoadRom(); absl::Status LoadAssets(); @@ -160,11 +171,14 @@ class EditorManager { EditorSet editors; std::string custom_name; // User-defined session name std::string filepath; // ROM filepath for duplicate detection + core::FeatureFlags::Flags feature_flags; // Per-session feature flags RomSession() = default; explicit RomSession(Rom&& r) : rom(std::move(r)), editors(&rom) { filepath = rom.filename(); + // Initialize with default feature flags + feature_flags = core::FeatureFlags::Flags{}; } // Get display name (custom name or ROM title) @@ -201,6 +215,9 @@ class EditorManager { void SwitchToSession(size_t index); size_t GetCurrentSessionIndex() const; void ResetWorkspaceLayout(); + + // Multi-session editor management + std::string GenerateUniqueEditorTitle(EditorType type, size_t session_index) const; void SaveWorkspaceLayout(); void LoadWorkspaceLayout(); void ShowAllWindows(); diff --git a/src/app/editor/editor_safeguards.h b/src/app/editor/editor_safeguards.h new file mode 100644 index 00000000..edb24f9c --- /dev/null +++ b/src/app/editor/editor_safeguards.h @@ -0,0 +1,42 @@ +#ifndef YAZE_APP_EDITOR_SAFEGUARDS_H +#define YAZE_APP_EDITOR_SAFEGUARDS_H + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" + +namespace yaze { +namespace editor { + +// Macro for checking ROM loading state in editor methods +#define REQUIRE_ROM_LOADED(rom_ptr, operation) \ + do { \ + if (!(rom_ptr) || !(rom_ptr)->is_loaded()) { \ + return absl::FailedPreconditionError( \ + absl::StrFormat("%s: ROM not loaded", (operation))); \ + } \ + } while (0) + +// Macro for ROM state checking with custom error message +#define CHECK_ROM_STATE(rom_ptr, message) \ + do { \ + if (!(rom_ptr) || !(rom_ptr)->is_loaded()) { \ + return absl::FailedPreconditionError(message); \ + } \ + } while (0) + +// Helper function for generating consistent ROM status messages +inline std::string GetRomStatusMessage(const Rom* rom) { + if (!rom) return "No ROM loaded"; + if (!rom->is_loaded()) return "ROM failed to load"; + return absl::StrFormat("ROM loaded: %s", rom->title()); +} + +// Helper function to check if ROM is in a valid state for editing +inline bool IsRomReadyForEditing(const Rom* rom) { + return rom && rom->is_loaded() && !rom->title().empty(); +} + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_SAFEGUARDS_H diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index bf2dcf2a..1a157205 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -164,6 +164,10 @@ void OverworldEditor::Initialize() { } absl::Status OverworldEditor::Load() { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + RETURN_IF_ERROR(LoadGraphics()); RETURN_IF_ERROR( tile16_editor_.Initialize(tile16_blockset_bmp_, current_gfx_bmp_, diff --git a/src/app/editor/overworld/overworld_editor.h b/src/app/editor/overworld/overworld_editor.h index 08278d1c..40d78315 100644 --- a/src/app/editor/overworld/overworld_editor.h +++ b/src/app/editor/overworld/overworld_editor.h @@ -106,6 +106,14 @@ class OverworldEditor : public Editor, public gfx::GfxContext { int jump_to_tab() { return jump_to_tab_; } int jump_to_tab_ = -1; + // ROM state methods (from Editor base class) + bool IsRomLoaded() const override { return rom_ && rom_->is_loaded(); } + std::string GetRomStatus() const override { + if (!rom_) return "No ROM loaded"; + if (!rom_->is_loaded()) return "ROM failed to load"; + return absl::StrFormat("ROM loaded: %s", rom_->title()); + } + /** * @brief Load the Bitmap objects for each OverworldMap. *