diff --git a/src/olympic/studio/applib/src/studioui.cpp b/src/olympic/studio/applib/src/studioui.cpp index b3716859..88027414 100644 --- a/src/olympic/studio/applib/src/studioui.cpp +++ b/src/olympic/studio/applib/src/studioui.cpp @@ -34,18 +34,6 @@ static bool shutdownHandler(turbine::Context &ctx) { return sctx->ui.handleShutdown(); } -void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept { - ox::String path = std::move(filePath); - if (beginsWith(path, "uuid://")) { - auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path); - if (err) { - return; - } - path = p; - } - ctx.ui.navigateTo(std::move(path), std::move(navArgs)); -} - namespace ig { extern bool s_mainWinHasFocus; } @@ -147,6 +135,9 @@ StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexc oxLogError(headerizeConfigFile(kctx)); turbine::setApplicationData(m_tctx, &m_sctx); turbine::setShutdownHandler(m_tctx, shutdownHandler); + m_sctx.navCallback = [this](ox::StringParam filePath, ox::StringParam navArgs) { + handleNavigationChange(std::move(filePath), std::move(navArgs)); + }; m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile); m_projectExplorer.addDir.connect(this, &StudioUI::addDir); m_projectExplorer.addItem.connect(this, &StudioUI::addFile); @@ -186,7 +177,7 @@ void StudioUI::handleKeyEvent(turbine::Key const key, bool const down) noexcept } } -void StudioUI::navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept { +void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept { m_navAction.emplace(std::move(path), std::move(navArgs)); } @@ -270,7 +261,7 @@ void StudioUI::drawMenu() noexcept { ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { - auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; + auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; if (ImGui::MenuItem( "Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) { oxLogError(undoStack->undo()); @@ -310,6 +301,17 @@ void StudioUI::drawMenu() noexcept { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Navigate")) { + if (ImGui::MenuItem("Back", STUDIO_CTRL "+{", false, m_sctx.navIdx > 1)) { + navigateBack(m_sctx); + } + if (ImGui::MenuItem( + "Forward", STUDIO_CTRL "+}", false, + m_sctx.navIdx < m_sctx.navStack.size())) { + navigateForward(m_sctx); + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) { m_aboutPopup.open(); @@ -353,6 +355,13 @@ void StudioUI::drawTabs() noexcept { } if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] { m_activeEditor->onActivated(); + if (!m_sctx.navIdx || + (m_sctx.navIdx <= m_sctx.navStack.size() && + m_sctx.navStack[m_sctx.navIdx - 1].filePath != m_activeEditor->itemPath())) { + m_sctx.navStack.resize(m_sctx.navIdx); + ++m_sctx.navIdx; + m_sctx.navStack.emplace_back(ox::String{m_activeEditor->itemPath()}, ox::String{""}); + } } if (m_closeActiveTab) [[unlikely]] { ImGui::SetTabItemClosed(e->itemDisplayName().c_str()); @@ -506,6 +515,22 @@ void StudioUI::handleKeyInput() noexcept { } } } + if constexpr (ox::defines::OS == ox::OS::Darwin) { + if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) { + navigateBack(m_sctx); + } else if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) { + navigateForward(m_sctx); + } + } + } + if constexpr (ox::defines::OS != ox::OS::Darwin) { + if (ImGui::IsKeyDown(ImGuiKey_ModAlt)) { + if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) { + navigateBack(m_sctx); + } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) { + navigateForward(m_sctx); + } + } } } diff --git a/src/olympic/studio/applib/src/studioui.hpp b/src/olympic/studio/applib/src/studioui.hpp index 863de46a..ed4c0f9f 100644 --- a/src/olympic/studio/applib/src/studioui.hpp +++ b/src/olympic/studio/applib/src/studioui.hpp @@ -87,7 +87,7 @@ class StudioUI final: public ox::SignalHandler { void handleKeyEvent(turbine::Key, bool down) noexcept; - void navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept; + void handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept; [[nodiscard]] constexpr Project *project() noexcept { diff --git a/src/olympic/studio/modlib/include/studio/context.hpp b/src/olympic/studio/modlib/include/studio/context.hpp index e25e543e..3efccceb 100644 --- a/src/olympic/studio/modlib/include/studio/context.hpp +++ b/src/olympic/studio/modlib/include/studio/context.hpp @@ -16,11 +16,21 @@ class StudioUI; struct Context { public: + friend void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept; + friend void navigateBack(Context &ctx) noexcept; + friend void navigateForward(Context &ctx) noexcept; friend StudioUI; StudioUI &ui; Project *project = nullptr; turbine::Context &tctx; protected: + struct NavPath { + ox::String filePath; + ox::String navArgs; + }; + size_t navIdx{}; + ox::Vector navStack; + std::function navCallback; Context(StudioUI &pUi, turbine::Context &pTctx) noexcept: ui{pUi}, tctx{pTctx} {} }; @@ -37,4 +47,8 @@ inline keel::Context const &keelCtx(Context const &ctx) noexcept { void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs = "") noexcept; +void navigateBack(Context &ctx) noexcept; + +void navigateForward(Context &ctx) noexcept; + } diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index e8d2bb25..dd0b6203 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -46,14 +46,14 @@ ox::Result getDragDropPayload() noexcept { static_cast(payload->DataSize)}); } -ox::Error setDragDropPayload(ox::CStringViewCR name, auto const&obj) noexcept { +ox::Error setDragDropPayload(ox::CStringViewCR name, auto const &obj) noexcept { OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); ImGui::SetDragDropPayload(name.c_str(), buff.data(), buff.size()); return {}; } template -ox::Error setDragDropPayload(T const&obj) noexcept { +ox::Error setDragDropPayload(T const &obj) noexcept { OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); ImGui::SetDragDropPayload(ox::ModelTypeName_v, buff.data(), buff.size()); return {}; @@ -77,7 +77,7 @@ class DragDropSource { } }; -auto dragDropSource(auto const&cb, ImGuiDragDropFlags const flags = 0) noexcept { +auto dragDropSource(auto const &cb, ImGuiDragDropFlags const flags = 0) noexcept { if constexpr(ox::is_same_v) { if (ig::DragDropSource const tgt{flags}; tgt) [[unlikely]] { return cb(); @@ -108,7 +108,7 @@ class DragDropTarget { } }; -auto dragDropTarget(auto const&cb) noexcept { +auto dragDropTarget(auto const &cb) noexcept { if constexpr(ox::is_same_v) { if (ig::DragDropTarget const tgt; tgt) [[unlikely]] { return cb(); @@ -124,7 +124,7 @@ auto dragDropTarget(auto const&cb) noexcept { class ChildStackItem { public: - explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz = {}) noexcept; + explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz = {}) noexcept; ~ChildStackItem() noexcept; }; @@ -146,7 +146,7 @@ class IndentStackItem { void centerNextWindow(turbine::Context &ctx) noexcept; -bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz = BtnSz) noexcept; +bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz = BtnSz) noexcept; template struct TextInput { @@ -234,7 +234,7 @@ PopupResponse PopupControlsOk( ox::CStringViewCR ok); [[nodiscard]] -bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); +bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz = {285, 0}); /** * @@ -266,7 +266,7 @@ bool ComboBox(ox::CStringView lbl, ox::Span list, size_t &sele */ bool ComboBox( ox::CStringViewCR lbl, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selectedIdx) noexcept; @@ -278,10 +278,10 @@ bool FileComboBox( bool ListBox( ox::CStringViewCR name, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selIdx, - ImVec2 const&sz = {0, 0}) noexcept; + ImVec2 const &sz = {0, 0}) noexcept; /** * @@ -290,7 +290,7 @@ bool ListBox( * @param selIdx * @return true if new value selected, false otherwise */ -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept; +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept; class FilePicker { private: @@ -306,7 +306,7 @@ class FilePicker { studio::Context &sctx, ox::StringParam title, ox::StringParam fileExt, - ImVec2 const&size = {}) noexcept; + ImVec2 const &size = {}) noexcept; void draw() noexcept; @@ -348,6 +348,8 @@ class QuestionPopup: public Popup { QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; + void setQuestion(ox::StringParam msg) noexcept; + void draw(Context &ctx) noexcept override; }; diff --git a/src/olympic/studio/modlib/src/CMakeLists.txt b/src/olympic/studio/modlib/src/CMakeLists.txt index 28bc2748..6adb19bb 100644 --- a/src/olympic/studio/modlib/src/CMakeLists.txt +++ b/src/olympic/studio/modlib/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_library( Studio configio.cpp + context.cpp editor.cpp filepickerpopup.cpp filetreemodel.cpp diff --git a/src/olympic/studio/modlib/src/context.cpp b/src/olympic/studio/modlib/src/context.cpp new file mode 100644 index 00000000..042f880b --- /dev/null +++ b/src/olympic/studio/modlib/src/context.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +namespace studio { + +void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept { + ox::String path = std::move(filePath); + if (beginsWith(path, "uuid://")) { + auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path); + if (err) { + return; + } + path = p; + } + ctx.navStack.resize(ctx.navIdx + 1); + ctx.navStack.emplace_back(ox::String{path}, ox::String{navArgs.view()}); + ctx.navCallback(std::move(path), std::move(navArgs)); +} + +void navigateBack(Context &ctx) noexcept { + if (!ctx.navIdx) { + return; + } + --ctx.navIdx; + if (ctx.navIdx < ctx.navStack.size() && ctx.navIdx) { + auto const &n = ctx.navStack[ctx.navIdx - 1]; + try { + ctx.navCallback(n.filePath, n.navArgs); + } catch (std::exception const &e) { + oxAssert(ctx.navCallback != nullptr, "navCallback is null"); + oxErrf("navigateForward failed: {}", e.what()); + } + } +} + +void navigateForward(Context &ctx) noexcept { + if (ctx.navIdx < ctx.navStack.size()) { + auto const &n = ctx.navStack[ctx.navIdx]; + try { + ctx.navCallback(n.filePath, n.navArgs); + } catch (std::exception const &e) { + oxAssert(ctx.navCallback != nullptr, "navCallback is null"); + oxErrf("navigateForward failed: {}", e.what()); + } + ++ctx.navIdx; + } +} + +} \ No newline at end of file diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index e4886d9a..2fc75859 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -10,7 +10,7 @@ namespace studio::ig { -ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz) noexcept { +ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz) noexcept { ImGui::BeginChild(id.c_str(), sz); } @@ -50,7 +50,7 @@ void centerNextWindow(turbine::Context &ctx) noexcept { ImGui::SetNextWindowPos(ImVec2(screenW / mod, screenH / mod), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); } -bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept { +bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz) noexcept { return ImGui::Button(lbl.c_str(), btnSz); } @@ -101,7 +101,7 @@ PopupResponse PopupControlsOk( return out; } -bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) { +bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz) { constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; centerNextWindow(ctx); ImGui::OpenPopup(popupName.c_str()); @@ -149,7 +149,7 @@ bool ComboBox( bool ComboBox( ox::CStringViewCR lbl, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selectedIdx) noexcept { bool out{}; @@ -172,16 +172,16 @@ bool FileComboBox( Context &sctx, ox::StringViewCR fileExt, size_t &selectedIdx) noexcept { - auto const&list = sctx.project->fileList(fileExt); + auto const &list = sctx.project->fileList(fileExt); return ComboBox(lbl, list, selectedIdx); } bool ListBox( ox::CStringViewCR name, - std::function const&f, + std::function const &f, size_t const strCnt, size_t &selIdx, - ImVec2 const&sz) noexcept { + ImVec2 const &sz) noexcept { auto out = false; if (ImGui::BeginListBox(name.c_str(), sz)) { for (size_t i = 0; i < strCnt; ++i) { @@ -199,13 +199,13 @@ bool ListBox( return out; } -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept { +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept { return ListBox(name, [list](size_t i) -> ox::CStringView { return list[i]; }, list.size(), selIdx); } -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept { +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept { return ListBox(name, [list](size_t i) -> ox::CStringView { return list[i]; }, list.size(), selIdx); @@ -216,7 +216,7 @@ FilePicker::FilePicker( Context &sctx, ox::StringParam title, ox::StringParam fileExt, - ImVec2 const&size) noexcept: + ImVec2 const &size) noexcept: m_sctx(sctx), m_title(std::move(title)), m_fileExt(std::move(fileExt)), @@ -230,7 +230,7 @@ void FilePicker::draw() noexcept { auto constexpr popupSz = ImVec2{450.f, 0}; IDStackItem const idStackItem(m_title); if (BeginPopup(m_sctx.tctx, m_title, m_show, popupSz)) { - auto const&list = m_sctx.project->fileList(m_fileExt); + auto const &list = m_sctx.project->fileList(m_fileExt); size_t selIdx{}; ComboBox(m_title, list, selIdx); if (PopupControlsOkCancel(popupSz.x, m_show) == ig::PopupResponse::OK) { @@ -267,6 +267,10 @@ QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) no m_question{std::move(question)} { } +void QuestionPopup::setQuestion(ox::StringParam msg) noexcept { + m_question = std::move(msg); +} + void QuestionPopup::draw(Context &ctx) noexcept { switch (m_stage) { case Stage::Closed: