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)",
272 if (closing_active_session && !
sessions_.empty()) {
278 LOG_INFO(
"SessionCoordinator",
"Closed session %zu (total: %zu)", index,
304 if (old_index != index) {
341 return session ? &session->rom :
nullptr;
346 return session ? &session->game_data :
nullptr;
351 return session ? &session->editors :
nullptr;
370 const std::string& filepath,
371 std::optional<size_t> excluded_session_id)
const {
372 if (filepath.empty()) {
373 return absl::OkStatus();
377 if (!session || !session->rom.is_loaded() ||
378 (excluded_session_id.has_value() &&
379 session->session_id() == *excluded_session_id)) {
383 const std::string rom_filepath = session->rom.filename();
386 const std::string& owner_path =
387 session->filepath.empty() ? rom_filepath : session->filepath;
388 return absl::AlreadyExistsError(absl::StrFormat(
389 "ROM backing file '%s' is already open in session %zu ('%s')",
390 filepath, session->session_id(), owner_path));
393 return absl::OkStatus();
397 const std::string& filepath)
const {
408 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
409 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
410 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
420 for (
size_t i = 0; i <
sessions_.size(); ++i) {
423 ImGui::PushID(
static_cast<int>(i));
435 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
436 ImGui::OpenPopup(
"SessionContextMenu");
439 if (ImGui::BeginPopup(
"SessionContextMenu")) {
450 if (ImGui::Button(absl::StrFormat(
"%s New Session",
ICON_MD_ADD).c_str())) {
462 ImGui::Button(absl::StrFormat(
"%s Close",
ICON_MD_CLOSE).c_str())) {
476 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
477 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
478 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
496 if (ImGui::BeginTable(
"SessionTable", 4,
497 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
498 ImGuiTableFlags_Resizable)) {
499 ImGui::TableSetupColumn(
"Session", ImGuiTableColumnFlags_WidthStretch,
501 ImGui::TableSetupColumn(
"ROM File", ImGuiTableColumnFlags_WidthStretch,
503 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
504 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
506 ImGui::TableHeadersRow();
508 for (
size_t i = 0; i <
sessions_.size(); ++i) {
512 ImGui::PushID(
static_cast<int>(i));
514 ImGui::TableNextRow();
517 ImGui::TableNextColumn();
528 ImGui::TableNextColumn();
529 if (session->rom.is_loaded()) {
530 ImGui::Text(
"%s", session->filepath.c_str());
532 ImGui::TextDisabled(tr(
"(No ROM loaded)"));
536 ImGui::TableNextColumn();
539 }
else if (session->rom.is_loaded()) {
546 ImGui::TableNextColumn();
547 if (!is_active && ImGui::SmallButton(tr(
"Switch"))) {
577 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
578 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
579 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
592 if (ImGui::Button(tr(
"OK"))) {
599 if (ImGui::Button(tr(
"Cancel"))) {
612 for (
size_t i = 0; i <
sessions_.size(); ++i) {
617 if (session->rom.is_loaded()) {
625 if (ImGui::BeginTabItem(tab_name.c_str())) {
637 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
638 ImGui::OpenPopup(absl::StrFormat(
"SessionTabContext_%zu", i).c_str());
641 if (ImGui::BeginPopup(
642 absl::StrFormat(
"SessionTabContext_%zu", i).c_str())) {
663 if (ImGui::IsItemHovered()) {
664 ImGui::SetTooltip(tr(
"Active Session: %s\nClick to open session switcher"),
668 if (ImGui::IsItemClicked()) {
675 return "Invalid Session";
680 if (!session->custom_name.empty()) {
681 return session->custom_name;
684 if (session->rom.is_loaded()) {
685 return absl::StrFormat(
686 "Session %zu (%s)", index,
687 std::filesystem::path(session->filepath).stem().string());
690 return absl::StrFormat(
"Session %zu (Empty)", index);
698 const std::string& new_name) {
702 sessions_[index]->custom_name = new_name;
703 LOG_INFO(
"SessionCoordinator",
"Renamed session %zu to '%s'", index,
708 const std::string& editor_name,
size_t session_index)
const {
719 const auto& session =
sessions_[session_index];
720 std::string session_name = session->custom_name.empty()
721 ? session->rom.title()
722 : session->custom_name;
725 if (session_name.length() > 20) {
726 session_name = session_name.substr(0, 17) +
"...";
729 return absl::StrFormat(
"%s - %s##session_%zu", editor_name, session_name,
778 for (
size_t session_idx = 0; session_idx <
sessions_.size(); ++session_idx) {
790 for (
auto editor : session->editors.active_editors_) {
791 if (*editor->active()) {
799 if (overworld_editor.jump_to_tab() != -1) {
801 session->editors.GetDungeonEditor()->add_room(
802 overworld_editor.jump_to_tab());
808 bool is_card_based_editor =
811 if (is_card_based_editor) {
818 absl::Status status = editor->Update();
822 std::string editor_name =
825 absl::StrFormat(
"%s Error: %s", editor_name, status.message()),
832 kEditorNames[
static_cast<int>(editor->type())], session_idx);
835 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
836 ImGuiCond_FirstUseEver);
837 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
838 ImGuiCond_FirstUseEver);
840 if (ImGui::Begin(window_title.c_str(), editor->active(),
841 ImGuiWindowFlags_None)) {
848 absl::Status status = editor->Update();
852 std::string editor_name =
887 if (session->rom.is_loaded()) {
899 size_t session_index) {
900 if (filename.empty()) {
901 return absl::InvalidArgumentError(
"Invalid parameters");
904 size_t target_index =
907 return absl::InvalidArgumentError(
"Invalid session index");
911 LOG_INFO(
"SessionCoordinator",
"LoadRomIntoSession: %s -> session %zu",
912 filename.c_str(), target_index);
914 return absl::OkStatus();
918 const std::string& filename) {
920 return absl::FailedPreconditionError(
"No active session");
924 LOG_INFO(
"SessionCoordinator",
"SaveActiveSession: session %zu",
927 return absl::OkStatus();
931 const std::string& filename) {
933 return absl::InvalidArgumentError(
"Invalid parameters");
937 LOG_INFO(
"SessionCoordinator",
"SaveSessionAs: session %zu -> %s",
938 session_index, filename.c_str());
940 return absl::OkStatus();
944 Rom&& rom,
const std::string& filepath) {
946 if (!path_status.ok()) {
951 const size_t new_session_index =
sessions_.size();
952 sessions_.push_back(std::make_unique<RomSession>(
955 session->filepath = filepath;
975 return session.get();
981 [session_id](
const std::unique_ptr<RomSession>& session) {
982 return session && session->session_id() == session_id;
985 return absl::NotFoundError(
986 absl::StrFormat(
"Session %zu is no longer available", session_id));
990 static_cast<size_t>(std::distance(
sessions_.begin(), session_it));
1009 if (closing_active_session) {
1017 "Discarded provisional session %zu after failed open", session_id);
1018 return absl::OkStatus();
1023 size_t loaded_count = 0;
1025 if (session->rom.is_loaded()) {
1030 if (loaded_count > 0) {
1032 if (!(*it)->rom.is_loaded() &&
sessions_.size() > 1) {
1041 LOG_INFO(
"SessionCoordinator",
"Cleaned up closed sessions (remaining: %zu)",
1060 LOG_INFO(
"SessionCoordinator",
"Cleared all sessions");
1100 throw std::out_of_range(
1101 absl::StrFormat(
"Invalid session index: %zu", index));
1106 const std::string& base_name)
const {
1110 std::string name = base_name;
1116 if (session->custom_name == name) {
1125 name = absl::StrFormat(
"%s %d", base_name, counter++);
1134 absl::StrFormat(
"Maximum %zu sessions allowed",
kMaxSessions),
1140 const std::string& operation,
bool success) {
1142 std::string message =
1143 absl::StrFormat(
"%s %s", operation, success ?
"succeeded" :
"failed");
1159 if (session->rom.is_loaded()) {
1167 if (ImGui::BeginTabItem(tab_name.c_str())) {
1175 ImGui::EndTabItem();
1180 if (ImGui::MenuItem(
1181 absl::StrFormat(
"%s Switch to Session",
ICON_MD_TAB).c_str())) {
1189 if (ImGui::MenuItem(absl::StrFormat(
"%s Rename",
ICON_MD_EDIT).c_str())) {
1197 if (ImGui::MenuItem(
1206 absl::StrFormat(
"%s Close Session",
ICON_MD_CLOSE).c_str())) {
1224 if (session->rom.is_loaded()) {
1233 static const ImVec4 colors[] = {
1234 ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
1235 ImVec4(0.0f, 0.5f, 1.0f, 1.0f),
1236 ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1237 ImVec4(1.0f, 0.0f, 1.0f, 1.0f),
1238 ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
1239 ImVec4(0.0f, 1.0f, 1.0f, 1.0f),
1240 ImVec4(1.0f, 0.0f, 0.0f, 1.0f),
1241 ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
1244 return colors[index % (
sizeof(colors) /
sizeof(colors[0]))];
1253 if (session->rom.is_loaded()) {
1274 if (session->rom.is_loaded() && session->rom.dirty()) {
1282 if (
auto* dungeon_editor =
1284 return dungeon_editor->HasPendingRoomChanges();
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)