2#include <absl/status/status.h>
3#include <absl/status/statusor.h>
16#include "absl/strings/str_format.h"
33#include "imgui/imgui.h"
43 std::filesystem::path path(filepath);
50 auto absolute_path = std::filesystem::absolute(path, ec);
52 path = std::move(absolute_path);
56 auto canonical_path = std::filesystem::weakly_canonical(path, ec);
58 path = std::move(canonical_path);
62 return path.lexically_normal();
66 const std::string& rhs) {
67 if (lhs.empty() || rhs.empty()) {
73 if (std::filesystem::equivalent(lhs, rhs, ec) && !ec) {
84 const std::string& rhs) {
85 return editor::PathsReferToSameBackingFile(lhs, rhs);
91 : window_manager_(window_manager),
92 toast_manager_(toast_manager),
93 user_settings_(user_settings) {}
137 sessions_.push_back(std::make_unique<RomSession>(
141 const size_t new_session_index =
sessions_.size() - 1;
149 LOG_INFO(
"SessionCoordinator",
"Created new session %zu (total: %zu)",
171 sessions_.push_back(std::make_unique<RomSession>(
175 const size_t new_session_index =
sessions_.size() - 1;
183 LOG_INFO(
"SessionCoordinator",
"Duplicated session %zu (total: %zu)",
250 sessions_[index]->editors.PrepareForSessionTeardown();
276 if (closing_active_session && !
sessions_.empty()) {
282 LOG_INFO(
"SessionCoordinator",
"Closed session %zu (total: %zu)", index,
308 if (old_index != index) {
345 return session ? &session->rom :
nullptr;
350 return session ? &session->game_data :
nullptr;
355 return session ? &session->editors :
nullptr;
374 const std::string& filepath,
375 std::optional<size_t> excluded_session_id)
const {
376 if (filepath.empty()) {
377 return absl::OkStatus();
381 if (!session || !session->rom.is_loaded() ||
382 (excluded_session_id.has_value() &&
383 session->session_id() == *excluded_session_id)) {
387 const std::string rom_filepath = session->rom.filename();
390 const std::string& owner_path =
391 session->filepath.empty() ? rom_filepath : session->filepath;
392 return absl::AlreadyExistsError(absl::StrFormat(
393 "ROM backing file '%s' is already open in session %zu ('%s')",
394 filepath, session->session_id(), owner_path));
397 return absl::OkStatus();
401 const std::string& filepath)
const {
412 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
413 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
414 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
424 for (
size_t i = 0; i <
sessions_.size(); ++i) {
427 ImGui::PushID(
static_cast<int>(i));
439 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
440 ImGui::OpenPopup(
"SessionContextMenu");
443 if (ImGui::BeginPopup(
"SessionContextMenu")) {
454 if (ImGui::Button(absl::StrFormat(
"%s New Session",
ICON_MD_ADD).c_str())) {
466 ImGui::Button(absl::StrFormat(
"%s Close",
ICON_MD_CLOSE).c_str())) {
480 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
481 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
482 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
500 if (ImGui::BeginTable(
"SessionTable", 4,
501 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
502 ImGuiTableFlags_Resizable)) {
503 ImGui::TableSetupColumn(
"Session", ImGuiTableColumnFlags_WidthStretch,
505 ImGui::TableSetupColumn(
"ROM File", ImGuiTableColumnFlags_WidthStretch,
507 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
508 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
510 ImGui::TableHeadersRow();
512 for (
size_t i = 0; i <
sessions_.size(); ++i) {
516 ImGui::PushID(
static_cast<int>(i));
518 ImGui::TableNextRow();
521 ImGui::TableNextColumn();
532 ImGui::TableNextColumn();
533 if (session->rom.is_loaded()) {
534 ImGui::Text(
"%s", session->filepath.c_str());
536 ImGui::TextDisabled(tr(
"(No ROM loaded)"));
540 ImGui::TableNextColumn();
543 }
else if (session->rom.is_loaded()) {
550 ImGui::TableNextColumn();
551 if (!is_active && ImGui::SmallButton(tr(
"Switch"))) {
581 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
582 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
583 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
596 if (ImGui::Button(tr(
"OK"))) {
603 if (ImGui::Button(tr(
"Cancel"))) {
616 for (
size_t i = 0; i <
sessions_.size(); ++i) {
621 if (session->rom.is_loaded()) {
629 if (ImGui::BeginTabItem(tab_name.c_str())) {
641 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
642 ImGui::OpenPopup(absl::StrFormat(
"SessionTabContext_%zu", i).c_str());
645 if (ImGui::BeginPopup(
646 absl::StrFormat(
"SessionTabContext_%zu", i).c_str())) {
667 if (ImGui::IsItemHovered()) {
668 ImGui::SetTooltip(tr(
"Active Session: %s\nClick to open session switcher"),
672 if (ImGui::IsItemClicked()) {
679 return "Invalid Session";
684 if (!session->custom_name.empty()) {
685 return session->custom_name;
688 if (session->rom.is_loaded()) {
689 return absl::StrFormat(
690 "Session %zu (%s)", index,
691 std::filesystem::path(session->filepath).stem().string());
694 return absl::StrFormat(
"Session %zu (Empty)", index);
702 const std::string& new_name) {
706 sessions_[index]->custom_name = new_name;
707 LOG_INFO(
"SessionCoordinator",
"Renamed session %zu to '%s'", index,
712 const std::string& editor_name,
size_t session_index)
const {
723 const auto& session =
sessions_[session_index];
724 std::string session_name = session->custom_name.empty()
725 ? session->rom.title()
726 : session->custom_name;
729 if (session_name.length() > 20) {
730 session_name = session_name.substr(0, 17) +
"...";
733 return absl::StrFormat(
"%s - %s##session_%zu", editor_name, session_name,
782 for (
size_t session_idx = 0; session_idx <
sessions_.size(); ++session_idx) {
794 for (
auto editor : session->editors.active_editors_) {
795 if (*editor->active()) {
803 if (overworld_editor.jump_to_tab() != -1) {
805 session->editors.GetDungeonEditor()->add_room(
806 overworld_editor.jump_to_tab());
812 bool is_card_based_editor =
815 if (is_card_based_editor) {
822 absl::Status status = editor->Update();
826 std::string editor_name =
829 absl::StrFormat(
"%s Error: %s", editor_name, status.message()),
836 kEditorNames[
static_cast<int>(editor->type())], session_idx);
839 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
840 ImGuiCond_FirstUseEver);
841 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
842 ImGuiCond_FirstUseEver);
844 if (ImGui::Begin(window_title.c_str(), editor->active(),
845 ImGuiWindowFlags_None)) {
852 absl::Status status = editor->Update();
856 std::string editor_name =
891 if (session->rom.is_loaded()) {
903 size_t session_index) {
904 if (filename.empty()) {
905 return absl::InvalidArgumentError(
"Invalid parameters");
908 size_t target_index =
911 return absl::InvalidArgumentError(
"Invalid session index");
915 LOG_INFO(
"SessionCoordinator",
"LoadRomIntoSession: %s -> session %zu",
916 filename.c_str(), target_index);
918 return absl::OkStatus();
922 const std::string& filename) {
924 return absl::FailedPreconditionError(
"No active session");
928 LOG_INFO(
"SessionCoordinator",
"SaveActiveSession: session %zu",
931 return absl::OkStatus();
935 const std::string& filename) {
937 return absl::InvalidArgumentError(
"Invalid parameters");
941 LOG_INFO(
"SessionCoordinator",
"SaveSessionAs: session %zu -> %s",
942 session_index, filename.c_str());
944 return absl::OkStatus();
948 Rom&& rom,
const std::string& filepath) {
950 if (!path_status.ok()) {
955 const size_t new_session_index =
sessions_.size();
956 sessions_.push_back(std::make_unique<RomSession>(
959 session->filepath = filepath;
979 return session.get();
985 [session_id](
const std::unique_ptr<RomSession>& session) {
986 return session && session->session_id() == session_id;
989 return absl::NotFoundError(
990 absl::StrFormat(
"Session %zu is no longer available", session_id));
994 static_cast<size_t>(std::distance(
sessions_.begin(), session_it));
996 (*session_it)->editors.PrepareForSessionTeardown();
1014 if (closing_active_session) {
1022 "Discarded provisional session %zu after failed open", session_id);
1023 return absl::OkStatus();
1028 size_t loaded_count = 0;
1030 if (session->rom.is_loaded()) {
1035 if (loaded_count > 0) {
1037 if (!(*it)->rom.is_loaded() &&
sessions_.size() > 1) {
1046 LOG_INFO(
"SessionCoordinator",
"Cleaned up closed sessions (remaining: %zu)",
1058 session->editors.PrepareForSessionTeardown();
1070 LOG_INFO(
"SessionCoordinator",
"Cleared all sessions");
1110 throw std::out_of_range(
1111 absl::StrFormat(
"Invalid session index: %zu", index));
1116 const std::string& base_name)
const {
1120 std::string name = base_name;
1126 if (session->custom_name == name) {
1135 name = absl::StrFormat(
"%s %d", base_name, counter++);
1144 absl::StrFormat(
"Maximum %zu sessions allowed",
kMaxSessions),
1150 const std::string& operation,
bool success) {
1152 std::string message =
1153 absl::StrFormat(
"%s %s", operation, success ?
"succeeded" :
"failed");
1169 if (session->rom.is_loaded()) {
1177 if (ImGui::BeginTabItem(tab_name.c_str())) {
1185 ImGui::EndTabItem();
1190 if (ImGui::MenuItem(
1191 absl::StrFormat(
"%s Switch to Session",
ICON_MD_TAB).c_str())) {
1199 if (ImGui::MenuItem(absl::StrFormat(
"%s Rename",
ICON_MD_EDIT).c_str())) {
1207 if (ImGui::MenuItem(
1216 absl::StrFormat(
"%s Close Session",
ICON_MD_CLOSE).c_str())) {
1234 if (session->rom.is_loaded()) {
1243 static const ImVec4 colors[] = {
1244 ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
1245 ImVec4(0.0f, 0.5f, 1.0f, 1.0f),
1246 ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1247 ImVec4(1.0f, 0.0f, 1.0f, 1.0f),
1248 ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
1249 ImVec4(0.0f, 1.0f, 1.0f, 1.0f),
1250 ImVec4(1.0f, 0.0f, 0.0f, 1.0f),
1251 ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
1254 return colors[index % (
sizeof(colors) /
sizeof(colors[0]))];
1263 if (session->rom.is_loaded()) {
1284 if (session->rom.is_loaded() && session->rom.dirty()) {
1288 if (session->editors.HasPendingGraphicsChanges()) {
1292 if (session->editors.HasPendingScreenChanges()) {
1300 if (session->project_dirty ||
1301 session->editors.HasPendingProjectDraftChanges() ||
1302 (session->project_file_editor_state.initialized &&
1303 session->project_file_editor_state.modified)) {
1309 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 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 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)
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 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 *, 14 > kEditorNames
ImVec4 ConvertColorToImVec4(const Color &color)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
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)