From 7b7d59cf63d77cf7ab6daf6ed7122eef97954555 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 19 Jan 2025 13:31:44 -0600 Subject: [PATCH] Squashed 'deps/nostalgia/' changes from a3d6a58c..d68e6493 d68e6493 [nostalgia/core/studio/tilesheeteditor] Add support for dragging palette to palette selector 1cbc5762 [studio] Complete drag/drop support for files 500b9356 [studio] Make new dir window OK on Enter key 800ca851 [ox/std] Fix possible error that occurs with appending on boundary of small string size cc466a9f [studio] Add support for adding and deleting directories 9d115584 [nostalgia] Rename player from 'nostalgia' to 'Nostalgia' a2139c09 [studio] Cleanup unused member a3e5f27a [ox/std] Fix Mac build 643f95ec [studio] Add confirmation dialog for file deletion, move deletion to Project 69241476 [studio] Add ability to add file through dir context menu 6e2b4fa7 [nostalgia] Cleanup player run in Makefile 4e5c7499 [studio] Add support for deleting files 66229de7 [ox/fs] FileSystem fixes with removing files 7eb37c53 [nostalgia/core/studio/paletteeditor] Fix adding page if there is no existing page 7a21b207 [nostalgia/core] Replace ContextDeleter with safeDelete(Context*) 894be237 [ox/std] Drop ox:: qualifier from safeDelete function for pointee 92e9d9cb [keel,studio] Add support for New Item templates b29b9a9b [ox/std] Add UAnyPtr 721f8442 [nostalgia/core/studio/tilesheeteditor] Fix subsheet and palette scrolling git-subtree-dir: deps/nostalgia git-subtree-split: d68e64931b37d7d8bbaff7b43bf131c7acf2aa97 --- Makefile | 16 +- deps/ox/src/ox/fs/filesystem/filesystem.cpp | 12 - deps/ox/src/ox/fs/filesystem/filesystem.hpp | 50 ++-- .../ox/src/ox/fs/filesystem/passthroughfs.cpp | 16 +- .../ox/src/ox/fs/filesystem/passthroughfs.hpp | 4 +- deps/ox/src/ox/std/anyptr.hpp | 91 ++++++- deps/ox/src/ox/std/string.hpp | 8 +- .../core/include/nostalgia/core/context.hpp | 6 +- .../modules/core/src/gba/context.cpp | 4 +- .../modules/core/src/opengl/context.cpp | 4 +- .../src/studio/paletteeditor/CMakeLists.txt | 1 + .../paletteeditor/commands/addpagecommand.cpp | 28 +++ .../paletteeditor/commands/addpagecommand.hpp | 31 +++ .../paletteeditor/commands/commands.hpp | 1 + .../commands/duplicatepagecommand.cpp | 2 +- .../paletteeditor/paletteeditor-imgui.cpp | 7 +- .../tilesheeteditor/tilesheeteditor-imgui.cpp | 41 ++- src/nostalgia/player/CMakeLists.txt | 14 +- src/nostalgia/player/app.hpp | 8 - src/olympic/keel/include/keel/typeconv.hpp | 20 +- src/olympic/studio/applib/src/CMakeLists.txt | 2 + .../studio/applib/src/deleteconfirmation.cpp | 55 ++++ .../studio/applib/src/deleteconfirmation.hpp | 43 ++++ src/olympic/studio/applib/src/newdir.cpp | 63 +++++ src/olympic/studio/applib/src/newdir.hpp | 49 ++++ src/olympic/studio/applib/src/newmenu.cpp | 120 ++++++--- src/olympic/studio/applib/src/newmenu.hpp | 70 ++++-- src/olympic/studio/applib/src/newproject.cpp | 2 +- .../studio/applib/src/projectexplorer.cpp | 8 +- .../studio/applib/src/projectexplorer.hpp | 16 +- .../studio/applib/src/projecttreemodel.cpp | 45 +++- .../studio/applib/src/projecttreemodel.hpp | 19 +- src/olympic/studio/applib/src/studioapp.cpp | 28 ++- src/olympic/studio/applib/src/studioapp.hpp | 17 +- .../studio/modlib/include/studio/dragdrop.hpp | 22 ++ .../modlib/include/studio/imguiutil.hpp | 15 +- .../modlib/include/studio/itemmaker.hpp | 238 +++++++++++++++--- .../studio/modlib/include/studio/module.hpp | 2 + .../studio/modlib/include/studio/popup.hpp | 2 +- .../studio/modlib/include/studio/project.hpp | 9 +- .../studio/modlib/include/studio/studio.hpp | 1 + src/olympic/studio/modlib/src/imguiutil.cpp | 32 ++- src/olympic/studio/modlib/src/module.cpp | 8 +- src/olympic/studio/modlib/src/popup.cpp | 4 +- src/olympic/studio/modlib/src/project.cpp | 29 ++- 45 files changed, 1024 insertions(+), 239 deletions(-) create mode 100644 src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.cpp create mode 100644 src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.hpp delete mode 100644 src/nostalgia/player/app.hpp create mode 100644 src/olympic/studio/applib/src/deleteconfirmation.cpp create mode 100644 src/olympic/studio/applib/src/deleteconfirmation.hpp create mode 100644 src/olympic/studio/applib/src/newdir.cpp create mode 100644 src/olympic/studio/applib/src/newdir.hpp create mode 100644 src/olympic/studio/modlib/include/studio/dragdrop.hpp diff --git a/Makefile b/Makefile index 90c6529..5e5c49c 100644 --- a/Makefile +++ b/Makefile @@ -5,23 +5,27 @@ BUILDCORE_PATH=deps/buildcore include ${BUILDCORE_PATH}/base.mk ifeq ($(BC_VAR_OS),darwin) - NOSTALGIA_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio.app/Contents/MacOS/${BC_VAR_PROJECT_NAME_CAP}Studio + PROJECT_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio.app/Contents/MacOS/${BC_VAR_PROJECT_NAME_CAP}Studio MGBA=/Applications/mGBA.app/Contents/MacOS/mGBA else - NOSTALGIA_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio + PROJECT_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio MGBA=mgba-qt endif +PROJECT_PLAYER=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP} .PHONY: pkg-gba pkg-gba: build ${BC_CMD_ENVRUN} ${BC_PY3} ./util/scripts/pkg-gba.py sample_project ${BC_VAR_PROJECT_NAME} +.PHONY: build-player +build-player: + ${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} ${BC_VAR_PROJECT_NAME_CAP} .PHONY: run -run: build - ./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME} sample_project +run: build-player + ${PROJECT_PLAYER} sample_project .PHONY: run-studio run-studio: build - ${NOSTALGIA_STUDIO} + ${PROJECT_STUDIO} .PHONY: gba-run gba-run: pkg-gba ${MGBA} ${BC_VAR_PROJECT_NAME}.gba @@ -30,7 +34,7 @@ debug: build ${BC_CMD_HOST_DEBUGGER} ./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME} sample_project .PHONY: debug-studio debug-studio: build - ${BC_CMD_HOST_DEBUGGER} ${NOSTALGIA_STUDIO} + ${BC_CMD_HOST_DEBUGGER} ${PROJECT_STUDIO} .PHONY: configure-gba configure-gba: diff --git a/deps/ox/src/ox/fs/filesystem/filesystem.cpp b/deps/ox/src/ox/fs/filesystem/filesystem.cpp index 750e417..105e2b9 100644 --- a/deps/ox/src/ox/fs/filesystem/filesystem.cpp +++ b/deps/ox/src/ox/fs/filesystem/filesystem.cpp @@ -63,18 +63,6 @@ Error FileSystem::read(const FileAddress &addr, std::size_t readStart, std::size } } -Error FileSystem::remove(const FileAddress &addr, bool recursive) noexcept { - switch (addr.type()) { - case FileAddressType::Inode: - return remove(addr.getInode().value, recursive); - case FileAddressType::ConstPath: - case FileAddressType::Path: - return remove(StringView(addr.getPath().value), recursive); - default: - return ox::Error(1); - } -} - Error FileSystem::write(const FileAddress &addr, const void *buffer, uint64_t size, FileType fileType) noexcept { switch (addr.type()) { case FileAddressType::Inode: diff --git a/deps/ox/src/ox/fs/filesystem/filesystem.hpp b/deps/ox/src/ox/fs/filesystem/filesystem.hpp index e785e4b..ebc8321 100644 --- a/deps/ox/src/ox/fs/filesystem/filesystem.hpp +++ b/deps/ox/src/ox/fs/filesystem/filesystem.hpp @@ -57,9 +57,9 @@ class FileSystem { virtual Result> ls(StringViewCR dir) const noexcept = 0; - virtual Error remove(StringViewCR path, bool recursive) noexcept = 0; - - Error remove(const FileAddress &addr, bool recursive = false) noexcept; + Error remove(StringViewCR path, bool recursive = false) noexcept { + return removePath(path, recursive); + } virtual Error resize(uint64_t size, void *buffer) noexcept = 0; @@ -142,6 +142,8 @@ class FileSystem { virtual Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept = 0; + virtual Error removePath(StringViewCR path, bool recursive) noexcept = 0; + virtual Error writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType fileType) noexcept = 0; virtual Error writeFileInode(uint64_t inode, const void *buffer, uint64_t size, FileType fileType) noexcept = 0; @@ -209,6 +211,8 @@ class FileSystemTemplate: public MemFS { Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override; + Error removePath(StringViewCR path, bool recursive) noexcept override; + Result directAccessInode(uint64_t) const noexcept override; Result> ls(StringViewCR dir) const noexcept override; @@ -216,8 +220,6 @@ class FileSystemTemplate: public MemFS { template Error ls(StringViewCR path, F cb) const; - Error remove(StringViewCR path, bool recursive) noexcept override; - /** * Resizes FileSystem to minimum possible size. */ @@ -356,6 +358,25 @@ Error FileSystemTemplate::readFileInodeRange(uint64_t inod return m_fs.read(inode, readStart, readSize, reinterpret_cast(buffer), size); } +template +Error FileSystemTemplate::removePath(StringViewCR path, bool recursive) noexcept { + OX_REQUIRE(fd, fileSystemData()); + Directory rootDir(m_fs, fd.rootDirInode); + OX_REQUIRE(inode, rootDir.find(path)); + OX_REQUIRE(st, statInode(inode)); + if (st.fileType == FileType::NormalFile || recursive) { + if (auto err = rootDir.remove(path)) { + // removal failed, try putting the index back + oxLogError(rootDir.write(path, inode)); + return err; + } + } else { + oxTrace("FileSystemTemplate.remove.fail", "Tried to remove directory without recursive setting."); + return ox::Error(1); + } + return ox::Error(0); +} + template Result FileSystemTemplate::directAccessInode(uint64_t inode) const noexcept { auto data = m_fs.read(inode); @@ -384,25 +405,6 @@ Error FileSystemTemplate::ls(StringViewCR path, F cb) cons return dir.ls(cb); } -template -Error FileSystemTemplate::remove(StringViewCR path, bool recursive) noexcept { - OX_REQUIRE(fd, fileSystemData()); - Directory rootDir(m_fs, fd.rootDirInode); - OX_REQUIRE(inode, rootDir.find(path)); - OX_REQUIRE(st, statInode(inode)); - if (st.fileType == FileType::NormalFile || recursive) { - if (auto err = rootDir.remove(path)) { - // removal failed, try putting the index back - oxLogError(rootDir.write(path, inode)); - return err; - } - } else { - oxTrace("FileSystemTemplate.remove.fail", "Tried to remove directory without recursive setting."); - return ox::Error(1); - } - return ox::Error(0); -} - template Error FileSystemTemplate::resize() noexcept { return m_fs.resize(); diff --git a/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp b/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp index dc56b9c..4cc7aa9 100644 --- a/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp +++ b/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp @@ -75,14 +75,6 @@ Result> PassThroughFS::ls(StringViewCR dir) const noexcept { return out; } -Error PassThroughFS::remove(StringViewCR path, bool recursive) noexcept { - if (recursive) { - return ox::Error(std::filesystem::remove_all(m_path / stripSlash(path)) != 0); - } else { - return ox::Error(std::filesystem::remove(m_path / stripSlash(path)) != 0); - } -} - Error PassThroughFS::resize(uint64_t, void*) noexcept { // unsupported return ox::Error(1, "resize is not supported by PassThroughFS"); @@ -167,6 +159,14 @@ Error PassThroughFS::readFileInodeRange(uint64_t, std::size_t, std::size_t, void return ox::Error(1, "read(uint64_t, std::size_t, std::size_t, void*, std::size_t*) is not supported by PassThroughFS"); } +Error PassThroughFS::removePath(StringViewCR path, bool const recursive) noexcept { + if (recursive) { + return ox::Error{std::filesystem::remove_all(m_path / stripSlash(path)) == 0}; + } else { + return ox::Error{!std::filesystem::remove(m_path / stripSlash(path))}; + } +} + Error PassThroughFS::writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType) noexcept { const auto p = (m_path / stripSlash(path)); try { diff --git a/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp b/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp index 0a42d82..a424cef 100644 --- a/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp +++ b/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp @@ -45,8 +45,6 @@ class PassThroughFS: public FileSystem { template Error ls(StringViewCR dir, F cb) const noexcept; - Error remove(StringViewCR path, bool recursive) noexcept override; - Error resize(uint64_t size, void *buffer) noexcept override; Result statInode(uint64_t inode) const noexcept override; @@ -75,6 +73,8 @@ class PassThroughFS: public FileSystem { Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override; + Error removePath(StringViewCR path, bool recursive) noexcept override; + Error writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType fileType) noexcept override; Error writeFileInode(uint64_t inode, const void *buffer, uint64_t size, FileType fileType) noexcept override; diff --git a/deps/ox/src/ox/std/anyptr.hpp b/deps/ox/src/ox/std/anyptr.hpp index 2e3ad1c..19f96bf 100644 --- a/deps/ox/src/ox/std/anyptr.hpp +++ b/deps/ox/src/ox/std/anyptr.hpp @@ -15,18 +15,22 @@ namespace ox { -class AnyPtr { +namespace detail { + +template +class AnyPtrT { private: struct WrapBase { virtual constexpr ~WrapBase() = default; virtual constexpr WrapBase *copyTo(ox::Span s) noexcept = 0; virtual constexpr operator bool() const noexcept = 0; + virtual void free() noexcept = 0; }; template - struct Wrap: public WrapBase { + struct Wrap final: WrapBase { T *data{}; - constexpr Wrap(T *pData) noexcept: data(pData) { + explicit constexpr Wrap(T *pData) noexcept: data(pData) { } constexpr WrapBase *copyTo(ox::Span s) noexcept override { oxAssert(s.size() >= sizeof(Wrap), "too small buffer"); @@ -39,39 +43,60 @@ class AnyPtr { constexpr operator bool() const noexcept override { return data != nullptr; } + constexpr void free() noexcept override { + safeDelete(data); + data = {}; + } }; WrapBase *m_wrapPtr{}; ox::Array)> m_wrapData; public: - constexpr AnyPtr() noexcept = default; + constexpr AnyPtrT() noexcept = default; template - constexpr AnyPtr(T *ptr) noexcept { + constexpr AnyPtrT(T *ptr) noexcept { if (std::is_constant_evaluated()) { - m_wrapPtr = new Wrap(ptr); + m_wrapPtr = new Wrap(ptr); } else { - m_wrapPtr = new(m_wrapData.data()) Wrap(ptr); + m_wrapPtr = new(m_wrapData.data()) Wrap(ptr); } } - constexpr AnyPtr(AnyPtr const&other) noexcept { + constexpr AnyPtrT(AnyPtrT const&other) noexcept { if (other) { m_wrapPtr = other.m_wrapPtr->copyTo(m_wrapData); } } - constexpr ~AnyPtr() noexcept { + constexpr AnyPtrT(AnyPtrT &&other) noexcept { + if (other) { + m_wrapPtr = other.m_wrapPtr->copyTo(m_wrapData); + if (std::is_constant_evaluated()) { + ox::safeDelete(m_wrapPtr); + } + other.m_wrapPtr = {}; + } + } + + constexpr ~AnyPtrT() noexcept { + if constexpr(unique) { + free(); + } if (std::is_constant_evaluated()) { ox::safeDelete(m_wrapPtr); } } template - constexpr AnyPtr &operator=(T *ptr) noexcept { - if (std::is_constant_evaluated()) { + constexpr AnyPtrT &operator=(T *ptr) noexcept { + if constexpr(unique) { + free(); + } else if (std::is_constant_evaluated()) { ox::safeDelete(m_wrapPtr); + } + if (std::is_constant_evaluated()) { m_wrapPtr = new Wrap(ptr); } else { m_wrapPtr = new(m_wrapData.data()) Wrap(ptr); @@ -79,10 +104,14 @@ class AnyPtr { return *this; } - constexpr AnyPtr &operator=(AnyPtr const&ptr) noexcept { + constexpr AnyPtrT &operator=(AnyPtrT const&ptr) noexcept { if (this != &ptr) { - if (ptr) { + if constexpr(unique) { + free(); + } else if (std::is_constant_evaluated()) { ox::safeDelete(m_wrapPtr); + } + if (ptr) { m_wrapPtr = ptr.m_wrapPtr->copyTo(m_wrapData); } else { m_wrapPtr = nullptr; @@ -91,10 +120,40 @@ class AnyPtr { return *this; } + constexpr AnyPtrT &operator=(AnyPtrT &&ptr) noexcept { + if (this != &ptr) { + if constexpr(unique) { + free(); + } else if (std::is_constant_evaluated()) { + ox::safeDelete(m_wrapPtr); + } + if (ptr) { + m_wrapPtr = ptr.m_wrapPtr->copyTo(m_wrapData); + if (std::is_constant_evaluated()) { + ox::safeDelete(ptr.m_wrapPtr); + ptr.m_wrapPtr = nullptr; + } + } else { + m_wrapPtr = nullptr; + } + } + return *this; + } + constexpr operator bool() const noexcept { return m_wrapPtr && *m_wrapPtr; } + constexpr void free() noexcept { + if (m_wrapPtr) { + m_wrapPtr->free(); + } + if (std::is_constant_evaluated()) { + ox::safeDelete(m_wrapPtr); + } + m_wrapPtr = nullptr; + } + template [[nodiscard]] constexpr T *get() const noexcept { @@ -104,6 +163,12 @@ class AnyPtr { return dynamic_cast*>(m_wrapPtr)->data; #endif } + }; +} + +using AnyPtr = detail::AnyPtrT; +using UAnyPtr = detail::AnyPtrT; + } \ No newline at end of file diff --git a/deps/ox/src/ox/std/string.hpp b/deps/ox/src/ox/std/string.hpp index caaf105..baf8f2d 100644 --- a/deps/ox/src/ox/std/string.hpp +++ b/deps/ox/src/ox/std/string.hpp @@ -423,9 +423,10 @@ constexpr BasicString BasicString::operato const std::size_t strLen = src.len(); const auto currentLen = len(); BasicString cpy(currentLen + strLen); - cpy.m_buff.resize(m_buff.size() + strLen); + cpy.m_buff.resize(m_buff.size() + strLen + 1); ox::listcpy(&cpy.m_buff[0], m_buff.data(), currentLen); - ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen + 1); + ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen); + cpy.m_buff[cpy.m_buff.size() - 1] = 0; return cpy; } @@ -436,7 +437,8 @@ constexpr BasicString BasicString::operato BasicString cpy(currentLen + strLen); cpy.m_buff.resize(m_buff.size() + strLen); ox::listcpy(&cpy.m_buff[0], m_buff.data(), currentLen); - ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen + 1); + ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen); + cpy.m_buff[cpy.m_buff.size() - 1] = 0; return cpy; } diff --git a/src/nostalgia/modules/core/include/nostalgia/core/context.hpp b/src/nostalgia/modules/core/include/nostalgia/core/context.hpp index 9fcffb2..2f2df27 100644 --- a/src/nostalgia/modules/core/include/nostalgia/core/context.hpp +++ b/src/nostalgia/modules/core/include/nostalgia/core/context.hpp @@ -17,11 +17,9 @@ namespace nostalgia::core { class Context; -struct ContextDeleter { - void operator()(Context *p) noexcept; -}; +void safeDelete(Context *ctx) noexcept; -using ContextUPtr = ox::UPtr; +using ContextUPtr = ox::UPtr; ox::Result init(turbine::Context &tctx, InitParams const¶ms = {}) noexcept; diff --git a/src/nostalgia/modules/core/src/gba/context.cpp b/src/nostalgia/modules/core/src/gba/context.cpp index d54262d..ab378d3 100644 --- a/src/nostalgia/modules/core/src/gba/context.cpp +++ b/src/nostalgia/modules/core/src/gba/context.cpp @@ -10,8 +10,8 @@ namespace nostalgia::core { -void ContextDeleter::operator()(Context *p) noexcept { - ox::safeDelete(p); +void safeDelete(Context *ctx) noexcept { + delete ctx; } Context::Context(turbine::Context &tctx) noexcept: turbineCtx(tctx) { diff --git a/src/nostalgia/modules/core/src/opengl/context.cpp b/src/nostalgia/modules/core/src/opengl/context.cpp index 0644e9a..fd727bc 100644 --- a/src/nostalgia/modules/core/src/opengl/context.cpp +++ b/src/nostalgia/modules/core/src/opengl/context.cpp @@ -7,8 +7,8 @@ namespace nostalgia::core { -void ContextDeleter::operator()(Context *p) noexcept { - ox::safeDelete(p); +void safeDelete(Context *ctx) noexcept { + delete ctx; } Context::Context(turbine::Context &tctx, InitParams const¶ms) noexcept: diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/CMakeLists.txt b/src/nostalgia/modules/core/src/studio/paletteeditor/CMakeLists.txt index 245c5e9..cdaf9d3 100644 --- a/src/nostalgia/modules/core/src/studio/paletteeditor/CMakeLists.txt +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources( NostalgiaCore-Studio PRIVATE commands/addcolorcommand.cpp + commands/addpagecommand.cpp commands/applycolorallpagescommand.cpp commands/duplicatepagecommand.cpp commands/movecolorcommand.cpp diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.cpp b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.cpp new file mode 100644 index 0000000..828bbc9 --- /dev/null +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.cpp @@ -0,0 +1,28 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include "commands.hpp" + +#include "addpagecommand.hpp" + +namespace nostalgia::core { + +AddPageCommand::AddPageCommand(Palette &pal) noexcept: + m_pal(pal) {} + +int AddPageCommand::commandId() const noexcept { + return static_cast(PaletteEditorCommandId::AddPage); +} + +ox::Error AddPageCommand::redo() noexcept { + m_pal.pages.emplace_back(ox::sfmt("Page {}", m_pal.pages.size() + 1), ox::Vector{}); + return {}; +} + +ox::Error AddPageCommand::undo() noexcept { + m_pal.pages.pop_back(); + return {}; +} + +} diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.hpp b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.hpp new file mode 100644 index 0000000..58f31d7 --- /dev/null +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/addpagecommand.hpp @@ -0,0 +1,31 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include + +namespace nostalgia::core { + +class AddPageCommand: public studio::UndoCommand { + private: + Palette &m_pal; + + public: + explicit AddPageCommand(Palette &pal) noexcept; + + ~AddPageCommand() noexcept override = default; + + [[nodiscard]] + int commandId() const noexcept final; + + ox::Error redo() noexcept final; + + ox::Error undo() noexcept final; + +}; + +} diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/commands.hpp b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/commands.hpp index 778e450..e6be3b4 100644 --- a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/commands.hpp +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/commands.hpp @@ -9,6 +9,7 @@ namespace nostalgia::core { enum class PaletteEditorCommandId { ApplyColorAllPages, RenamePage, + AddPage, DuplicatePage, RemovePage, AddColor, diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/duplicatepagecommand.cpp b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/duplicatepagecommand.cpp index 6f52860..e4888ef 100644 --- a/src/nostalgia/modules/core/src/studio/paletteeditor/commands/duplicatepagecommand.cpp +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/commands/duplicatepagecommand.cpp @@ -23,7 +23,7 @@ int DuplicatePageCommand::commandId() const noexcept { } ox::Error DuplicatePageCommand::redo() noexcept { - m_pal.pages.emplace(m_dstIdx, "", std::move(m_page)); + m_pal.pages.emplace(m_dstIdx, ox::sfmt("Page {}", m_pal.pages.size() + 1), std::move(m_page)); return {}; } diff --git a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp index ade3751..1ae4f7b 100644 --- a/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp +++ b/src/nostalgia/modules/core/src/studio/paletteeditor/paletteeditor-imgui.cpp @@ -7,6 +7,7 @@ #include #include "commands/addcolorcommand.hpp" +#include "commands/addpagecommand.hpp" #include "commands/applycolorallpagescommand.hpp" #include "commands/duplicatepagecommand.hpp" #include "commands/movecolorcommand.hpp" @@ -196,7 +197,11 @@ void PaletteEditorImGui::drawPagesEditor() noexcept { constexpr auto toolbarHeight = 40; auto const btnSz = ImVec2{paneSz.x / 4 - 5.5f, 24}; if (ImGui::Button("Add", btnSz)) { - std::ignore = pushCommand(m_pal, 0u, m_pal.pages.size()); + if (m_pal.pages.empty()) { + std::ignore = pushCommand(m_pal); + } else { + std::ignore = pushCommand(m_pal, 0u, m_pal.pages.size()); + } m_page = m_pal.pages.size() - 1; } ImGui::SameLine(); diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp index ccce34e..5211211 100644 --- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -234,7 +234,7 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { ImGui::BeginChild("SubSheets", {s_palViewWidth - 24, ySize / 2.f}, true); { static constexpr auto btnHeight = ig::BtnSz.y; - auto const btnSize = ImVec2{btnHeight, btnHeight}; + auto constexpr btnSize = ImVec2{btnHeight, btnHeight}; if (ig::PushButton("+", btnSize)) { auto insertOnIdx = m_model.activeSubSheetIdx(); auto const&parent = m_model.activeSubSheet(); @@ -258,16 +258,19 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { m_exportMenu.show(); } TileSheet::SubSheetIdx path; - static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; - if (ImGui::BeginTable("Subsheets", 4, flags)) { - ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide); - ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 25); - ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50); - ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50); - ImGui::TableHeadersRow(); - drawSubsheetSelector(m_view.img().subsheet, path); - ImGui::EndTable(); - } + static constexpr auto flags = + ImGuiTableFlags_RowBg | + ImGuiTableFlags_NoBordersInBody | + ImGuiTableFlags_ScrollY; + if (ImGui::BeginTable("Subsheets", 4, flags)) { + ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide); + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 25); + ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50); + ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50); + ImGui::TableHeadersRow(); + drawSubsheetSelector(m_view.img().subsheet, path); + ImGui::EndTable(); + } } ImGui::EndChild(); } @@ -440,6 +443,16 @@ void TileSheetEditorImGui::drawPaletteMenu() noexcept { if (ig::ComboBox("Palette", files, m_selectedPaletteIdx)) { oxLogError(m_model.setPalette(files[m_selectedPaletteIdx])); } + if (ig::DragDropTarget const dragDropTarget; dragDropTarget) { + auto const [ref, err] = ig::getDragDropPayload("FileRef"); + if (!err) { + auto const oldVal = m_selectedPaletteIdx; + std::ignore = ox::findIdx(files.begin(), files.end(), ref.path).moveTo(m_selectedPaletteIdx); + if (oldVal != m_selectedPaletteIdx) { + oxLogError(m_model.setPalette(files[m_selectedPaletteIdx])); + } + } + } auto const pages = m_model.pal().pages.size(); if (pages > 1) { ig::IndentStackItem const indentStackItem{20}; @@ -462,8 +475,12 @@ void TileSheetEditorImGui::drawPaletteMenu() noexcept { } } // header + auto constexpr palTblFlags = + ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp | + ImGuiTableFlags_ScrollY; if (ImGui::BeginTable( - "PaletteTable", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + "PaletteTable", 4, palTblFlags)) { ImGui::TableSetupColumn("Idx", 0, 0.6f); ImGui::TableSetupColumn("", 0, 0.22f); ImGui::TableSetupColumn("Name", 0, 3); diff --git a/src/nostalgia/player/CMakeLists.txt b/src/nostalgia/player/CMakeLists.txt index d260957..331df8b 100644 --- a/src/nostalgia/player/CMakeLists.txt +++ b/src/nostalgia/player/CMakeLists.txt @@ -1,26 +1,26 @@ add_executable( - nostalgia WIN32 + Nostalgia WIN32 app.cpp ) # enable LTO if(NOT WIN32) - set_property(TARGET nostalgia PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + set_property(TARGET Nostalgia PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) endif() if(COMMAND OBJCOPY_FILE) - set_target_properties(nostalgia + set_target_properties(Nostalgia PROPERTIES LINK_FLAGS ${LINKER_FLAGS} COMPILER_FLAGS "-mthumb -mthumb-interwork" ) - OBJCOPY_FILE(nostalgia) - #PADBIN_FILE(nostalgia) + OBJCOPY_FILE(Nostalgia) + #PADBIN_FILE(Nostalgia) endif() target_link_libraries( - nostalgia + Nostalgia NostalgiaKeelModules NostalgiaProfile OlympicApplib @@ -29,7 +29,7 @@ target_link_libraries( install( TARGETS - nostalgia + Nostalgia DESTINATION bin ) diff --git a/src/nostalgia/player/app.hpp b/src/nostalgia/player/app.hpp deleted file mode 100644 index 3435715..0000000 --- a/src/nostalgia/player/app.hpp +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. - */ - -#include -#include - -typename ox::Error run(ox::UniquePtr &&fs) noexcept; diff --git a/src/olympic/keel/include/keel/typeconv.hpp b/src/olympic/keel/include/keel/typeconv.hpp index 3d9ea00..07a238f 100644 --- a/src/olympic/keel/include/keel/typeconv.hpp +++ b/src/olympic/keel/include/keel/typeconv.hpp @@ -20,6 +20,8 @@ class Wrap { virtual ox::CStringView typeName() const noexcept = 0; [[nodiscard]] virtual int typeVersion() const noexcept = 0; + [[nodiscard]] + virtual ox::UAnyPtr moveToCopy() noexcept = 0; }; template @@ -27,6 +29,11 @@ class WrapT: public Wrap { public: [[nodiscard]] virtual constexpr T &obj() noexcept = 0; + + ox::UAnyPtr moveToCopy() noexcept final { + return new T{std::move(obj())}; + } + }; template @@ -184,7 +191,18 @@ ox::Result> convert( auto &src, ox::StringViewCR dstTypeName, int const dstTypeVersion) noexcept { - return convert(ctx, WrapRef{src}, dstTypeName, dstTypeVersion); + WrapRef ref{src}; + return convert(ctx, static_cast(ref), dstTypeName, dstTypeVersion); +} + +ox::Result> convert( + keel::Context &ctx, + auto const&src, + ox::StringViewCR dstTypeName, + int const dstTypeVersion) noexcept { + auto srcCpy = src; + WrapRef ref{srcCpy}; + return convert(ctx, ref, dstTypeName, dstTypeVersion); } template diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index 52788ec..2127d8f 100644 --- a/src/olympic/studio/applib/src/CMakeLists.txt +++ b/src/olympic/studio/applib/src/CMakeLists.txt @@ -2,8 +2,10 @@ add_library( StudioAppLib aboutpopup.cpp clawviewer.cpp + 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 new file mode 100644 index 0000000..9ef0e06 --- /dev/null +++ b/src/olympic/studio/applib/src/deleteconfirmation.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include "deleteconfirmation.hpp" + +namespace studio { + +DeleteConfirmation::DeleteConfirmation() noexcept { + setTitle("Delete Item"); +} + +void DeleteConfirmation::openPath(ox::StringViewCR path) noexcept { + open(); + m_path = path; +} + +void DeleteConfirmation::open() noexcept { + m_path = ""; + m_stage = Stage::Opening; +} + +void DeleteConfirmation::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool DeleteConfirmation::isOpen() const noexcept { + return m_open; +} + +void DeleteConfirmation::draw(StudioContext &ctx) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(title().c_str()); + m_stage = Stage::Open; + m_open = true; + [[fallthrough]]; + 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, "Yes", "No") != ig::PopupResponse::None) { + deleteFile.emit(m_path); + close(); + } + }); + break; + } +} + +} diff --git a/src/olympic/studio/applib/src/deleteconfirmation.hpp b/src/olympic/studio/applib/src/deleteconfirmation.hpp new file mode 100644 index 0000000..012dbcb --- /dev/null +++ b/src/olympic/studio/applib/src/deleteconfirmation.hpp @@ -0,0 +1,43 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +namespace studio { + +class DeleteConfirmation final: public Popup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_path; + + public: + ox::Signal deleteFile; + + DeleteConfirmation() 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; + +}; + +} diff --git a/src/olympic/studio/applib/src/newdir.cpp b/src/olympic/studio/applib/src/newdir.cpp new file mode 100644 index 0000000..01caed3 --- /dev/null +++ b/src/olympic/studio/applib/src/newdir.cpp @@ -0,0 +1,63 @@ +/* + * 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 (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + newDir.emit(m_path + "/" + m_str); + close(); + } + 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 0000000..b79ef0b --- /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/newmenu.cpp b/src/olympic/studio/applib/src/newmenu.cpp index 806e3dc..a0b5be5 100644 --- a/src/olympic/studio/applib/src/newmenu.cpp +++ b/src/olympic/studio/applib/src/newmenu.cpp @@ -14,7 +14,7 @@ namespace studio { NewMenu::NewMenu() noexcept { setTitle("New Item"); - setSize({230, 140}); + setSize({280, 180}); } void NewMenu::open() noexcept { @@ -22,6 +22,12 @@ void NewMenu::open() noexcept { m_selectedType = 0; m_itemName = ""; m_typeName = ""; + m_path = ""; +} + +void NewMenu::openPath(ox::StringParam path) noexcept { + open(); + m_path = std::move(path); } void NewMenu::close() noexcept { @@ -33,7 +39,7 @@ bool NewMenu::isOpen() const noexcept { return m_open; } -void NewMenu::draw(studio::StudioContext &sctx) noexcept { +void NewMenu::draw(StudioContext &sctx) noexcept { switch (m_stage) { case Stage::Opening: ImGui::OpenPopup(title().c_str()); @@ -46,51 +52,73 @@ void NewMenu::draw(studio::StudioContext &sctx) noexcept { case Stage::NewItemName: drawNewItemName(sctx); break; + case Stage::NewItemTemplate: + drawNewItemTemplate(sctx); + break; case Stage::Closed: m_open = false; break; } } -void NewMenu::addItemMaker(ox::UniquePtr &&im) noexcept { +void NewMenu::addItemMaker(ox::UPtr &&im) noexcept { m_types.emplace_back(std::move(im)); std::sort( m_types.begin(), m_types.end(), [](ox::UPtr const&im1, ox::UPtr const&im2) { - return im1->typeName < im2->typeName; + return im1->typeDisplayName() < im2->typeDisplayName(); }); } -void NewMenu::drawNewItemType(studio::StudioContext &sctx) noexcept { - drawWindow(sctx.tctx, &m_open, [this] { - auto const allocSz = m_types.size() * sizeof(char const*); - auto mem = ox_malloca(allocSz, char const*, nullptr); - auto items = ox::Span{mem.get(), allocSz}; - for (auto i = 0u; auto const&im : m_types) { - items[i] = im->typeName.c_str(); - ++i; +void NewMenu::installItemTemplate(ox::UPtr &tmplt) noexcept { + for (auto const&im : m_types) { + if (im->installTemplate(tmplt)) { + break; } - ImGui::ListBox("Item Type", &m_selectedType, items.data(), static_cast(m_types.size())); - drawFirstPageButtons(); + } +} + +void NewMenu::drawNewItemType(StudioContext const&sctx) noexcept { + drawWindow(sctx.tctx, m_open, [this] { + ig::ListBox("Item Type", [&](size_t const i) -> ox::CStringView { + return m_types[i]->typeDisplayName(); + }, m_types.size(), m_selectedType, {200, 100}); + auto const&im = *m_types[m_selectedType]; + drawFirstPageButtons(im.itemTemplates().size() == 1 ? + Stage::NewItemName : Stage::NewItemTemplate); }); } -void NewMenu::drawNewItemName(studio::StudioContext &sctx) noexcept { - drawWindow(sctx.tctx, &m_open, [this, &sctx] { - auto const typeIdx = static_cast(m_selectedType); - if (typeIdx < m_types.size()) { +void NewMenu::drawNewItemTemplate(StudioContext &sctx) noexcept { + drawWindow(sctx.tctx, m_open, [this] { + auto const&templates = + m_types[m_selectedType]->itemTemplates(); + ig::ListBox("Template", [&](size_t const i) -> ox::CStringView { + return templates[i]->name(); + }, templates.size(), m_selectedTemplate, {200, 100}); + drawButtons(Stage::NewItemType, Stage::NewItemName); + }); +} + +void NewMenu::drawNewItemName(StudioContext &sctx) noexcept { + drawWindow(sctx.tctx, m_open, [this, &sctx] { + if (m_selectedType < m_types.size()) { ig::InputText("Name", m_itemName); } drawLastPageButtons(sctx); }); } -void NewMenu::drawFirstPageButtons() noexcept { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 130); +void NewMenu::drawButtons(Stage const prev, Stage const next) noexcept { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 198); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20); - auto const btnSz = ImVec2(60, 20); + constexpr ImVec2 btnSz{60, 20}; + if (ImGui::Button("Back", btnSz)) { + m_stage = prev; + } + ImGui::SameLine(); if (ImGui::Button("Next", btnSz)) { - m_stage = Stage::NewItemName; + m_stage = next; } ImGui::SameLine(); if (ImGui::Button("Cancel", btnSz)) { @@ -99,35 +127,51 @@ void NewMenu::drawFirstPageButtons() noexcept { } } -void NewMenu::drawLastPageButtons(studio::StudioContext &sctx) noexcept { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 138); +void NewMenu::drawFirstPageButtons(Stage const next) noexcept { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 130); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20); - if (ImGui::Button("Back")) { - m_stage = Stage::NewItemType; + constexpr ImVec2 btnSz{60, 20}; + if (ImGui::Button("Next", btnSz)) { + m_stage = next; } ImGui::SameLine(); - if (ImGui::Button("Finish")) { - finish(sctx); - } - ImGui::SameLine(); - if (ImGui::Button("Quit")) { + if (ImGui::Button("Cancel", btnSz)) { ImGui::CloseCurrentPopup(); m_stage = Stage::Closed; } } -void NewMenu::finish(studio::StudioContext &sctx) noexcept { +void NewMenu::drawLastPageButtons(StudioContext &sctx) noexcept { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 198); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20); + constexpr ImVec2 btnSz{60, 20}; + if (ImGui::Button("Back", btnSz)) { + m_stage = Stage::NewItemType; + } + ImGui::SameLine(); + if (ImGui::Button("Finish", btnSz)) { + finish(sctx); + } + ImGui::SameLine(); + if (ImGui::Button("Quit", btnSz)) { + ImGui::CloseCurrentPopup(); + m_stage = Stage::Closed; + } +} + +void NewMenu::finish(StudioContext &sctx) noexcept { if (m_itemName.len() == 0) { - oxLogError(ox::Error(1, "New file error: no file name")); + oxLogError(ox::Error{1, "New file error: no file name"}); return; } - auto const&typeMaker = *m_types[static_cast(m_selectedType)]; - if (sctx.project->exists(typeMaker.itemPath(m_itemName))) { - oxLogError(ox::Error(1, "New file error: file already exists")); + auto const&im = *m_types[m_selectedType]; + auto const path = m_path.len() ? + im.itemPath(m_itemName, m_path) : im.itemPath(m_itemName); + if (sctx.project->exists(path)) { + oxLogError(ox::Error{1, "New file error: file already exists"}); return; } - auto const [path, err] = typeMaker.write(sctx, m_itemName); - if (err) { + if (auto const err = im.write(sctx, path, m_selectedTemplate)) { oxLogError(err); return; } diff --git a/src/olympic/studio/applib/src/newmenu.hpp b/src/olympic/studio/applib/src/newmenu.hpp index c8ac34e..30ebb68 100644 --- a/src/olympic/studio/applib/src/newmenu.hpp +++ b/src/olympic/studio/applib/src/newmenu.hpp @@ -13,13 +13,14 @@ namespace studio { -class NewMenu: public studio::Popup { +class NewMenu final: public Popup { public: enum class Stage { Closed, Opening, NewItemType, NewItemName, + NewItemTemplate, }; // emits path parameter @@ -29,13 +30,17 @@ class NewMenu: public studio::Popup { Stage m_stage = Stage::Closed; ox::String m_typeName; ox::IString<255> m_itemName; - ox::Vector> m_types; - int m_selectedType = 0; + ox::String m_path; + ox::Vector> m_types; + size_t m_selectedType = 0; + size_t m_selectedTemplate = 0; bool m_open = false; public: NewMenu() noexcept; + void openPath(ox::StringParam path) noexcept; + void open() noexcept override; void close() noexcept override; @@ -43,37 +48,70 @@ class NewMenu: public studio::Popup { [[nodiscard]] bool isOpen() const noexcept override; - void draw(studio::StudioContext &sctx) noexcept override; + void draw(StudioContext &sctx) noexcept override; template - void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept; + void addItemType( + ox::StringParam displayName, + ox::StringParam parentDir, + ox::StringParam fileExt, + T itemTempl, + ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept; template - void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept; + void addItemType( + ox::StringParam displayName, + ox::StringParam parentDir, + ox::StringParam fileExt, + ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept; - void addItemMaker(ox::UniquePtr &&im) noexcept; + void addItemMaker(ox::UPtr &&im) noexcept; + + void installItemTemplate(ox::UPtr &tmplt) noexcept; private: - void drawNewItemType(studio::StudioContext &sctx) noexcept; + void drawNewItemType(StudioContext const&sctx) noexcept; - void drawNewItemName(studio::StudioContext &sctx) noexcept; + void drawNewItemName(StudioContext &sctx) noexcept; - void drawFirstPageButtons() noexcept; + void drawNewItemTemplate(StudioContext &sctx) noexcept; - void drawLastPageButtons(studio::StudioContext &sctx) noexcept; + void drawButtons(Stage prev, Stage next) noexcept; - void finish(studio::StudioContext &sctx) noexcept; + void drawFirstPageButtons(Stage next) noexcept; + + void drawLastPageButtons(StudioContext &sctx) noexcept; + + void finish(StudioContext &sctx) noexcept; }; template -void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt) noexcept { - m_types.emplace_back(ox::make>(std::move(displayName), std::move(parentDir), std::move(fileExt), std::move(itemTempl), pFmt)); +void NewMenu::addItemType( + ox::StringParam displayName, + ox::StringParam parentDir, + ox::StringParam fileExt, + T itemTempl, + ox::ClawFormat const pFmt) noexcept { + m_types.emplace_back(ox::make>( + std::move(displayName), + std::move(parentDir), + std::move(fileExt), + std::move(itemTempl), + pFmt)); } template -void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt) noexcept { - m_types.emplace_back(ox::make>(std::move(displayName), std::move(parentDir), std::move(fileExt), pFmt)); +void NewMenu::addItemType( + ox::StringParam displayName, + ox::StringParam parentDir, + ox::StringParam fileExt, + ox::ClawFormat const pFmt) noexcept { + m_types.emplace_back(ox::make>( + std::move(displayName), + std::move(parentDir), + std::move(fileExt), + pFmt)); } } diff --git a/src/olympic/studio/applib/src/newproject.cpp b/src/olympic/studio/applib/src/newproject.cpp index 6a0cdbe..136aa8c 100644 --- a/src/olympic/studio/applib/src/newproject.cpp +++ b/src/olympic/studio/applib/src/newproject.cpp @@ -49,7 +49,7 @@ void NewProject::draw(studio::StudioContext &ctx) noexcept { } void NewProject::drawNewProjectName(studio::StudioContext &sctx) noexcept { - drawWindow(sctx.tctx, &m_open, [this, &sctx] { + drawWindow(sctx.tctx, m_open, [this, &sctx] { ig::InputText("Name", m_projectName); ImGui::Text("Path: %s", m_projectPath.c_str()); if (ImGui::Button("Browse")) { diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index 0a05842..464f671 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -12,12 +12,12 @@ namespace studio { static ox::Result> buildProjectTreeModel( ProjectExplorer &explorer, - ox::StringView name, + ox::StringParam name, ox::StringView path, ProjectTreeModel *parent) noexcept { auto const fs = explorer.romFs(); OX_REQUIRE(stat, fs->stat(path)); - auto out = ox::make_unique(explorer, ox::String(name), parent); + auto out = ox::make_unique(explorer, std::move(name), parent); if (stat.fileType == ox::FileType::Directory) { OX_REQUIRE_M(children, fs->ls(path)); std::sort(children.begin(), children.end()); @@ -37,7 +37,7 @@ static ox::Result> buildProjectTreeModel( ProjectExplorer::ProjectExplorer(turbine::Context &ctx) noexcept: m_ctx(ctx) { } -void ProjectExplorer::draw(studio::StudioContext &ctx) noexcept { +void ProjectExplorer::draw(StudioContext &ctx) noexcept { auto const viewport = ImGui::GetContentRegionAvail(); ImGui::BeginChild("ProjectExplorer", ImVec2(300, viewport.y), true); ImGui::SetNextItemOpen(true); @@ -54,7 +54,7 @@ void ProjectExplorer::setModel(ox::UPtr &&model) noexcept { ox::Error ProjectExplorer::refreshProjectTreeModel(ox::StringViewCR) noexcept { OX_REQUIRE_M(model, buildProjectTreeModel(*this, "Project", "/", nullptr)); setModel(std::move(model)); - return ox::Error(0); + return {}; } } diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index 8341670..8ef2576 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -12,27 +12,31 @@ namespace studio { -class ProjectExplorer: public studio::Widget { +class ProjectExplorer: public Widget { private: ox::UPtr m_treeModel; turbine::Context &m_ctx; + public: + // slots + ox::Signal fileChosen; + ox::Signal addItem; + ox::Signal addDir; + ox::Signal deleteItem; + explicit ProjectExplorer(turbine::Context &ctx) noexcept; - void draw(studio::StudioContext &ctx) noexcept override; + void draw(StudioContext &ctx) noexcept override; void setModel(ox::UPtr &&model) noexcept; ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept; [[nodiscard]] - inline ox::FileSystem *romFs() noexcept { + ox::FileSystem *romFs() noexcept { return rom(m_ctx); } - // slots - public: - ox::Signal fileChosen; }; } diff --git a/src/olympic/studio/applib/src/projecttreemodel.cpp b/src/olympic/studio/applib/src/projecttreemodel.cpp index 40d98dc..cc7afd1 100644 --- a/src/olympic/studio/applib/src/projecttreemodel.cpp +++ b/src/olympic/studio/applib/src/projecttreemodel.cpp @@ -4,13 +4,18 @@ #include +#include +#include + #include "projectexplorer.hpp" #include "projecttreemodel.hpp" namespace studio { -ProjectTreeModel::ProjectTreeModel(ProjectExplorer &explorer, ox::String name, - ProjectTreeModel *parent) noexcept: +ProjectTreeModel::ProjectTreeModel( + ProjectExplorer &explorer, + ox::StringParam name, + ProjectTreeModel *parent) noexcept: m_explorer(explorer), m_parent(parent), m_name(std::move(name)) { @@ -23,14 +28,18 @@ 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 { + ig::IDStackItem const idStackItem{m_name}; + drawDirContextMenu(); } } else { auto const path = fullPath(); @@ -39,7 +48,17 @@ void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { m_explorer.fileChosen.emit(path); } + if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::MenuItem("Delete")) { + m_explorer.deleteItem.emit(path); + } + ImGui::EndPopup(); + } ImGui::TreePop(); + std::ignore = ig::dragDropSource([this] { + ImGui::Text("%s", m_name.c_str()); + return ig::setDragDropPayload("FileRef", FileRef{fullPath()}); + }); } } } @@ -48,11 +67,19 @@ void ProjectTreeModel::setChildren(ox::Vector> childr m_children = std::move(children); } -ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept { - if (m_parent) { - return m_parent->fullPath() + "/" + ox::StringView(m_name); - } - return {}; +void ProjectTreeModel::drawDirContextMenu() const noexcept { + if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::MenuItem("Add Item")) { + m_explorer.addItem.emit(fullPath()); + } + if (ImGui::MenuItem("Add Directory")) { + m_explorer.addDir.emit(fullPath()); + } + if (ImGui::MenuItem("Delete")) { + m_explorer.deleteItem.emit(fullPath()); + } + ImGui::EndPopup(); + } } } diff --git a/src/olympic/studio/applib/src/projecttreemodel.hpp b/src/olympic/studio/applib/src/projecttreemodel.hpp index 6e20505..fdfc1fa 100644 --- a/src/olympic/studio/applib/src/projecttreemodel.hpp +++ b/src/olympic/studio/applib/src/projecttreemodel.hpp @@ -17,19 +17,30 @@ class ProjectTreeModel { ProjectTreeModel *m_parent = nullptr; ox::String m_name; ox::Vector> m_children; + public: - explicit ProjectTreeModel(class ProjectExplorer &explorer, ox::String name, - ProjectTreeModel *parent = nullptr) noexcept; + explicit ProjectTreeModel( + ProjectExplorer &explorer, ox::StringParam name, + ProjectTreeModel *parent = nullptr) noexcept; ProjectTreeModel(ProjectTreeModel &&other) noexcept; - void draw(turbine::Context &ctx) const noexcept; + void draw(turbine::Context &tctx) const noexcept; void setChildren(ox::Vector> children) noexcept; private: + void drawDirContextMenu() const noexcept; + + template> [[nodiscard]] - ox::BasicString<255> fullPath() const noexcept; + Str fullPath() const noexcept { + if (m_parent) { + return m_parent->fullPath() + "/" + m_name; + } + return {}; + } + }; } diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 7ae49c5..a9358b3 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -52,6 +52,9 @@ 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); m_newMenu.finished.connect(this, &StudioUI::openFile); ImGui::GetIO().IniFilename = nullptr; @@ -251,6 +254,10 @@ void StudioUI::loadModule(Module const&mod) noexcept { for (auto &im : mod.itemMakers(m_sctx)) { m_newMenu.addItemMaker(std::move(im)); } + auto tmplts = mod.itemTemplates(m_sctx); + for (auto &t : tmplts) { + m_newMenu.installItemTemplate(t); + } } void StudioUI::loadModules() noexcept { @@ -338,6 +345,21 @@ 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 {}; +} + +ox::Error StudioUI::deleteFile(ox::StringViewCR path) noexcept { + m_deleteConfirmation.openPath(path); + return {}; +} + ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept { std::error_code ec; std::filesystem::create_directories(toStdStringView(path), ec); @@ -352,9 +374,11 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { OX_RETURN_ERROR( ox::make_unique_catch(keelCtx(m_tctx), std::move(path), m_projectDataDir) .moveTo(m_project)); - auto const sctx = applicationData(m_tctx); - sctx->project = m_project.get(); + 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_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 6490786..199feac 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -12,7 +12,10 @@ #include #include #include + #include "aboutpopup.hpp" +#include "deleteconfirmation.hpp" +#include "newdir.hpp" #include "newmenu.hpp" #include "newproject.hpp" #include "projectexplorer.hpp" @@ -38,12 +41,16 @@ class StudioUI: public ox::SignalHandler { BaseEditor *m_activeEditor = nullptr; 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_aboutPopup, + &m_deleteConfirmation, + &m_newDirDialog, }; bool m_showProjectExplorer = true; @@ -83,6 +90,12 @@ 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; + ox::Error createOpenProject(ox::StringViewCR path) noexcept; ox::Error openProjectPath(ox::StringParam path) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/dragdrop.hpp b/src/olympic/studio/modlib/include/studio/dragdrop.hpp new file mode 100644 index 0000000..e75bb04 --- /dev/null +++ b/src/olympic/studio/modlib/include/studio/dragdrop.hpp @@ -0,0 +1,22 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include +#include + +namespace studio { + +struct FileRef { + static constexpr auto TypeName = "net.drinkingtea.studio.FileRef"; + static constexpr auto TypeVersion = 1; + ox::String path; +}; + +OX_MODEL_BEGIN(FileRef) + OX_MODEL_FIELD(path) +OX_MODEL_END() + +} diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 2ef22c3..60bff75 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -63,6 +63,7 @@ auto dragDropSource(auto const&cb) noexcept { if (ig::DragDropSource const tgt; tgt) [[unlikely]] { return cb(); } + return ox::Error{}; } else { if (ig::DragDropSource const tgt; tgt) [[unlikely]] { cb(); @@ -175,9 +176,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}); @@ -214,7 +222,8 @@ bool ListBox( ox::CStringViewCR name, std::function const&f, size_t strCnt, - size_t &selIdx) noexcept; + size_t &selIdx, + ImVec2 const&sz = {0, 0}) noexcept; /** * diff --git a/src/olympic/studio/modlib/include/studio/itemmaker.hpp b/src/olympic/studio/modlib/include/studio/itemmaker.hpp index ec2f79b..e0b70e6 100644 --- a/src/olympic/studio/modlib/include/studio/itemmaker.hpp +++ b/src/olympic/studio/modlib/include/studio/itemmaker.hpp @@ -4,7 +4,9 @@ #pragma once -#include +#include + +#include #include #include @@ -13,75 +15,249 @@ namespace studio { -class ItemMaker { +class ItemTemplate { + private: + ox::String const m_name{"Default"}; + public: - ox::String const typeName; - ox::String const parentDir; - ox::String const fileExt; - constexpr explicit ItemMaker( + explicit ItemTemplate() noexcept = default; + + explicit ItemTemplate(ox::StringParam name) noexcept: m_name{std::move(name)} {} + + virtual ~ItemTemplate() = default; + + [[nodiscard]] + constexpr ox::String const&name() const noexcept { + return m_name; + } + + constexpr bool operator<=>(ItemTemplate const&other) const noexcept { + return m_name != other.name() ? (m_name < other.name() ? -1 : 1) : 0; + } + + [[nodiscard]] + ox::CStringView displayName() const noexcept { + return m_name; + } + + [[nodiscard]] + virtual ox::CStringView typeName() const noexcept = 0; + + [[nodiscard]] + virtual int typeVersion() const noexcept = 0; + + virtual ox::Result getItem( + keel::Context &ctx, ox::StringViewCR name, int version) noexcept = 0; + +}; + +template +class ItemTemplateT: public ItemTemplate { + private: + T const m_item; + + public: + constexpr ItemTemplateT() noexcept = default; + + explicit ItemTemplateT(ox::StringParam name, T item) noexcept: + ItemTemplate{std::move(name)}, m_item{std::move(item)} {} + + [[nodiscard]] + ox::CStringView typeName() const noexcept final { + return ox::ModelTypeName_v; + } + + [[nodiscard]] + int typeVersion() const noexcept final { + return ox::ModelTypeVersion_v; + } + + ox::Result getItem( + keel::Context &kctx, ox::StringViewCR name, int const version) noexcept final { + if (ox::ModelTypeVersion_v != version || ox::ModelTypeName_v != name) { + OX_REQUIRE_M(item, keel::convert(kctx, m_item, name, version)); + auto out = ox::Result{item->moveToCopy()}; + ox::safeDelete(item.release()); + return out; + } + return ox::UAnyPtr{new T{m_item}}; + } + +}; + +class ItemMaker { + private: + ox::Vector> m_templates; + ox::String const m_parentDir; + ox::String const m_fileExt; + ox::String const m_typeDisplayName; + + public: + constexpr ItemMaker( ox::StringParam pName, ox::StringParam pParentDir, ox::StringParam pFileExt) noexcept: - typeName{std::move(pName)}, - parentDir{std::move(pParentDir)}, - fileExt{std::move(pFileExt)} { + m_parentDir{std::move(pParentDir)}, + m_fileExt{std::move(pFileExt)}, + m_typeDisplayName{std::move(pName)} { } + virtual ~ItemMaker() noexcept = default; [[nodiscard]] - virtual ox::String itemPath(ox::StringView pName) const noexcept { - return ox::sfmt("/{}/{}.{}", parentDir, pName, fileExt); + ox::String const&typeDisplayName() const noexcept { + return m_typeDisplayName; + } + + bool installTemplate(ox::UPtr &tmpl) { + if (typeName() == tmpl->typeName() && + typeVersion() <= tmpl->typeVersion()) { + m_templates.emplace_back(std::move(tmpl)); + // begin() + 1 because 'Default' should always be first + std::sort(m_templates.begin() + 1, m_templates.end()); + return true; + } + return false; + } + + bool installTemplate(ox::UPtr &&tmpl) { + return installTemplate(tmpl); + } + + constexpr ox::Vector> const&itemTemplates() const noexcept { + return m_templates; + } + + [[nodiscard]] + ox::String const&defaultPath() const noexcept { + return m_parentDir; + } + + [[nodiscard]] + ox::String itemPath(ox::StringViewCR pName, ox::StringViewCR pPath) const noexcept { + return ox::sfmt("/{}/{}.{}", pPath, pName, m_fileExt); + } + + [[nodiscard]] + ox::String itemPath(ox::StringViewCR pName) const noexcept { + return ox::sfmt("/{}/{}.{}", m_parentDir, pName, m_fileExt); + } + + [[nodiscard]] + virtual ox::StringView typeName() const noexcept = 0; + + [[nodiscard]] + virtual int typeVersion() const noexcept = 0; + + /** + * Returns path of the file created. + * @param ctx + * @param pPath + * @param pTemplateIdx + * @return path of file or error in Result + */ + ox::Error write( + StudioContext &ctx, + ox::StringViewCR pPath, + size_t pTemplateIdx) const noexcept { + return writeItem(ctx, pPath, pTemplateIdx); } /** * Returns path of the file created. * @param ctx + * @param pPath * @param pName + * @param pTemplateIdx * @return path of file or error in Result */ - virtual ox::Result write(StudioContext &ctx, ox::StringView pName) const noexcept = 0; + ox::Error write( + StudioContext &ctx, + ox::StringViewCR pPath, + ox::StringViewCR pName, + size_t pTemplateIdx) const noexcept { + auto const path = itemPath(pName, pPath); + return writeItem(ctx, path, pTemplateIdx); + } + + protected: + virtual ox::Error writeItem( + StudioContext &ctx, + ox::StringViewCR pPath, + size_t pTemplateIdx) const noexcept = 0; + }; template -class ItemMakerT: public ItemMaker { +class ItemMakerT final: public ItemMaker { private: - T const m_item; ox::ClawFormat const m_fmt; + public: constexpr ItemMakerT( ox::StringParam pDisplayName, ox::StringParam pParentDir, ox::StringParam fileExt, - ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept: - ItemMaker(std::move(pDisplayName), std::move(pParentDir), std::move(fileExt)), - m_fmt(pFmt) { + ox::ClawFormat const pFmt = ox::ClawFormat::Metal) noexcept: + ItemMaker( + std::move(pDisplayName), + std::move(pParentDir), + std::move(fileExt)), + m_fmt{pFmt} { + installTemplate(ox::make_unique>()); } + constexpr ItemMakerT( ox::StringParam pDisplayName, ox::StringParam pParentDir, ox::StringParam fileExt, - T pItem, - ox::ClawFormat pFmt) noexcept: - ItemMaker(std::move(pDisplayName), std::move(pParentDir), std::move(fileExt)), - m_item(std::move(pItem)), - m_fmt(pFmt) { + T const&pItem, + ox::ClawFormat const pFmt) noexcept: + ItemMaker( + std::move(pDisplayName), + std::move(pParentDir), + std::move(fileExt)), + m_fmt{pFmt} { + installTemplate(ox::make_unique>(std::move(pItem))); } + constexpr ItemMakerT( ox::StringParam pDisplayName, ox::StringParam pParentDir, ox::StringParam fileExt, T &&pItem, - ox::ClawFormat pFmt) noexcept: - ItemMaker(std::move(pDisplayName), std::move(pParentDir), std::move(fileExt)), - m_item(std::move(pItem)), - m_fmt(pFmt) { + ox::ClawFormat const pFmt) noexcept: + ItemMaker( + std::move(pDisplayName), + std::move(pParentDir), + std::move(fileExt)), + m_fmt{pFmt} { + installTemplate(ox::make_unique>(std::move(pItem))); } - ox::Result write(studio::StudioContext &sctx, ox::StringView const pName) const noexcept override { - auto const path = itemPath(pName); - createUuidMapping(keelCtx(sctx.tctx), path, ox::UUID::generate().unwrap()); - OX_RETURN_ERROR(sctx.project->writeObj(path, m_item, m_fmt)); - return path; + + ox::StringView typeName() const noexcept override { + return ox::ModelTypeName_v; } + + int typeVersion() const noexcept override { + return ox::ModelTypeVersion_v; + } + + ox::Error writeItem( + StudioContext &ctx, + ox::StringViewCR pPath, + size_t const pTemplateIdx) const noexcept override { + createUuidMapping(keelCtx(ctx.tctx), pPath, ox::UUID::generate().unwrap()); + auto const&templates = itemTemplates(); + auto const tmplIdx = pTemplateIdx < templates.size() ? pTemplateIdx : 0; + OX_REQUIRE_M(tmpl, templates[tmplIdx]->getItem( + keelCtx(ctx), typeName(), typeVersion())); + auto item = tmpl.template get(); + OX_RETURN_ERROR(ctx.project->writeObj(pPath, *item, m_fmt)); + tmpl.free(); + return {}; + } + }; } diff --git a/src/olympic/studio/modlib/include/studio/module.hpp b/src/olympic/studio/modlib/include/studio/module.hpp index 8019be0..3844bda 100644 --- a/src/olympic/studio/modlib/include/studio/module.hpp +++ b/src/olympic/studio/modlib/include/studio/module.hpp @@ -31,6 +31,8 @@ class Module { virtual ox::Vector> itemMakers(studio::StudioContext&) const; + virtual ox::Vector> itemTemplates(studio::StudioContext&) const; + }; template diff --git a/src/olympic/studio/modlib/include/studio/popup.hpp b/src/olympic/studio/modlib/include/studio/popup.hpp index 40ae00f..9154b5c 100644 --- a/src/olympic/studio/modlib/include/studio/popup.hpp +++ b/src/olympic/studio/modlib/include/studio/popup.hpp @@ -47,7 +47,7 @@ class Popup { return m_title; } - void drawWindow(turbine::Context &ctx, bool *open, std::function const&drawContents); + void drawWindow(turbine::Context &ctx, bool &open, std::function const&drawContents); }; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index dc13901..8ee319f 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. @@ -45,7 +46,7 @@ constexpr ox::StringView parentDir(ox::StringView path) noexcept { return substr(path, 0, extStart); } -class Project { +class Project: public ox::SignalHandler { private: ox::SmallMap> m_typeFmt; keel::Context &m_ctx; @@ -91,6 +92,8 @@ class Project { ox::Result stat(ox::StringViewCR path) const noexcept; + ox::Error deleteItem(ox::StringViewCR path) noexcept; + [[nodiscard]] bool exists(ox::StringViewCR path) const noexcept; @@ -118,6 +121,7 @@ class Project { // 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 @@ -175,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/include/studio/studio.hpp b/src/olympic/studio/modlib/include/studio/studio.hpp index b18e826..cd74747 100644 --- a/src/olympic/studio/modlib/include/studio/studio.hpp +++ b/src/olympic/studio/modlib/include/studio/studio.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index be7d946..b6d8fb2 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) { @@ -136,11 +143,12 @@ bool FileComboBox( bool ListBox( ox::CStringViewCR name, std::function const&f, - size_t strCnt, - size_t &selIdx) noexcept { + size_t const strCnt, + size_t &selIdx, + ImVec2 const&sz) noexcept { auto out = false; - if (ImGui::BeginListBox(name.c_str())) { - for (auto i = 0u; i < strCnt; ++i) { + if (ImGui::BeginListBox(name.c_str(), sz)) { + for (size_t i = 0; i < strCnt; ++i) { auto str = f(i); ig::IDStackItem const idStackItem2(static_cast(i)); if (ImGui::Selectable(str.c_str(), selIdx == i)) { @@ -161,6 +169,12 @@ bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t }, list.size(), selIdx); } +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); +} + FilePicker::FilePicker( StudioContext &sctx, diff --git a/src/olympic/studio/modlib/src/module.cpp b/src/olympic/studio/modlib/src/module.cpp index b2fccce..4bd20ac 100644 --- a/src/olympic/studio/modlib/src/module.cpp +++ b/src/olympic/studio/modlib/src/module.cpp @@ -6,11 +6,15 @@ namespace studio { -ox::Vector Module::editors(studio::StudioContext&) const { +ox::Vector Module::editors(StudioContext&) const { return {}; } -ox::Vector> Module::itemMakers(studio::StudioContext&) const { +ox::Vector> Module::itemMakers(StudioContext&) const { + return {}; +} + +ox::Vector> Module::itemTemplates(StudioContext&) const { return {}; } diff --git a/src/olympic/studio/modlib/src/popup.cpp b/src/olympic/studio/modlib/src/popup.cpp index a31e859..04e7ba1 100644 --- a/src/olympic/studio/modlib/src/popup.cpp +++ b/src/olympic/studio/modlib/src/popup.cpp @@ -7,11 +7,11 @@ namespace studio { -void Popup::drawWindow(turbine::Context &ctx, bool *open, std::function const&drawContents) { +void Popup::drawWindow(turbine::Context &ctx, bool &open, std::function const&drawContents) { studio::ig::centerNextWindow(ctx); ImGui::SetNextWindowSize(static_cast(m_size)); constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; - if (ImGui::BeginPopupModal(m_title.c_str(), open, modalFlags)) { + if (ImGui::BeginPopupModal(m_title.c_str(), &open, modalFlags)) { drawContents(); ImGui::EndPopup(); } diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 84acad6..cd90d8a 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -59,16 +59,41 @@ 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 { return m_fs.stat(path); } +ox::Error Project::deleteItem(ox::StringViewCR path) noexcept { + 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; + } +} + bool Project::exists(ox::StringViewCR path) const noexcept { return m_fs.stat(path).error == 0; }