From ff1e8f260bfaa1369217b49622eb5269cf765dde Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 6 May 2025 22:22:26 -0500 Subject: [PATCH] [studio] Add popup to warn about UUID duplication --- src/olympic/studio/applib/src/studioui.cpp | 72 ++++++++++++++----- src/olympic/studio/applib/src/studioui.hpp | 4 +- .../modlib/include/studio/imguiutil.hpp | 49 +++++++++++-- src/olympic/studio/modlib/src/imguiutil.cpp | 61 ++++++++++++++-- 4 files changed, 152 insertions(+), 34 deletions(-) diff --git a/src/olympic/studio/applib/src/studioui.cpp b/src/olympic/studio/applib/src/studioui.cpp index 48c2f86e..c82ae85f 100644 --- a/src/olympic/studio/applib/src/studioui.cpp +++ b/src/olympic/studio/applib/src/studioui.cpp @@ -17,6 +17,12 @@ #include "font.hpp" #include "studioui.hpp" +#ifdef OX_OS_Darwin +#define STUDIO_CTRL "Cmd" +#else +#define STUDIO_CTRL "Ctrl" +#endif + namespace studio { static bool shutdownHandler(turbine::Context &ctx) { @@ -174,45 +180,64 @@ bool StudioUI::handleShutdown() noexcept { void StudioUI::drawMenu() noexcept { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New...", "Ctrl+N", false, m_project)) { + if (ImGui::MenuItem("New...", STUDIO_CTRL "+N", false, m_project)) { m_newMenu.open(); } - if (ImGui::MenuItem("New Project...", "Ctrl+Shift+N")) { + if (ImGui::MenuItem("New Project...", STUDIO_CTRL "+Shift+N")) { m_newProject.open(); } - if (ImGui::MenuItem("Open Project...", "Ctrl+O")) { + if (ImGui::MenuItem("Open Project...", STUDIO_CTRL "+O")) { m_taskRunner.add(*ox::make(this, &StudioUI::openProjectPath)); } - if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) { + if (ImGui::MenuItem( + "Save", + STUDIO_CTRL "+S", + false, + m_activeEditor && m_activeEditor->unsavedChanges())) { m_activeEditor->save(); } - if (ImGui::MenuItem("Quit", "Ctrl+Q")) { + if (ImGui::MenuItem("Quit", STUDIO_CTRL "+Q")) { turbine::requestShutdown(m_tctx); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; - if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) { + if (ImGui::MenuItem( + "Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) { oxLogError(undoStack->undo()); } - if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) { + if (ImGui::MenuItem( + "Redo", STUDIO_CTRL "+Y", false, undoStack && undoStack->canRedo())) { oxLogError(undoStack->redo()); } ImGui::Separator(); - if (ImGui::MenuItem("Copy", "Ctrl+C", false, m_activeEditor && m_activeEditor->copyEnabled())) { + if (ImGui::MenuItem( + "Copy", + STUDIO_CTRL "+C", + false, + m_activeEditor && m_activeEditor->copyEnabled())) { m_activeEditor->copy(); } - if (ImGui::MenuItem("Cut", "Ctrl+X", false, m_activeEditor && m_activeEditor->cutEnabled())) { + if (ImGui::MenuItem( + "Cut", + STUDIO_CTRL "+X", + false, + m_activeEditor && m_activeEditor->cutEnabled())) { m_activeEditor->cut(); } - if (ImGui::MenuItem("Paste", "Ctrl+V", false, m_activeEditor && m_activeEditor->pasteEnabled())) { + if (ImGui::MenuItem( + "Paste", + STUDIO_CTRL "+V", + false, + m_activeEditor && m_activeEditor->pasteEnabled())) { m_activeEditor->paste(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { - if (ImGui::MenuItem("Project Explorer", "Ctrl+Shift+1", m_showProjectExplorer)) { + if (ImGui::MenuItem( + "Project Explorer", STUDIO_CTRL "+Shift+1", m_showProjectExplorer)) { toggleProjectExplorer(); } ImGui::EndMenu(); @@ -278,12 +303,8 @@ void StudioUI::drawTabs() noexcept { if (m_activeEditor == (*it).get()) { m_activeEditor = nullptr; } - try { - OX_THROW_ERROR(m_editors.erase(it).moveTo(it)); - } catch (ox::Exception const&ex) { - oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.src.file_name(), ex.src.line()); - } catch (std::exception const&ex) { - oxErrf("Editor tab deletion failed: {}\n", ex.what()); + if (auto const err = m_editors.erase(it).moveTo(it)) { + oxErrf("Editor tab deletion failed: {} ({}:{})\n", toStr(err), err.src.file_name(), err.src.line()); } } } else { @@ -478,12 +499,25 @@ ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept { ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { OX_REQUIRE_M(fs, keel::loadRomFs(path.view())); - OX_RETURN_ERROR(keel::setRomFs(keelCtx(m_tctx), std::move(fs))); + keel::DuplicateSet ds; + OX_RETURN_ERROR(keel::setRomFs(keelCtx(m_tctx), std::move(fs), ds)); + if (ds.size()) { + ox::String msg; + msg += "Multiple files have the same UUID:\n"; + for (auto const &k : ds.keys()) { + msg += ox::sfmt("\n\t{}:\n", k.toString()); + for (auto const &v : ds[k]) { + msg += ox::sfmt("\t\t - {}\n", v); + } + } + m_messagePopup.show(msg); + } OX_RETURN_ERROR( ox::make_unique_catch(keelCtx(m_tctx), std::move(path), m_projectDataDir) .moveTo(m_project)); m_sctx.project = m_project.get(); - turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); + turbine::setWindowTitle( + m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem); m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); diff --git a/src/olympic/studio/applib/src/studioui.hpp b/src/olympic/studio/applib/src/studioui.hpp index 18a6c092..e2fe5c98 100644 --- a/src/olympic/studio/applib/src/studioui.hpp +++ b/src/olympic/studio/applib/src/studioui.hpp @@ -52,11 +52,12 @@ class StudioUI: public ox::SignalHandler { "Close Application?", "There are files with unsaved changes. Close?" }; + ig::MessagePopup m_messagePopup{"Message", ""}; MakeCopyPopup m_copyFilePopup; RenameFile m_renameFile; NewProject m_newProject; AboutPopup m_aboutPopup; - ox::Array const m_widgets { + ox::Array const m_widgets { &m_closeFileConfirm, &m_closeAppConfirm, &m_copyFilePopup, @@ -66,6 +67,7 @@ class StudioUI: public ox::SignalHandler { &m_deleteConfirmation, &m_newDirDialog, &m_renameFile, + &m_messagePopup, }; bool m_showProjectExplorer = true; struct NavAction { diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 5519bb6f..1b38936a 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -169,7 +169,9 @@ TextInput> InputText( out.changed = ImGui::InputText( label.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data); if (out.changed) { + OX_ALLOW_UNSAFE_BUFFERS_BEGIN std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str())); + OX_ALLOW_UNSAFE_BUFFERS_END } return out; } @@ -186,7 +188,9 @@ TextInput> InputTextWithHint( out.changed = ImGui::InputTextWithHint( label.c_str(), hint.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data); if (out.changed) { + OX_ALLOW_UNSAFE_BUFFERS_BEGIN std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str())); + OX_ALLOW_UNSAFE_BUFFERS_END } return out; } @@ -201,7 +205,9 @@ bool InputText( auto const out = ImGui::InputText( label.c_str(), text.data(), StrCap + 1, flags, callback, user_data); if (out) { + OX_ALLOW_UNSAFE_BUFFERS_BEGIN std::ignore = text.unsafeResize(ox::strlen(text.c_str())); + OX_ALLOW_UNSAFE_BUFFERS_END } return out; } @@ -223,6 +229,10 @@ PopupResponse PopupControlsOkCancel( ox::CStringViewCR ok = "OK", ox::CStringViewCR cancel = "Cancel"); +PopupResponse PopupControlsOk( + bool &popupOpen, + ox::CStringViewCR ok); + [[nodiscard]] bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); @@ -250,7 +260,7 @@ bool ComboBox(ox::CStringView lbl, ox::Span list, size_t &sele /** * * @param lbl - * @param callback + * @param f callback function * @param selectedIdx * @return true if new value selected, false otherwise */ @@ -285,7 +295,7 @@ bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t class FilePicker { private: bool m_show{}; - studio::StudioContext &m_sctx; + StudioContext &m_sctx; ox::String const m_title; ox::String const m_fileExt; ImVec2 const m_size; @@ -304,8 +314,8 @@ class FilePicker { }; -class QuestionPopup: public Widget { - private: +class Popup: public Widget { + protected: enum class Stage { Closed, Opening, @@ -314,12 +324,11 @@ class QuestionPopup: public Widget { Stage m_stage = Stage::Closed; bool m_open{}; ox::String m_title; - ox::String m_question; public: ox::Signal response; - QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; + explicit Popup(ox::StringParam title) noexcept; void open() noexcept; @@ -328,7 +337,33 @@ class QuestionPopup: public Widget { [[nodiscard]] bool isOpen() const noexcept; - void draw(StudioContext &ctx) noexcept; +}; + +class QuestionPopup: public Popup { + private: + ox::String m_question; + + public: + ox::Signal response; + + QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; + + void draw(StudioContext &ctx) noexcept override; + +}; + +class MessagePopup: public Popup { + private: + ox::String m_msg; + + public: + ox::Signal response; + + MessagePopup(ox::StringParam title, ox::StringParam msg) noexcept; + + void show(ox::StringParam msg) noexcept; + + void draw(StudioContext &ctx) noexcept override; }; diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index a22e7c26..53c60309 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -88,7 +88,7 @@ PopupResponse PopupControlsOk( auto out = PopupResponse::None; constexpr auto btnSz = ImVec2{50, BtnSz.y}; ImGui::Separator(); - ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - 101); + ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - 42); if (ImGui::Button(ok.c_str(), btnSz)) { popupOpen = false; out = PopupResponse::OK; @@ -245,24 +245,28 @@ void FilePicker::show() noexcept { } -QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept: - m_title{std::move(title)}, - m_question{std::move(question)} { +Popup::Popup(ox::StringParam title) noexcept: m_title{std::move(title)} { } -void QuestionPopup::open() noexcept { +void Popup::open() noexcept { m_stage = Stage::Opening; } -void QuestionPopup::close() noexcept { +void Popup::close() noexcept { m_stage = Stage::Closed; m_open = false; } -bool QuestionPopup::isOpen() const noexcept { +bool Popup::isOpen() const noexcept { return m_open; } + +QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept: + Popup{std::move(title)}, + m_question{std::move(question)} { +} + void QuestionPopup::draw(StudioContext &ctx) noexcept { switch (m_stage) { case Stage::Closed: @@ -298,6 +302,49 @@ void QuestionPopup::draw(StudioContext &ctx) noexcept { } +MessagePopup::MessagePopup(ox::StringParam title, ox::StringParam msg) noexcept: + Popup{std::move(title)}, + m_msg{std::move(msg)} { +} + +void MessagePopup::show(ox::StringParam msg) noexcept { + m_msg = std::move(msg); + open(); +} + +void MessagePopup::draw(StudioContext &ctx) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(m_title.c_str()); + m_stage = Stage::Open; + m_open = true; + [[fallthrough]]; + case Stage::Open: + centerNextWindow(ctx.tctx); + ImGui::SetNextWindowSize({}); + constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { + ImGui::Text("%s", m_msg.c_str()); + auto const r = PopupControlsOk(m_open, "OK"); + switch (r) { + case PopupResponse::None: + break; + case PopupResponse::OK: + response.emit(r); + close(); + break; + case PopupResponse::Cancel: + break; + } + ImGui::EndPopup(); + } + break; + } +} + + bool s_mainWinHasFocus{}; bool mainWinHasFocus() noexcept { return s_mainWinHasFocus;