Implement multi-session support and welcome screen in EditorManager

- Removed the homepage display logic and replaced it with a welcome screen that appears when no ROM is loaded or no active editors are present.
- Enhanced session management by iterating through all sessions to check for active editors, allowing for better multi-session docking.
- Introduced new methods for generating unique editor titles based on session context and added feature flags for per-session configurations.
- Added safeguards for ROM loading state checks in editor methods to prevent operations on unloaded ROMs.
This commit is contained in:
scawful
2025-09-26 17:32:21 -04:00
parent 0f37061299
commit a53e759043
8 changed files with 358 additions and 26 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -3,8 +3,11 @@
#include <array>
#include <vector>
#include <functional>
#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<typename T>
absl::StatusOr<T> SafeRomAccess(std::function<T()> 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

View File

@@ -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<OverworldEditor &>(*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<OverworldEditor &>(*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<int>(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

View File

@@ -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();

View File

@@ -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

View File

@@ -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_,

View File

@@ -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.
*