From 671dd86206db857581d88a8e8a143848169379ee Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 02:01:40 -0600 Subject: [PATCH] [keel,studio] Add Make Copy option to ProjectExplorer --- src/olympic/keel/include/keel/asset.hpp | 2 + src/olympic/keel/src/asset.cpp | 16 ++++- src/olympic/studio/applib/src/CMakeLists.txt | 1 + .../studio/applib/src/makecopypopup.cpp | 70 +++++++++++++++++++ .../studio/applib/src/makecopypopup.hpp | 42 +++++++++++ .../studio/applib/src/projectexplorer.cpp | 3 + .../studio/applib/src/projectexplorer.hpp | 1 + src/olympic/studio/applib/src/studioapp.cpp | 7 ++ src/olympic/studio/applib/src/studioapp.hpp | 4 ++ .../studio/modlib/include/studio/project.hpp | 2 + src/olympic/studio/modlib/src/project.cpp | 8 +++ 11 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/olympic/studio/applib/src/makecopypopup.cpp create mode 100644 src/olympic/studio/applib/src/makecopypopup.hpp diff --git a/src/olympic/keel/include/keel/asset.hpp b/src/olympic/keel/include/keel/asset.hpp index 35d53e61..5b25a2af 100644 --- a/src/olympic/keel/include/keel/asset.hpp +++ b/src/olympic/keel/include/keel/asset.hpp @@ -15,6 +15,8 @@ constexpr auto K1HdrSz = 40; ox::Result readUuidHeader(ox::BufferView buff) noexcept; +ox::Result regenerateUuidHeader(ox::Buffer &buff) noexcept; + ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept { OX_RETURN_ERROR(write(writer, "K1;")); OX_RETURN_ERROR(uuid.toString(writer)); diff --git a/src/olympic/keel/src/asset.cpp b/src/olympic/keel/src/asset.cpp index d4d907ef..b7b9023f 100644 --- a/src/olympic/keel/src/asset.cpp +++ b/src/olympic/keel/src/asset.cpp @@ -8,15 +8,25 @@ namespace keel { ox::Result readUuidHeader(ox::BufferView buff) noexcept { if (buff.size() < K1HdrSz) [[unlikely]] { - return ox::Error(1, "Insufficient data to contain complete Keel header"); + return ox::Error{1, "Insufficient data to contain complete Keel header"}; } constexpr ox::StringView k1Hdr = "K1;"; - if (k1Hdr != ox::StringView(buff.data(), k1Hdr.bytes())) [[unlikely]] { - return ox::Error(2, "No Keel asset header data"); + if (k1Hdr != ox::StringView{buff.data(), k1Hdr.bytes()}) [[unlikely]] { + return ox::Error{2, "No Keel asset header data"}; } return ox::UUID::fromString(ox::StringView(&buff[k1Hdr.bytes()], 36)); } +ox::Result regenerateUuidHeader(ox::Buffer &buff) noexcept { + OX_RETURN_ERROR(readUuidHeader(buff)); + OX_REQUIRE(id, ox::UUID::generate()); + auto const str = id.toString(); + for (size_t i = 0; i < ox::UUIDStr::cap(); ++i) { + buff[i + 3] = str[i]; + } + return id; +} + ox::Result readAsset(ox::TypeStore &ts, ox::BufferView buff) noexcept { std::size_t offset = 0; if (!readUuidHeader(buff).error) { diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index 59f238e2..773f36cf 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 + makecopypopup.cpp newdir.cpp newmenu.cpp newproject.cpp diff --git a/src/olympic/studio/applib/src/makecopypopup.cpp b/src/olympic/studio/applib/src/makecopypopup.cpp new file mode 100644 index 00000000..db6c4f3b --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.cpp @@ -0,0 +1,70 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include "makecopypopup.hpp" + +namespace studio { + +ox::Error MakeCopyPopup::open(ox::StringViewCR path) noexcept { + m_stage = Stage::Opening; + OX_REQUIRE(idx, ox::findIdx(path.rbegin(), path.rend(), '/')); + m_srcPath = path; + m_dirPath = substr(path, 0, idx + 1); + m_title = sfmt("Copy {}", path); + m_fileName = ""; + return {}; +} + +void MakeCopyPopup::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool MakeCopyPopup::isOpen() const noexcept { + return m_open; +} + +void MakeCopyPopup::draw(StudioContext const &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: + ig::centerNextWindow(ctx.tctx); + ImGui::SetNextWindowSize(sz); + constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + ig::InputText("Name", m_fileName); + bool open = true; + switch (ig::PopupControlsOkCancel(open)) { + case ig::PopupResponse::None: + break; + case ig::PopupResponse::OK: + { + auto const p = sfmt("{}{}", m_dirPath, m_fileName); + if (!ctx.project->exists(p)) { + makeCopy.emit(m_srcPath, p); + close(); + } + } + break; + case ig::PopupResponse::Cancel: + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + + +} \ No newline at end of file diff --git a/src/olympic/studio/applib/src/makecopypopup.hpp b/src/olympic/studio/applib/src/makecopypopup.hpp new file mode 100644 index 00000000..10313506 --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.hpp @@ -0,0 +1,42 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +namespace studio { + +class MakeCopyPopup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_title{"Copy File"}; + ox::String m_srcPath; + ox::String m_dirPath; + ox::IString<255> m_fileName; + + public: + ox::Signal makeCopy; + + ox::Error open(ox::StringViewCR path) noexcept; + + void close() noexcept; + + [[nodiscard]] + bool isOpen() const noexcept; + + void draw(StudioContext const &ctx, ImVec2 const &sz = {}) noexcept; + +}; + +} diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index 20ba0dfa..d3b374db 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -42,6 +42,9 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::MenuItem("Rename")) { renameItem.emit(path); } + if (ImGui::MenuItem("Make Copy")) { + makeCopy.emit(path); + } ImGui::EndPopup(); } } diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index c0b4420a..74bcaef2 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer { ox::Signal addDir; ox::Signal deleteItem; ox::Signal renameItem; + ox::Signal makeCopy; ox::Signal moveDir; ox::Signal moveItem; diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 944939de..37ef95d6 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -59,6 +59,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_projectExplorer.addItem.connect(this, &StudioUI::addFile); m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); + m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg); m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove); m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove); m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); @@ -136,6 +137,7 @@ void StudioUI::draw() noexcept { p->draw(m_sctx); } m_closeFileConfirm.draw(m_sctx); + m_copyFilePopup.draw(m_sctx, {250, 0}); } ImGui::End(); handleKeyInput(); @@ -451,6 +453,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { m_sctx.project = m_project.get(); 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); m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); @@ -513,6 +516,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi return {}; } +ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept { + return m_copyFilePopup.open(path); +} + ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { if (response == ig::PopupResponse::OK && m_activeEditor) { return closeCurrentFile(); diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 0ced3dca..6809102a 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -16,6 +16,7 @@ #include "aboutpopup.hpp" #include "deleteconfirmation.hpp" +#include "makecopypopup.hpp" #include "newdir.hpp" #include "newmenu.hpp" #include "newproject.hpp" @@ -48,6 +49,7 @@ class StudioUI: public ox::SignalHandler { DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; + MakeCopyPopup m_copyFilePopup; RenameFile m_renameFile; NewProject m_newProject; AboutPopup m_aboutPopup; @@ -117,6 +119,8 @@ class StudioUI: public ox::SignalHandler { ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; + ox::Error makeCopyDlg(ox::StringViewCR path) noexcept; + ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; ox::Error closeCurrentFile() noexcept; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index 76489c68..73b0e2b7 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -92,6 +92,8 @@ class Project: public ox::SignalHandler { ox::Result stat(ox::StringViewCR path) const noexcept; + ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; + ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept; diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index dbf34dbb..c4cde1cc 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -97,6 +97,14 @@ ox::Result Project::stat(ox::StringViewCR path) const noexcept { return m_fs.stat(path); } +ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { + OX_REQUIRE_M(buff, loadBuff(src)); + OX_REQUIRE(id, keel::regenerateUuidHeader(buff)); + OX_RETURN_ERROR(writeBuff(dest, buff)); + createUuidMapping(m_kctx, dest, id); + return {}; +} + ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { OX_RETURN_ERROR(m_fs.move(src, dest)); OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest));