diff --git a/src/olympic/keel/include/keel/assetmanager.hpp b/src/olympic/keel/include/keel/assetmanager.hpp index 4c8ae38d..0a1525e2 100644 --- a/src/olympic/keel/include/keel/assetmanager.hpp +++ b/src/olympic/keel/include/keel/assetmanager.hpp @@ -9,7 +9,6 @@ #endif #include -#include #include #include #include @@ -190,6 +189,8 @@ class AssetManager { public: ~AssetTypeManagerBase() override = default; + virtual ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept = 0; + virtual void gc() noexcept = 0; }; @@ -202,7 +203,7 @@ class AssetManager { ox::HashMap>> m_cache; public: - AssetTypeManager(Loader &&loader) noexcept: m_loader(std::move(loader)) {} + explicit AssetTypeManager(Loader &&loader) noexcept: m_loader(std::move(loader)) {} ox::Result> getAsset(ox::StringViewCR assetId) const noexcept { OX_REQUIRE(out, m_cache.at(assetId)); @@ -236,6 +237,14 @@ class AssetManager { return {}; } + ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept final { + auto &o = m_cache[oldId]; + auto &n = m_cache[newId]; + n = std::move(o); + m_cache.erase(oldId); + return {}; + } + void gc() noexcept final { for (auto const&ack : m_cache.keys()) { auto &ac = m_cache[ack]; @@ -244,10 +253,11 @@ class AssetManager { } } } + }; ox::HashMap> m_assetTypeManagers; - ox::HashMap> m_fileUpdated; + ox::HashMap>> m_fileUpdated; template ox::Result*> getTypeManager() noexcept { @@ -279,7 +289,7 @@ class AssetManager { } ox::Error reloadAsset(ox::StringViewCR assetId) noexcept { - m_fileUpdated[assetId].emit(assetId); + m_fileUpdated[assetId]->emit(assetId); return {}; } @@ -287,15 +297,40 @@ class AssetManager { ox::Result> loadAsset(ox::StringViewCR assetId) noexcept { OX_REQUIRE(m, getTypeManager()); OX_REQUIRE(out, m->loadAsset(assetId)); - m_fileUpdated[assetId].connect(m, &AssetTypeManager::reloadAsset); + if (!m_fileUpdated.contains(assetId)) [[unlikely]] { + m_fileUpdated[assetId] = ox::make_unique>(); + } + m_fileUpdated[assetId]->connect(m, &AssetTypeManager::reloadAsset); return out; } + ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept { + gc(); + if (m_fileUpdated.contains(newId)) { + return ox::Error{1, "new asset ID already has an entry"}; + } + auto &o = m_fileUpdated[oldId]; + auto &n = m_fileUpdated[newId]; + n = std::move(o); + m_fileUpdated.erase(oldId); + for (auto &k : m_assetTypeManagers.keys()) { + auto const &tm = m_assetTypeManagers[k]; + std::ignore = tm->updateAssetId(oldId, newId); + } + return {}; + } + void gc() noexcept { for (auto const&amk : m_assetTypeManagers.keys()) { auto &am = m_assetTypeManagers[amk]; am->gc(); } + for (auto const&k : m_fileUpdated.keys()) { + auto &s = m_fileUpdated[k]; + if (s->connectionCnt() == 0) { + m_fileUpdated.erase(k); + } + } } }; #else diff --git a/src/olympic/keel/include/keel/media.hpp b/src/olympic/keel/include/keel/media.hpp index 228bab75..7faa272e 100644 --- a/src/olympic/keel/include/keel/media.hpp +++ b/src/olympic/keel/include/keel/media.hpp @@ -47,6 +47,8 @@ ox::Result getPath(Context &ctx, ox::FileAddress const&fileAddr ox::Result getPath(Context &ctx, ox::CStringViewCR fileId) noexcept; +ox::Error updatePath(Context &ctx, ox::StringViewCR oldPath, ox::StringViewCR newPath) noexcept; + constexpr ox::Result uuidUrlToUuid(ox::StringViewCR uuidUrl) noexcept { return ox::UUID::fromString(substr(uuidUrl, 7)); } diff --git a/src/olympic/keel/src/media.cpp b/src/olympic/keel/src/media.cpp index ea59e415..01e6f90d 100644 --- a/src/olympic/keel/src/media.cpp +++ b/src/olympic/keel/src/media.cpp @@ -129,6 +129,21 @@ ox::Result getPath(Context &ctx, ox::CStringViewCR fileId) noex } } +ox::Error updatePath(Context &ctx, ox::StringViewCR oldPath, ox::StringViewCR newPath) noexcept { +#ifndef OX_BARE_METAL + if (auto const r = ctx.pathToUuid.at(oldPath); r.ok()) { + auto const ustr = r.value->toString(); + ctx.pathToUuid[newPath] = *r.value; + ctx.pathToUuid.erase(oldPath); + ctx.uuidToPath[ustr] = newPath; + return {}; + } + return ctx.assetManager.updateAssetId(oldPath, newPath); +#else + return ox::Error(1, "updating path is not supported on this platform"); +#endif +} + ox::Result uuidUrlToPath(Context &ctx, ox::StringViewCR uuid) noexcept { #ifndef OX_BARE_METAL OX_REQUIRE_M(out, ctx.uuidToPath.at(substr(uuid, 7))); diff --git a/src/olympic/keel/src/pack.cpp b/src/olympic/keel/src/pack.cpp index 301ffe49..1b88bdb4 100644 --- a/src/olympic/keel/src/pack.cpp +++ b/src/olympic/keel/src/pack.cpp @@ -39,10 +39,11 @@ static ox::Error pathToInode( auto const uuid = ox::substr(path, 7); OX_RETURN_ERROR(keel::uuidToPath(ctx, uuid).to().moveTo(path)); } - OX_REQUIRE(s, dest.stat(path)); + auto const s = dest.stat(path); + auto const inode = s.ok() ? s.value.inode : 0; OX_RETURN_ERROR(typeVal->set(static_cast(ox::FileAddressType::Inode))); - oxOutf("\tpath to inode: {} => {}\n", path, s.inode); - return data.set(2, s.inode); + oxOutf("\tpath to inode: {} => {}\n", path, inode); + return data.set(2, inode); } static ox::Error transformFileAddressesObj( diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index f69e669b..59f238e2 100644 --- a/src/olympic/studio/applib/src/CMakeLists.txt +++ b/src/olympic/studio/applib/src/CMakeLists.txt @@ -9,6 +9,7 @@ add_library( newmenu.cpp newproject.cpp projectexplorer.cpp + renamefile.cpp studioapp.cpp ) target_compile_definitions( diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index 286ab4d3..fd1fdcc6 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -31,6 +31,9 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::MenuItem("Delete")) { deleteItem.emit(path); } + if (ImGui::MenuItem("Rename")) { + renameItem.emit(path); + } ImGui::EndPopup(); } } diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index 400155c7..8abe6e2d 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -19,6 +19,7 @@ class ProjectExplorer final: public FileExplorer { ox::Signal addItem; ox::Signal addDir; ox::Signal deleteItem; + ox::Signal renameItem; explicit ProjectExplorer(keel::Context &kctx) noexcept; diff --git a/src/olympic/studio/applib/src/renamefile.cpp b/src/olympic/studio/applib/src/renamefile.cpp new file mode 100644 index 00000000..b4c633c7 --- /dev/null +++ b/src/olympic/studio/applib/src/renamefile.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include "renamefile.hpp" + +namespace studio { + +RenameFile::RenameFile() { + setTitle("Rename File"); +} + +ox::Error RenameFile::openPath(ox::StringParam path) noexcept { + m_oldPath = std::move(path); + OX_REQUIRE(idx, ox::findIdx(m_oldPath.rbegin(), m_oldPath.rend(), '/')); + m_name = substr(m_oldPath, idx + 1); + m_path = substr(m_oldPath, 0, idx + 1); + open(); + return {}; +} + +void RenameFile::open() noexcept { + m_stage = Stage::Opening; +} + +void RenameFile::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool RenameFile::isOpen() const noexcept { + return m_open; +} + +void RenameFile::draw(StudioContext &ctx) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(title().c_str()); + m_open = true; + m_stage = Stage::Open; + [[fallthrough]]; + case Stage::Open: + setSize({250, 0}); + drawWindow(ctx.tctx, m_open, [this] { + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + ig::InputText("Name", m_name); + ImGui::Text("%s%s", m_path.c_str(), m_name.c_str()); + if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) { + moveFile.emit(m_oldPath, m_path + m_name); + close(); + } + }); + break; + } +} + +} \ No newline at end of file diff --git a/src/olympic/studio/applib/src/renamefile.hpp b/src/olympic/studio/applib/src/renamefile.hpp new file mode 100644 index 00000000..2af0326d --- /dev/null +++ b/src/olympic/studio/applib/src/renamefile.hpp @@ -0,0 +1,44 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include +#include + +namespace studio { + +class RenameFile: public Popup { + + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage{}; + ox::String m_oldPath; + ox::String m_path; + ox::IString<255> m_name; + bool m_open{}; + + public: + ox::Signal moveFile; + + RenameFile(); + + ox::Error openPath(ox::StringParam path) noexcept; + + void open() noexcept override; + + void close() noexcept override; + + [[nodiscard]] + bool isOpen() const noexcept override; + + void draw(StudioContext &ctx) noexcept override; + +}; + +} diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 05d4542d..01244728 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -47,20 +47,20 @@ OX_MODEL_BEGIN(StudioConfig) OX_MODEL_END() StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept: - m_sctx(*this, ctx), - m_tctx(ctx), - m_projectDataDir(std::move(projectDataDir)), - m_projectExplorer(keelCtx(m_tctx)), - m_newProject(m_projectDataDir), - m_aboutPopup(m_tctx) { + m_sctx{*this, ctx}, + m_tctx{ctx}, + m_projectDataDir{std::move(projectDataDir)}, + m_projectExplorer{keelCtx(m_tctx)}, + m_newProject{m_projectDataDir}, + 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_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); m_newProject.finished.connect(this, &StudioUI::createOpenProject); m_newMenu.finished.connect(this, &StudioUI::openFile); - ImGui::GetIO().IniFilename = nullptr; loadModules(); // open project and files auto const [config, err] = studio::readConfig(keelCtx(m_tctx)); @@ -369,6 +369,14 @@ ox::Error StudioUI::deleteFile(ox::StringViewCR path) noexcept { return {}; } +ox::Error StudioUI::renameFile(ox::StringViewCR path) noexcept { + return m_renameFile.openPath(path); +} + +ox::Error StudioUI::handleMoveFile(ox::StringViewCR, ox::UUID const&) noexcept { + return m_projectExplorer.refreshProjectTreeModel(); +} + ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept { std::error_code ec; std::filesystem::create_directories(toStdStringView(path), ec); @@ -386,10 +394,12 @@ 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_renameFile.moveFile.connect(m_project.get(), &Project::moveItem); 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_project->fileMoved.connect(this, &StudioUI::handleMoveFile); m_openFiles.clear(); m_editors.clear(); studio::editConfig(keelCtx(m_tctx), [&](StudioConfig &config) { diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 6eee9f4f..39a3e30c 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -19,6 +19,7 @@ #include "newmenu.hpp" #include "newproject.hpp" #include "projectexplorer.hpp" +#include "renamefile.hpp" namespace studio { @@ -42,14 +43,16 @@ class StudioUI: public ox::SignalHandler { NewMenu m_newMenu{keelCtx(m_tctx)}; DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; + RenameFile m_renameFile; 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, + &m_renameFile, }; bool m_showProjectExplorer = true; @@ -95,6 +98,10 @@ class StudioUI: public ox::SignalHandler { ox::Error deleteFile(ox::StringViewCR path) noexcept; + ox::Error renameFile(ox::StringViewCR path) noexcept; + + ox::Error handleMoveFile(ox::StringViewCR path, ox::UUID const&id) noexcept; + ox::Error createOpenProject(ox::StringViewCR path) noexcept; ox::Error openProjectPath(ox::StringParam path) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index 8ee319fe..dc8a221a 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -26,6 +25,7 @@ enum class ProjectEvent { FileRecognized, FileDeleted, FileUpdated, + FileMoved, }; [[nodiscard]] @@ -49,7 +49,7 @@ constexpr ox::StringView parentDir(ox::StringView path) noexcept { class Project: public ox::SignalHandler { private: ox::SmallMap> m_typeFmt; - keel::Context &m_ctx; + keel::Context &m_kctx; ox::String m_path; ox::String m_projectDataDir; ox::String m_typeDescPath; @@ -92,6 +92,8 @@ class Project: public ox::SignalHandler { ox::Result stat(ox::StringViewCR path) const noexcept; + ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; + ox::Error deleteItem(ox::StringViewCR path) noexcept; [[nodiscard]] @@ -114,7 +116,7 @@ class Project: public ox::SignalHandler { ox::Result loadBuff(ox::StringViewCR path) const noexcept; - ox::Error lsProcDir(ox::Vector *paths, ox::StringViewCR path) const noexcept; + ox::Error lsProcDir(ox::Vector &paths, ox::StringViewCR path) const noexcept; ox::Result> listFiles(ox::StringViewCR path = "") const noexcept; @@ -128,7 +130,8 @@ class Project: public ox::SignalHandler { // file. ox::Signal fileRecognized; ox::Signal fileDeleted; - ox::Signal fileUpdated; + ox::Signal fileUpdated; + ox::Signal fileMoved; }; @@ -151,8 +154,8 @@ ox::Error Project::writeObj(ox::StringViewCR path, T const&obj, ox::ClawFormat f if (!descExists) { OX_RETURN_ERROR(writeTypeStore()); } - OX_RETURN_ERROR(keel::reloadAsset(m_ctx, path)); - OX_REQUIRE(uuid, pathToUuid(m_ctx, path)); + OX_RETURN_ERROR(keel::reloadAsset(m_kctx, path)); + OX_REQUIRE(uuid, pathToUuid(m_kctx, path)); fileUpdated.emit(path, uuid); return {}; } @@ -200,6 +203,9 @@ ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&s case ProjectEvent::FileUpdated: connect(this, &Project::fileUpdated, tgt, slot); break; + case ProjectEvent::FileMoved: + connect(this, &Project::fileMoved, tgt, slot); + break; } return {}; } diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 9548975e..4ec709a9 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -26,13 +26,14 @@ static void generateTypes(ox::TypeStore &ts) noexcept { } } + Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir): - m_ctx(ctx), + m_kctx(ctx), m_path(std::move(path)), m_projectDataDir(projectDataDir), m_typeDescPath(ox::sfmt("/{}/type_descriptors", m_projectDataDir)), - m_typeStore(*m_ctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)), - m_fs(*m_ctx.rom) { + m_typeStore(*m_kctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)), + m_fs(*m_kctx.rom) { oxTracef("studio", "Project: {}", m_path); generateTypes(m_typeStore); if constexpr(ox::defines::Debug) { @@ -69,6 +70,14 @@ ox::Result Project::stat(ox::StringViewCR path) const noexcept { return m_fs.stat(path); } +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)); + OX_REQUIRE(uuid, keel::pathToUuid(m_kctx, dest)); + fileMoved.emit(dest, uuid); + return {}; +} + ox::Error Project::deleteItem(ox::StringViewCR path) noexcept { OX_REQUIRE(stat, m_fs.stat(path)); if (stat.fileType == ox::FileType::Directory) { @@ -142,7 +151,7 @@ ox::Error Project::writeBuff(ox::StringViewCR path, ox::BufferView const&buff) n ox::Buffer outBuff; outBuff.reserve(buff.size() + HdrSz); ox::BufferWriter writer(&outBuff); - auto const [uuid, err] = pathToUuid(m_ctx, path); + auto const [uuid, err] = pathToUuid(m_kctx, path); if (!err) { OX_RETURN_ERROR(keel::writeUuidHeader(writer, uuid)); } @@ -162,14 +171,14 @@ ox::Result Project::loadBuff(ox::StringViewCR path) const noexcept { return m_fs.read(path); } -ox::Error Project::lsProcDir(ox::Vector *paths, ox::StringViewCR path) const noexcept { +ox::Error Project::lsProcDir(ox::Vector &paths, ox::StringViewCR path) const noexcept { OX_REQUIRE(files, m_fs.ls(path)); for (auto const&name : files) { auto fullPath = ox::sfmt("{}/{}", path, name); OX_REQUIRE(stat, m_fs.stat(ox::StringView(fullPath))); switch (stat.fileType) { case ox::FileType::NormalFile: - paths->emplace_back(std::move(fullPath)); + paths.emplace_back(std::move(fullPath)); break; case ox::FileType::Directory: OX_RETURN_ERROR(lsProcDir(paths, fullPath)); @@ -183,7 +192,7 @@ ox::Error Project::lsProcDir(ox::Vector *paths, ox::StringViewCR pat ox::Result> Project::listFiles(ox::StringViewCR path) const noexcept { ox::Vector paths; - OX_RETURN_ERROR(lsProcDir(&paths, path)); + OX_RETURN_ERROR(lsProcDir(paths, path)); return paths; } diff --git a/src/olympic/turbine/src/glfw/gfx.cpp b/src/olympic/turbine/src/glfw/gfx.cpp index 058cc496..40f3b103 100644 --- a/src/olympic/turbine/src/glfw/gfx.cpp +++ b/src/olympic/turbine/src/glfw/gfx.cpp @@ -222,6 +222,7 @@ ox::Error initGfx(Context &ctx) noexcept { //io.MouseDrawCursor = true; ImGui_ImplGlfw_InitForOpenGL(ctx.window, true); ImGui_ImplOpenGL3_Init(); + io.IniFilename = nullptr; themeImgui(); #endif return {};