2#include <absl/status/status.h>
3#include <absl/status/statusor.h>
16#include "absl/strings/str_format.h"
34#include "imgui/imgui.h"
44 std::filesystem::path path(filepath);
51 auto absolute_path = std::filesystem::absolute(path, ec);
53 path = std::move(absolute_path);
57 auto canonical_path = std::filesystem::weakly_canonical(path, ec);
59 path = std::move(canonical_path);
63 return path.lexically_normal();
67 const std::string& rhs) {
68 if (lhs.empty() || rhs.empty()) {
74 if (std::filesystem::equivalent(lhs, rhs, ec) && !ec) {
85 const std::string& rhs) {
86 return editor::PathsReferToSameBackingFile(lhs, rhs);
92 : window_manager_(window_manager),
93 toast_manager_(toast_manager),
94 user_settings_(user_settings) {}
140 sessions_.push_back(std::make_unique<RomSession>(
144 const size_t new_session_index =
sessions_.size() - 1;
152 LOG_INFO(
"SessionCoordinator",
"Created new session %zu (total: %zu)",
174 sessions_.push_back(std::make_unique<RomSession>(
178 const size_t new_session_index =
sessions_.size() - 1;
186 LOG_INFO(
"SessionCoordinator",
"Duplicated session %zu (total: %zu)",
244 bool allow_closing_last) {
259 if (closing_active_session) {
261 if (!transition.ok()) {
263 "Close Session: " + std::string(transition.message()),
false);
270 sessions_[index]->editors.PrepareForSessionTeardown();
296 if (closing_active_session && !
sessions_.empty()) {
308 LOG_INFO(
"SessionCoordinator",
"Closed session %zu (total: %zu)", index,
328 if (!transition.ok()) {
330 "Switch Session: " + std::string(transition.message()),
false);
352 if (old_index != index) {
389 return session ? &session->rom :
nullptr;
394 return session ? &session->game_data :
nullptr;
399 return session ? &session->editors :
nullptr;
418 const std::string& filepath,
419 std::optional<size_t> excluded_session_id)
const {
420 if (filepath.empty()) {
421 return absl::OkStatus();
425 if (!session || !session->rom.is_loaded() ||
426 (excluded_session_id.has_value() &&
427 session->session_id() == *excluded_session_id)) {
431 const std::string rom_filepath = session->rom.filename();
434 const std::string& owner_path =
435 session->filepath.empty() ? rom_filepath : session->filepath;
436 return absl::AlreadyExistsError(absl::StrFormat(
437 "ROM backing file '%s' is already open in session %zu ('%s')",
438 filepath, session->session_id(), owner_path));
441 return absl::OkStatus();
445 const std::string& filepath)
const {
456 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
457 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
458 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
468 for (
size_t i = 0; i <
sessions_.size(); ++i) {
471 ImGui::PushID(
static_cast<int>(i));
483 if (ImGui::BeginPopupContextItem(
"SessionContextMenu")) {
494 if (ImGui::Button(absl::StrFormat(
"%s New Session",
ICON_MD_ADD).c_str())) {
506 ImGui::Button(absl::StrFormat(
"%s Close",
ICON_MD_CLOSE).c_str())) {
520 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
521 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
522 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
540 if (ImGui::BeginTable(
"SessionTable", 4,
541 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
542 ImGuiTableFlags_Resizable)) {
543 ImGui::TableSetupColumn(
"Session", ImGuiTableColumnFlags_WidthStretch,
545 ImGui::TableSetupColumn(
"ROM File", ImGuiTableColumnFlags_WidthStretch,
547 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
548 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
550 ImGui::TableHeadersRow();
552 for (
size_t i = 0; i <
sessions_.size(); ++i) {
556 ImGui::PushID(
static_cast<int>(i));
558 ImGui::TableNextRow();
561 ImGui::TableNextColumn();
572 ImGui::TableNextColumn();
573 if (session->rom.is_loaded()) {
574 ImGui::Text(
"%s", session->filepath.c_str());
576 ImGui::TextDisabled(tr(
"(No ROM loaded)"));
580 ImGui::TableNextColumn();
583 }
else if (session->rom.is_loaded()) {
590 ImGui::TableNextColumn();
591 if (!is_active && ImGui::SmallButton(tr(
"Switch"))) {
621 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
622 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
623 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
636 if (ImGui::Button(tr(
"OK"))) {
643 if (ImGui::Button(tr(
"Cancel"))) {
656 for (
size_t i = 0; i <
sessions_.size(); ++i) {
661 if (session->rom.is_loaded()) {
669 if (ImGui::BeginTabItem(tab_name.c_str())) {
681 if (ImGui::BeginPopupContextItem(
682 absl::StrFormat(
"SessionTabContext_%zu", i).c_str())) {
703 if (ImGui::IsItemHovered()) {
704 ImGui::SetTooltip(tr(
"Active Session: %s\nClick to open session switcher"),
708 if (ImGui::IsItemClicked()) {
715 return "Invalid Session";
720 if (!session->custom_name.empty()) {
721 return session->custom_name;
724 if (session->rom.is_loaded()) {
725 return absl::StrFormat(
726 "Session %zu (%s)", index,
727 std::filesystem::path(session->filepath).stem().string());
730 return absl::StrFormat(
"Session %zu (Empty)", index);
738 const std::string& new_name) {
742 sessions_[index]->custom_name = new_name;
743 LOG_INFO(
"SessionCoordinator",
"Renamed session %zu to '%s'", index,
748 const std::string& editor_name,
size_t session_index)
const {
759 const auto& session =
sessions_[session_index];
760 std::string session_name = session->custom_name.empty()
761 ? session->rom.title()
762 : session->custom_name;
765 if (session_name.length() > 20) {
766 session_name = session_name.substr(0, 17) +
"...";
769 return absl::StrFormat(
"%s - %s##session_%zu", editor_name, session_name,
818 for (
size_t session_idx = 0; session_idx <
sessions_.size(); ++session_idx) {
830 for (
auto editor : session->editors.active_editors_) {
831 if (*editor->active()) {
839 if (overworld_editor.jump_to_tab() != -1) {
841 session->editors.GetDungeonEditor()->add_room(
842 overworld_editor.jump_to_tab());
845 if (overworld_editor.jump_to_entrance_ != -1) {
846 session->editors.GetDungeonEditor()->OpenRegularEntrance(
847 overworld_editor.jump_to_entrance_);
848 overworld_editor.jump_to_entrance_ = -1;
853 bool is_card_based_editor =
856 if (is_card_based_editor) {
863 absl::Status status = editor->Update();
867 std::string editor_name =
870 absl::StrFormat(
"%s Error: %s", editor_name, status.message()),
877 kEditorNames[
static_cast<int>(editor->type())], session_idx);
880 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
881 ImGuiCond_FirstUseEver);
882 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
883 ImGuiCond_FirstUseEver);
885 if (ImGui::Begin(window_title.c_str(), editor->active(),
886 ImGuiWindowFlags_None)) {
893 absl::Status status = editor->Update();
897 std::string editor_name =
932 if (session->rom.is_loaded()) {
944 size_t session_index) {
945 if (filename.empty()) {
946 return absl::InvalidArgumentError(
"Invalid parameters");
949 size_t target_index =
952 return absl::InvalidArgumentError(
"Invalid session index");
956 LOG_INFO(
"SessionCoordinator",
"LoadRomIntoSession: %s -> session %zu",
957 filename.c_str(), target_index);
959 return absl::OkStatus();
963 const std::string& filename) {
965 return absl::FailedPreconditionError(
"No active session");
969 LOG_INFO(
"SessionCoordinator",
"SaveActiveSession: session %zu",
972 return absl::OkStatus();
976 const std::string& filename) {
978 return absl::InvalidArgumentError(
"Invalid parameters");
982 LOG_INFO(
"SessionCoordinator",
"SaveSessionAs: session %zu -> %s",
983 session_index, filename.c_str());
985 return absl::OkStatus();
989 Rom&& rom,
const std::string& filepath) {
991 if (!path_status.ok()) {
995 if (!transition.ok()) {
1000 const size_t new_session_index =
sessions_.size();
1001 sessions_.push_back(std::make_unique<RomSession>(
1004 session->filepath = filepath;
1024 return session.get();
1030 [session_id](
const std::unique_ptr<RomSession>& session) {
1031 return session && session->session_id() == session_id;
1034 return absl::NotFoundError(
1035 absl::StrFormat(
"Session %zu is no longer available", session_id));
1038 const size_t index =
1039 static_cast<size_t>(std::distance(
sessions_.begin(), session_it));
1041 if (closing_active_session) {
1043 if (!transition.ok()) {
1047 (*session_it)->editors.PrepareForSessionTeardown();
1065 if (closing_active_session) {
1073 "Discarded provisional session %zu after failed open", session_id);
1074 return absl::OkStatus();
1079 size_t loaded_count = 0;
1081 if (session->rom.is_loaded()) {
1086 if (loaded_count > 0) {
1088 if (!(*it)->rom.is_loaded() &&
sessions_.size() > 1) {
1097 LOG_INFO(
"SessionCoordinator",
"Cleaned up closed sessions (remaining: %zu)",
1105 if (!transition.ok()) {
1107 "Clear Sessions: " + std::string(transition.message()),
false);
1115 session->editors.PrepareForSessionTeardown();
1127 LOG_INFO(
"SessionCoordinator",
"Cleared all sessions");
1167 throw std::out_of_range(
1168 absl::StrFormat(
"Invalid session index: %zu", index));
1173 const std::string& base_name)
const {
1177 std::string name = base_name;
1183 if (session->custom_name == name) {
1192 name = absl::StrFormat(
"%s %d", base_name, counter++);
1201 absl::StrFormat(
"Maximum %zu sessions allowed",
kMaxSessions),
1207 const std::string& operation,
bool success) {
1209 std::string message =
1210 absl::StrFormat(
"%s %s", operation, success ?
"succeeded" :
"failed");
1226 if (session->rom.is_loaded()) {
1234 if (ImGui::BeginTabItem(tab_name.c_str())) {
1242 ImGui::EndTabItem();
1247 std::vector<gui::MenuItemSpec> items;
1248 items.emplace_back(
"Switch to Session",
ICON_MD_TAB, [
this, index]() {
1255 items.back().separator_after =
true;
1267 items.back().separator_after =
true;
1272 modified ?
"Close Session..." :
"Close Session",
ICON_MD_CLOSE,
1282 items.push_back(std::move(close_item));
1297 if (session->rom.is_loaded()) {
1306 static const ImVec4 colors[] = {
1307 ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
1308 ImVec4(0.0f, 0.5f, 1.0f, 1.0f),
1309 ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1310 ImVec4(1.0f, 0.0f, 1.0f, 1.0f),
1311 ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
1312 ImVec4(0.0f, 1.0f, 1.0f, 1.0f),
1313 ImVec4(1.0f, 0.0f, 0.0f, 1.0f),
1314 ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
1317 return colors[index % (
sizeof(colors) /
sizeof(colors[0]))];
1326 if (session->rom.is_loaded()) {
1347 if (session->rom.is_loaded() && session->rom.dirty()) {
1351 if (session->editors.HasPendingSpriteChanges() ||
1352 session->editors.HasPendingGraphicsChanges() ||
1353 session->HasPendingGfxGroupChanges()) {
1357 if (session->editors.HasPendingScreenChanges()) {
1365 if (session->project_dirty ||
1366 session->editors.HasPendingProjectDraftChanges() ||
1367 (session->project_file_editor_state.initialized &&
1368 session->project_file_editor_state.modified)) {
1374 return dungeon_editor->HasPendingDungeonChanges();
void Publish(const T &event)
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
DungeonEditorV2 - Simplified dungeon editor using component delegation.
static bool IsPanelBasedEditor(EditorType type)
static bool UpdateAllowedWithoutLoadedRom(EditorType type)
Contains a complete set of editors for a single ROM instance.
Interface for editor classes.
virtual void RequestCloseSession(size_t index)=0
virtual void RequestSwitchToSession(size_t index)=0
virtual Editor * GetCurrentEditor() const =0
virtual void ConfigureSession(RomSession *session)=0
virtual void SetCurrentEditor(Editor *editor)=0
Main UI class for editing overworld maps in A Link to the Past.
void NotifySessionCreated(size_t index, RomSession *session)
void SwitchToSession(size_t index)
void * GetActiveSession() const
char session_rename_buffer_[256]
void * GetSession(size_t index) const
size_t GetEmptySessionCount() const
std::string GenerateUniqueEditorTitle(const std::string &editor_name, size_t session_index) const
void CloseSessionAllowingEmpty(size_t index)
void CleanupClosedSessions()
zelda3::GameData * GetCurrentGameData() const
EditorSet * GetCurrentEditorSet() const
size_t GetActiveSessionIndex() const
Compact zero-based UI position in sessions_.
void CloseCurrentSession()
void NotifySessionSwitched(size_t old_index, size_t new_index, RomSession *session, bool transient)
EditorRegistry * editor_registry_
void NotifySessionClosed(size_t index)
RomSession * GetActiveRomSession() const
void ShowAllPanelsInActiveSession()
void CloseSessionInternal(size_t index, bool allow_closing_last)
void CloseSession(size_t index)
size_t GetActiveSessionCount() const
void NotifySessionRomLoaded(size_t index, RomSession *session)
void FocusPreviousSession()
void UpdateSessionCount()
void DrawSessionManager()
absl::StatusOr< RomSession * > CreateSessionFromRom(Rom &&rom, const std::string &filepath)
SessionCoordinator(WorkspaceWindowManager *window_manager, ToastManager *toast_manager, UserSettings *user_settings)
UserSettings * user_settings_
void DuplicateCurrentSession()
void DrawSessionContextMenu(size_t index)
absl::Status SaveSessionAs(size_t session_index, const std::string &filename)
size_t GetSessionId(size_t index) const
Resolve a compact UI index to its stable workspace identity.
size_t active_session_index_
absl::Status SaveActiveSession(const std::string &filename="")
void ActivateSession(size_t index)
ImVec4 GetSessionColor(size_t index) const
absl::Status LoadRomIntoSession(const std::string &filename, size_t session_index=SIZE_MAX)
absl::Status CheckBackingFileAvailable(const std::string &filepath, std::optional< size_t > excluded_session_id=std::nullopt) const
std::string GetSessionDisplayName(size_t index) const
void RenameSession(size_t index, const std::string &new_name)
bool IsSessionModified(size_t index) const
absl::Status DiscardProvisionalSession(size_t session_id)
void ActivateCreatedSession(size_t index)
size_t session_to_rename_
void ApplySessionActivation(size_t index, bool transient)
size_t GetTotalSessionCount() const
void UpdateActiveSession()
bool HasDuplicateSession(const std::string &filepath) const
static constexpr size_t kMinSessions
void ToggleSessionSwitcher()
void RequestCloseCurrentSession()
void ShowPanelsInCategory(const std::string &category)
ToastManager * toast_manager_
void HidePanelsInCategory(const std::string &category)
void HideAllPanelsInActiveSession()
bool IsSessionClosed(size_t index) const
size_t GetActiveSessionId() const
Stable workspace identity that is never reused while this coordinator lives.
WorkspaceWindowManager * window_manager_
std::string GetSessionIcon(size_t index) const
bool show_session_manager_
void DrawSessionRenameDialog()
void ShowSessionLimitWarning()
void DrawSessionSwitcher()
std::string GetActiveSessionDisplayName() const
bool show_session_switcher_
Rom * GetCurrentRom() const
void ShowSessionOperationResult(const std::string &operation, bool success)
bool IsValidSessionIndex(size_t index) const
bool IsSessionEmpty(size_t index) const
void SwitchToSessionInternal(size_t index, bool transient)
bool HasMultipleSessions() const
void DrawSessionTab(size_t index, bool is_active)
std::function< absl::Status()> before_session_transition_
void DrawSessionBadge(size_t index)
void RemoveSession(size_t index)
absl::Status PrepareActiveSessionTransition()
void SetActiveSessionIndex(size_t index)
bool IsSessionActive(size_t index) const
std::vector< std::unique_ptr< RomSession > > sessions_
void ValidateSessionIndex(size_t index) const
ISessionConfigurator * editor_manager_
void DrawSessionIndicator()
bool IsSessionLoaded(size_t index) const
size_t GetLoadedSessionCount() const
std::string GenerateUniqueSessionName(const std::string &base_name) const
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
bool show_session_rename_dialog_
static constexpr size_t kMaxSessions
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Manages user preferences and settings persistence.
Central registry for all editor cards with session awareness and dependency injection.
void HideAllWindowsInCategory(size_t session_id, const std::string &category)
void SetActiveSession(size_t session_id)
void UnregisterSession(size_t session_id)
void HideAllWindowsInSession(size_t session_id)
void ShowAllWindowsInSession(size_t session_id)
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
static PaletteManager & Get()
Get the singleton instance.
RAII guard for ImGui style colors.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_RADIO_BUTTON_CHECKED
#define ICON_MD_CONTENT_COPY
#define ICON_MD_RADIO_BUTTON_UNCHECKED
#define ICON_MD_ANALYTICS
#define ICON_MD_DRIVE_FILE_RENAME_OUTLINE
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
std::filesystem::path NormalizeBackingFilePath(const std::string &filepath)
bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
constexpr std::array< const char *, 15 > kEditorNames
ImVec4 ConvertColorToImVec4(const Color &color)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
static RomLoadedEvent Create(Rom *r, const std::string &file, size_t session)
Represents a single session, containing a ROM and its associated editors.
static SessionClosedEvent Create(size_t idx)
static SessionCreatedEvent Create(size_t idx, RomSession *sess)
static SessionSwitchedEvent Create(size_t old_idx, size_t new_idx, RomSession *sess, bool is_transient=false)