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) {}
138 sessions_.push_back(std::make_unique<RomSession>(
142 const size_t new_session_index =
sessions_.size() - 1;
150 LOG_INFO(
"SessionCoordinator",
"Created new session %zu (total: %zu)",
172 sessions_.push_back(std::make_unique<RomSession>(
176 const size_t new_session_index =
sessions_.size() - 1;
184 LOG_INFO(
"SessionCoordinator",
"Duplicated session %zu (total: %zu)",
242 bool allow_closing_last) {
260 sessions_[index]->editors.PrepareForSessionTeardown();
286 if (closing_active_session && !
sessions_.empty()) {
298 LOG_INFO(
"SessionCoordinator",
"Closed session %zu (total: %zu)", index,
324 if (old_index != index) {
361 return session ? &session->rom :
nullptr;
366 return session ? &session->game_data :
nullptr;
371 return session ? &session->editors :
nullptr;
390 const std::string& filepath,
391 std::optional<size_t> excluded_session_id)
const {
392 if (filepath.empty()) {
393 return absl::OkStatus();
397 if (!session || !session->rom.is_loaded() ||
398 (excluded_session_id.has_value() &&
399 session->session_id() == *excluded_session_id)) {
403 const std::string rom_filepath = session->rom.filename();
406 const std::string& owner_path =
407 session->filepath.empty() ? rom_filepath : session->filepath;
408 return absl::AlreadyExistsError(absl::StrFormat(
409 "ROM backing file '%s' is already open in session %zu ('%s')",
410 filepath, session->session_id(), owner_path));
413 return absl::OkStatus();
417 const std::string& filepath)
const {
428 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
429 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
430 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
440 for (
size_t i = 0; i <
sessions_.size(); ++i) {
443 ImGui::PushID(
static_cast<int>(i));
455 if (ImGui::BeginPopupContextItem(
"SessionContextMenu")) {
466 if (ImGui::Button(absl::StrFormat(
"%s New Session",
ICON_MD_ADD).c_str())) {
478 ImGui::Button(absl::StrFormat(
"%s Close",
ICON_MD_CLOSE).c_str())) {
492 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
493 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
494 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
512 if (ImGui::BeginTable(
"SessionTable", 4,
513 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
514 ImGuiTableFlags_Resizable)) {
515 ImGui::TableSetupColumn(
"Session", ImGuiTableColumnFlags_WidthStretch,
517 ImGui::TableSetupColumn(
"ROM File", ImGuiTableColumnFlags_WidthStretch,
519 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
520 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
522 ImGui::TableHeadersRow();
524 for (
size_t i = 0; i <
sessions_.size(); ++i) {
528 ImGui::PushID(
static_cast<int>(i));
530 ImGui::TableNextRow();
533 ImGui::TableNextColumn();
544 ImGui::TableNextColumn();
545 if (session->rom.is_loaded()) {
546 ImGui::Text(
"%s", session->filepath.c_str());
548 ImGui::TextDisabled(tr(
"(No ROM loaded)"));
552 ImGui::TableNextColumn();
555 }
else if (session->rom.is_loaded()) {
562 ImGui::TableNextColumn();
563 if (!is_active && ImGui::SmallButton(tr(
"Switch"))) {
593 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
594 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
595 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
608 if (ImGui::Button(tr(
"OK"))) {
615 if (ImGui::Button(tr(
"Cancel"))) {
628 for (
size_t i = 0; i <
sessions_.size(); ++i) {
633 if (session->rom.is_loaded()) {
641 if (ImGui::BeginTabItem(tab_name.c_str())) {
653 if (ImGui::BeginPopupContextItem(
654 absl::StrFormat(
"SessionTabContext_%zu", i).c_str())) {
675 if (ImGui::IsItemHovered()) {
676 ImGui::SetTooltip(tr(
"Active Session: %s\nClick to open session switcher"),
680 if (ImGui::IsItemClicked()) {
687 return "Invalid Session";
692 if (!session->custom_name.empty()) {
693 return session->custom_name;
696 if (session->rom.is_loaded()) {
697 return absl::StrFormat(
698 "Session %zu (%s)", index,
699 std::filesystem::path(session->filepath).stem().string());
702 return absl::StrFormat(
"Session %zu (Empty)", index);
710 const std::string& new_name) {
714 sessions_[index]->custom_name = new_name;
715 LOG_INFO(
"SessionCoordinator",
"Renamed session %zu to '%s'", index,
720 const std::string& editor_name,
size_t session_index)
const {
731 const auto& session =
sessions_[session_index];
732 std::string session_name = session->custom_name.empty()
733 ? session->rom.title()
734 : session->custom_name;
737 if (session_name.length() > 20) {
738 session_name = session_name.substr(0, 17) +
"...";
741 return absl::StrFormat(
"%s - %s##session_%zu", editor_name, session_name,
790 for (
size_t session_idx = 0; session_idx <
sessions_.size(); ++session_idx) {
802 for (
auto editor : session->editors.active_editors_) {
803 if (*editor->active()) {
811 if (overworld_editor.jump_to_tab() != -1) {
813 session->editors.GetDungeonEditor()->add_room(
814 overworld_editor.jump_to_tab());
820 bool is_card_based_editor =
823 if (is_card_based_editor) {
830 absl::Status status = editor->Update();
834 std::string editor_name =
837 absl::StrFormat(
"%s Error: %s", editor_name, status.message()),
844 kEditorNames[
static_cast<int>(editor->type())], session_idx);
847 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
848 ImGuiCond_FirstUseEver);
849 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
850 ImGuiCond_FirstUseEver);
852 if (ImGui::Begin(window_title.c_str(), editor->active(),
853 ImGuiWindowFlags_None)) {
860 absl::Status status = editor->Update();
864 std::string editor_name =
899 if (session->rom.is_loaded()) {
911 size_t session_index) {
912 if (filename.empty()) {
913 return absl::InvalidArgumentError(
"Invalid parameters");
916 size_t target_index =
919 return absl::InvalidArgumentError(
"Invalid session index");
923 LOG_INFO(
"SessionCoordinator",
"LoadRomIntoSession: %s -> session %zu",
924 filename.c_str(), target_index);
926 return absl::OkStatus();
930 const std::string& filename) {
932 return absl::FailedPreconditionError(
"No active session");
936 LOG_INFO(
"SessionCoordinator",
"SaveActiveSession: session %zu",
939 return absl::OkStatus();
943 const std::string& filename) {
945 return absl::InvalidArgumentError(
"Invalid parameters");
949 LOG_INFO(
"SessionCoordinator",
"SaveSessionAs: session %zu -> %s",
950 session_index, filename.c_str());
952 return absl::OkStatus();
956 Rom&& rom,
const std::string& filepath) {
958 if (!path_status.ok()) {
963 const size_t new_session_index =
sessions_.size();
964 sessions_.push_back(std::make_unique<RomSession>(
967 session->filepath = filepath;
987 return session.get();
993 [session_id](
const std::unique_ptr<RomSession>& session) {
994 return session && session->session_id() == session_id;
997 return absl::NotFoundError(
998 absl::StrFormat(
"Session %zu is no longer available", session_id));
1001 const size_t index =
1002 static_cast<size_t>(std::distance(
sessions_.begin(), session_it));
1004 (*session_it)->editors.PrepareForSessionTeardown();
1022 if (closing_active_session) {
1030 "Discarded provisional session %zu after failed open", session_id);
1031 return absl::OkStatus();
1036 size_t loaded_count = 0;
1038 if (session->rom.is_loaded()) {
1043 if (loaded_count > 0) {
1045 if (!(*it)->rom.is_loaded() &&
sessions_.size() > 1) {
1054 LOG_INFO(
"SessionCoordinator",
"Cleaned up closed sessions (remaining: %zu)",
1066 session->editors.PrepareForSessionTeardown();
1078 LOG_INFO(
"SessionCoordinator",
"Cleared all sessions");
1118 throw std::out_of_range(
1119 absl::StrFormat(
"Invalid session index: %zu", index));
1124 const std::string& base_name)
const {
1128 std::string name = base_name;
1134 if (session->custom_name == name) {
1143 name = absl::StrFormat(
"%s %d", base_name, counter++);
1152 absl::StrFormat(
"Maximum %zu sessions allowed",
kMaxSessions),
1158 const std::string& operation,
bool success) {
1160 std::string message =
1161 absl::StrFormat(
"%s %s", operation, success ?
"succeeded" :
"failed");
1177 if (session->rom.is_loaded()) {
1185 if (ImGui::BeginTabItem(tab_name.c_str())) {
1193 ImGui::EndTabItem();
1198 std::vector<gui::MenuItemSpec> items;
1199 items.emplace_back(
"Switch to Session",
ICON_MD_TAB, [
this, index]() {
1206 items.back().separator_after =
true;
1218 items.back().separator_after =
true;
1223 modified ?
"Close Session..." :
"Close Session",
ICON_MD_CLOSE,
1233 items.push_back(std::move(close_item));
1248 if (session->rom.is_loaded()) {
1257 static const ImVec4 colors[] = {
1258 ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
1259 ImVec4(0.0f, 0.5f, 1.0f, 1.0f),
1260 ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1261 ImVec4(1.0f, 0.0f, 1.0f, 1.0f),
1262 ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
1263 ImVec4(0.0f, 1.0f, 1.0f, 1.0f),
1264 ImVec4(1.0f, 0.0f, 0.0f, 1.0f),
1265 ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
1268 return colors[index % (
sizeof(colors) /
sizeof(colors[0]))];
1277 if (session->rom.is_loaded()) {
1298 if (session->rom.is_loaded() && session->rom.dirty()) {
1302 if (session->editors.HasPendingGraphicsChanges() ||
1303 session->HasPendingGfxGroupChanges()) {
1307 if (session->editors.HasPendingScreenChanges()) {
1315 if (session->project_dirty ||
1316 session->editors.HasPendingProjectDraftChanges() ||
1317 (session->project_file_editor_state.initialized &&
1318 session->project_file_editor_state.modified)) {
1324 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_
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)
void DrawSessionBadge(size_t index)
void RemoveSession(size_t index)
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)