diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index 1c94ba44..2127d8fe 100644 --- a/src/olympic/studio/applib/src/CMakeLists.txt +++ b/src/olympic/studio/applib/src/CMakeLists.txt @@ -5,6 +5,7 @@ add_library( deleteconfirmation.cpp filedialogmanager.cpp main.cpp + newdir.cpp newmenu.cpp newproject.cpp projectexplorer.cpp diff --git a/src/olympic/studio/applib/src/deleteconfirmation.cpp b/src/olympic/studio/applib/src/deleteconfirmation.cpp index 9eaa591c..9ef0e063 100644 --- a/src/olympic/studio/applib/src/deleteconfirmation.cpp +++ b/src/olympic/studio/applib/src/deleteconfirmation.cpp @@ -43,7 +43,7 @@ void DeleteConfirmation::draw(StudioContext &ctx) noexcept { case Stage::Open: drawWindow(ctx.tctx, m_open, [this] { ImGui::Text("Are you sure you want to delete %s?", m_path.c_str()); - if (ig::PopupControlsOkCancel(m_open) != ig::PopupResponse::None) { + if (ig::PopupControlsOkCancel(m_open, "Yes", "No") != ig::PopupResponse::None) { deleteFile.emit(m_path); close(); } diff --git a/src/olympic/studio/applib/src/newdir.cpp b/src/olympic/studio/applib/src/newdir.cpp new file mode 100644 index 00000000..9f2c8709 --- /dev/null +++ b/src/olympic/studio/applib/src/newdir.cpp @@ -0,0 +1,59 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include "newdir.hpp" + +namespace studio { + +NewDir::NewDir() noexcept { + setTitle("New Directory"); + setSize({280, 0}); +} + +void NewDir::openPath(ox::StringViewCR path) noexcept { + open(); + m_path = path; +} + +void NewDir::open() noexcept { + m_path = ""; + m_stage = Stage::Opening; +} + +void NewDir::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool NewDir::isOpen() const noexcept { + return m_open; +} + +void NewDir::draw(StudioContext &ctx) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(title().c_str()); + m_open = true; + [[fallthrough]]; + case Stage::Open: + drawWindow(ctx.tctx, m_open, [this] { + if (m_stage == Stage::Opening) { + ImGui::SetKeyboardFocusHere(); + } + ig::InputText("Name", m_str); + if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) { + newDir.emit(m_path + "/" + m_str); + close(); + } + }); + m_stage = Stage::Open; + break; + } +} + +} diff --git a/src/olympic/studio/applib/src/newdir.hpp b/src/olympic/studio/applib/src/newdir.hpp new file mode 100644 index 00000000..b79ef0b9 --- /dev/null +++ b/src/olympic/studio/applib/src/newdir.hpp @@ -0,0 +1,49 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +namespace studio { + +class NewDir final: public Popup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_path; + ox::IString<255> m_str; + + public: + ox::Signal newDir; + + NewDir() noexcept; + + void openPath(ox::StringViewCR path) noexcept; + + void open() noexcept override; + + void close() noexcept override; + + [[nodiscard]] + bool isOpen() const noexcept override; + + void draw(StudioContext &ctx) noexcept override; + + [[nodiscard]] + constexpr ox::CStringView value() const noexcept { + return m_str; + } + +}; + +} diff --git a/src/olympic/studio/applib/src/projecttreemodel.cpp b/src/olympic/studio/applib/src/projecttreemodel.cpp index aab3604f..51e945ef 100644 --- a/src/olympic/studio/applib/src/projecttreemodel.cpp +++ b/src/olympic/studio/applib/src/projecttreemodel.cpp @@ -27,13 +27,13 @@ ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept: m_children(std::move(other.m_children)) { } -void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept { +void ProjectTreeModel::draw(turbine::Context &tctx) const noexcept { constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (!m_children.empty()) { if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) { drawDirContextMenu(); for (auto const&child : m_children) { - child->draw(ctx); + child->draw(tctx); } ImGui::TreePop(); } else { @@ -54,6 +54,9 @@ void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept { ImGui::EndPopup(); } ImGui::TreePop(); + ig::dragDropSource([this] { + ImGui::Text("%s", m_name.c_str()); + }); } } } @@ -70,13 +73,16 @@ void ProjectTreeModel::drawDirContextMenu() const noexcept { if (ImGui::MenuItem("Add Directory")) { m_explorer.addDir.emit(fullPath()); } + if (ImGui::MenuItem("Delete")) { + m_explorer.deleteItem.emit(fullPath()); + } ImGui::EndPopup(); } } ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept { if (m_parent) { - return m_parent->fullPath() + "/" + ox::StringView(m_name); + return m_parent->fullPath() + "/" + m_name; } return {}; } diff --git a/src/olympic/studio/applib/src/projecttreemodel.hpp b/src/olympic/studio/applib/src/projecttreemodel.hpp index f8b1bee3..e7060d4e 100644 --- a/src/olympic/studio/applib/src/projecttreemodel.hpp +++ b/src/olympic/studio/applib/src/projecttreemodel.hpp @@ -25,7 +25,7 @@ class ProjectTreeModel { ProjectTreeModel(ProjectTreeModel &&other) noexcept; - void draw(turbine::Context &ctx) const noexcept; + void draw(turbine::Context &tctx) const noexcept; void setChildren(ox::Vector> children) noexcept; diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 6265f863..a9358b33 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -52,6 +52,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_aboutPopup(m_tctx) { turbine::setApplicationData(m_tctx, &m_sctx); m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile); + m_projectExplorer.addDir.connect(this, &StudioUI::addDir); m_projectExplorer.addItem.connect(this, &StudioUI::addFile); m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); m_newProject.finished.connect(this, &StudioUI::createOpenProject); @@ -344,6 +345,11 @@ void StudioUI::handleKeyInput() noexcept { } } +ox::Error StudioUI::addDir(ox::StringViewCR path) noexcept { + m_newDirDialog.openPath(path); + return {}; +} + ox::Error StudioUI::addFile(ox::StringViewCR path) noexcept { m_newMenu.openPath(path); return {}; @@ -369,8 +375,10 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { ox::make_unique_catch(keelCtx(m_tctx), std::move(path), m_projectDataDir) .moveTo(m_project)); m_sctx.project = m_project.get(); - m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); + m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); + m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); + m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_openFiles.clear(); diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index a0373624..199feac2 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -15,6 +15,7 @@ #include "aboutpopup.hpp" #include "deleteconfirmation.hpp" +#include "newdir.hpp" #include "newmenu.hpp" #include "newproject.hpp" #include "projectexplorer.hpp" @@ -41,13 +42,15 @@ class StudioUI: public ox::SignalHandler { BaseEditor *m_activeEditorUpdatePending = nullptr; NewMenu m_newMenu; DeleteConfirmation m_deleteConfirmation; + NewDir m_newDirDialog; NewProject m_newProject; AboutPopup m_aboutPopup; - ox::Array const m_popups = { + ox::Array const m_popups = { &m_newMenu, &m_newProject, &m_aboutPopup, &m_deleteConfirmation, + &m_newDirDialog, }; bool m_showProjectExplorer = true; @@ -87,6 +90,8 @@ class StudioUI: public ox::SignalHandler { void handleKeyInput() noexcept; + ox::Error addDir(ox::StringViewCR path) noexcept; + ox::Error addFile(ox::StringViewCR path) noexcept; ox::Error deleteFile(ox::StringViewCR path) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 01cd3e84..c82f32e8 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -175,9 +175,16 @@ enum class PopupResponse { Cancel, }; -PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen); +PopupResponse PopupControlsOkCancel( + float popupWidth, + bool &popupOpen, + ox::CStringViewCR ok = "OK", + ox::CStringViewCR cancel = "Cancel"); -PopupResponse PopupControlsOkCancel(bool &popupOpen); +PopupResponse PopupControlsOkCancel( + bool &popupOpen, + ox::CStringViewCR ok = "OK", + ox::CStringViewCR cancel = "Cancel"); [[nodiscard]] bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index 8c8221b4..8ee319fe 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -19,6 +19,7 @@ namespace studio { enum class ProjectEvent { None, + DirAdded, FileAdded, // FileRecognized is triggered for all matching files upon a new // subscription to a section of the project and upon the addition of a file. @@ -120,6 +121,7 @@ class Project: public ox::SignalHandler { // signals public: ox::Signal fileEvent; + ox::Signal dirAdded; ox::Signal fileAdded; // FileRecognized is triggered for all matching files upon a new // subscription to a section of the project and upon the addition of a @@ -177,6 +179,9 @@ ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&s switch (e) { case ProjectEvent::None: break; + case ProjectEvent::DirAdded: + connect(this, &Project::dirAdded, tgt, slot); + break; case ProjectEvent::FileAdded: connect(this, &Project::fileAdded, tgt, slot); break; diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 7618a2aa..b6d8fb2a 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -54,18 +54,22 @@ bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept { return ImGui::Button(lbl.c_str(), btnSz); } -PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) { +PopupResponse PopupControlsOkCancel( + float popupWidth, + bool &popupOpen, + ox::CStringViewCR ok, + ox::CStringViewCR cancel) { auto out = PopupResponse::None; constexpr auto btnSz = ImVec2{50, BtnSz.y}; ImGui::Separator(); ImGui::SetCursorPosX(popupWidth - 118); - if (ImGui::Button("OK", btnSz)) { + if (ImGui::Button(ok.c_str(), btnSz)) { ImGui::CloseCurrentPopup(); popupOpen = false; out = PopupResponse::OK; } ImGui::SameLine(); - if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button("Cancel", btnSz)) { + if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button(cancel.c_str(), btnSz)) { ImGui::CloseCurrentPopup(); popupOpen = false; out = PopupResponse::Cancel; @@ -73,8 +77,11 @@ PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) { return out; } -PopupResponse PopupControlsOkCancel(bool &popupOpen) { - return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen); +PopupResponse PopupControlsOkCancel( + bool &popupOpen, + ox::CStringViewCR ok, + ox::CStringViewCR cancel) { + return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen, ok, cancel); } bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) { diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 2777483a..cd90d8a0 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -59,10 +59,10 @@ ox::Error Project::mkdir(ox::StringViewCR path) const noexcept { auto const [stat, err] = m_fs.stat(path); if (err) { OX_RETURN_ERROR(m_fs.mkdir(path, true)); - fileUpdated.emit(path, {}); + dirAdded.emit(path); } return stat.fileType == ox::FileType::Directory ? - ox::Error{} : ox::Error(1, "path exists as normal file"); + ox::Error{} : ox::Error{1, "path exists as normal file"}; } ox::Result Project::stat(ox::StringViewCR path) const noexcept { @@ -70,11 +70,28 @@ ox::Result Project::stat(ox::StringViewCR path) const noexcept { } ox::Error Project::deleteItem(ox::StringViewCR path) noexcept { - auto const err = m_fs.remove(path); - if (!err) { - fileDeleted.emit(path); + OX_REQUIRE(stat, m_fs.stat(path)); + if (stat.fileType == ox::FileType::Directory) { + bool partialRemoval{}; + OX_REQUIRE(members, m_fs.ls(path)); + for (auto const&p : members) { + partialRemoval = m_fs.remove(ox::sfmt("{}/{}", path, p)) || partialRemoval; + } + if (partialRemoval) { + return ox::Error{1, "failed to remove one or more directory members"}; + } + auto const err = m_fs.remove(path); + if (!err) { + fileDeleted.emit(path); + } + return err; + } else { + auto const err = m_fs.remove(path); + if (!err) { + fileDeleted.emit(path); + } + return err; } - return err; } bool Project::exists(ox::StringViewCR path) const noexcept {