diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 3cbe35a9..61a06f12 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -64,6 +64,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); m_newProject.finished.connect(this, &StudioUI::createOpenProject); m_newMenu.finished.connect(this, &StudioUI::openFile); + m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse); loadModules(); // open project and files auto const [config, err] = studio::readConfig(keelCtx(m_tctx)); @@ -134,6 +135,7 @@ void StudioUI::draw() noexcept { for (auto const p : m_popups) { p->draw(m_sctx); } + m_closeFileConfirm.draw(m_sctx); } ImGui::End(); handleKeyInput(); @@ -214,7 +216,7 @@ void StudioUI::drawTabs() noexcept { auto open = true; auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0; auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0; - auto const flags = unsavedChanges | selected; + auto const flags = unsavedChanges | selected | ImGuiTabItemFlags_NoAssumedClosure; if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) { if (m_activeEditor != e.get()) [[unlikely]] { m_activeEditor = e.get(); @@ -237,16 +239,20 @@ void StudioUI::drawTabs() noexcept { ImGui::EndTabItem(); } if (!open) { - e->close(); - 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 (e->unsavedChanges()) { + m_closeFileConfirm.open(); + } else { + e->close(); + 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()); + } } } else { ++it; @@ -483,6 +489,20 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi return {}; } +ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { + if (response == ig::PopupResponse::OK && m_activeEditor) { + for (size_t i{}; auto &e : m_editors) { + if (m_activeEditor == e.get()) { + oxLogError(closeFile(e->itemPath())); + oxLogError(m_editors.erase(i).error); + break; + } + ++i; + } + } + return {}; +} + ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept { if (!m_openFiles.contains(path)) { return {}; diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 6dfc730f..9c413e1c 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -45,6 +46,7 @@ class StudioUI: public ox::SignalHandler { NewMenu m_newMenu{keelCtx(m_tctx)}; DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; + ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; RenameFile m_renameFile; NewProject m_newProject; AboutPopup m_aboutPopup; @@ -114,6 +116,8 @@ class StudioUI: public ox::SignalHandler { ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; + ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; + ox::Error closeFile(ox::StringViewCR path) noexcept; ox::Error queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 32855a63..542879e9 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -303,6 +303,34 @@ class FilePicker { }; +class QuestionPopup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + 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; + + void open() noexcept; + + void close() noexcept; + + [[nodiscard]] + bool isOpen() const noexcept; + + void draw(StudioContext &ctx, ImVec2 const &sz = {}) noexcept; + +}; + [[nodiscard]] bool mainWinHasFocus() noexcept; diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 4935dc8e..8bab9d2a 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -225,6 +225,59 @@ void FilePicker::show() noexcept { m_show = true; } + +QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept: + m_title{std::move(title)}, + m_question{std::move(question)} { +} + +void QuestionPopup::open() noexcept { + m_stage = Stage::Opening; +} + +void QuestionPopup::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool QuestionPopup::isOpen() const noexcept { + return m_open; +} + +void QuestionPopup::draw(StudioContext &ctx, ImVec2 const &sz) 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(static_cast(sz)); + constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { + ImGui::Text("%s", m_question.c_str()); + auto const r = PopupControlsOkCancel(m_open, "Yes", "No"); + response.emit(r); + switch (r) { + case PopupResponse::None: + break; + case PopupResponse::OK: + close(); + break; + case PopupResponse::Cancel: + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + + bool s_mainWinHasFocus{}; bool mainWinHasFocus() noexcept { return s_mainWinHasFocus;