diff --git a/Makefile b/Makefile index 90c6529d..5e5c49cb 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 750e4172..105e2b93 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 e785e4b1..ebc83217 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 dc56b9c4..4cc7aa90 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 0a42d829..a424cefe 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 9d566ad7..19f96bf8 100644 --- a/deps/ox/src/ox/std/anyptr.hpp +++ b/deps/ox/src/ox/std/anyptr.hpp @@ -58,9 +58,9 @@ class AnyPtrT { template 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); } } diff --git a/deps/ox/src/ox/std/string.hpp b/deps/ox/src/ox/std/string.hpp index caaf105f..baf8f2d6 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/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp index f5717a54..5211211e 100644 --- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -443,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}; diff --git a/src/nostalgia/player/CMakeLists.txt b/src/nostalgia/player/CMakeLists.txt index d2609578..331df8ba 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 34357153..00000000 --- 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/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index 52788ec9..2127d8fe 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 00000000..9ef0e063 --- /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 00000000..012dbcbb --- /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 00000000..01caed37 --- /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 00000000..b79ef0b9 --- /dev/null +++ b/src/olympic/studio/applib/src/newdir.hpp @@ -0,0 +1,49 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +namespace studio { + +class NewDir final: public Popup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_path; + ox::IString<255> m_str; + + public: + ox::Signal newDir; + + NewDir() noexcept; + + void openPath(ox::StringViewCR path) noexcept; + + void open() noexcept override; + + void close() noexcept override; + + [[nodiscard]] + bool isOpen() const noexcept override; + + void draw(StudioContext &ctx) noexcept override; + + [[nodiscard]] + constexpr ox::CStringView value() const noexcept { + return m_str; + } + +}; + +} diff --git a/src/olympic/studio/applib/src/newmenu.cpp b/src/olympic/studio/applib/src/newmenu.cpp index 1fbe1b64..828de7c2 100644 --- a/src/olympic/studio/applib/src/newmenu.cpp +++ b/src/olympic/studio/applib/src/newmenu.cpp @@ -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 { @@ -158,7 +164,7 @@ void NewMenu::finish(StudioContext &sctx) noexcept { oxLogError(ox::Error{1, "New file error: no file name"}); return; } - auto const&im = *m_types[static_cast(m_selectedType)]; + auto const&im = *m_types[m_selectedType]; if (sctx.project->exists(im.itemPath(m_itemName))) { oxLogError(ox::Error{1, "New file error: file already exists"}); return; diff --git a/src/olympic/studio/applib/src/newmenu.hpp b/src/olympic/studio/applib/src/newmenu.hpp index c535764a..30ebb685 100644 --- a/src/olympic/studio/applib/src/newmenu.hpp +++ b/src/olympic/studio/applib/src/newmenu.hpp @@ -30,6 +30,7 @@ class NewMenu final: public Popup { Stage m_stage = Stage::Closed; ox::String m_typeName; ox::IString<255> m_itemName; + ox::String m_path; ox::Vector> m_types; size_t m_selectedType = 0; size_t m_selectedTemplate = 0; @@ -38,6 +39,8 @@ class NewMenu final: public Popup { public: NewMenu() noexcept; + void openPath(ox::StringParam path) noexcept; + void open() noexcept override; void close() noexcept override; diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index 0a058422..464f6718 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 8341670d..8ef25761 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 40d98dc0..cc7afd1a 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 6e205051..fdfc1fae 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 c4193a8d..a9358b33 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; @@ -342,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); @@ -356,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 64907860..199feac2 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 00000000..e75bb040 --- /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 01cd3e84..60bff752 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}); diff --git a/src/olympic/studio/modlib/include/studio/itemmaker.hpp b/src/olympic/studio/modlib/include/studio/itemmaker.hpp index edc536da..e0b70e61 100644 --- a/src/olympic/studio/modlib/include/studio/itemmaker.hpp +++ b/src/olympic/studio/modlib/include/studio/itemmaker.hpp @@ -128,6 +128,16 @@ class ItemMaker { 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); @@ -142,12 +152,40 @@ class ItemMaker { /** * 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::StringViewCR pName, size_t pTemplateIdx) 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 @@ -205,20 +243,19 @@ class ItemMakerT final: public ItemMaker { return ox::ModelTypeVersion_v; } - ox::Result write( - StudioContext &sctx, - ox::StringViewCR pName, + ox::Error writeItem( + StudioContext &ctx, + ox::StringViewCR pPath, size_t const pTemplateIdx) const noexcept override { - auto const path = itemPath(pName); - createUuidMapping(keelCtx(sctx.tctx), path, ox::UUID::generate().unwrap()); + 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(sctx), typeName(), typeVersion())); + keelCtx(ctx), typeName(), typeVersion())); auto item = tmpl.template get(); - OX_RETURN_ERROR(sctx.project->writeObj(path, *item, m_fmt)); + OX_RETURN_ERROR(ctx.project->writeObj(pPath, *item, m_fmt)); tmpl.free(); - return path; + return {}; } }; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index dc13901c..8ee319fe 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -19,6 +19,7 @@ namespace studio { enum class ProjectEvent { None, + DirAdded, FileAdded, // FileRecognized is triggered for all matching files upon a new // subscription to a section of the project and upon the addition of a file. @@ -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 b18e8263..cd747473 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 7618a2aa..b6d8fb2a 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -54,18 +54,22 @@ bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept { return ImGui::Button(lbl.c_str(), btnSz); } -PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) { +PopupResponse PopupControlsOkCancel( + float popupWidth, + bool &popupOpen, + ox::CStringViewCR ok, + ox::CStringViewCR cancel) { auto out = PopupResponse::None; constexpr auto btnSz = ImVec2{50, BtnSz.y}; ImGui::Separator(); ImGui::SetCursorPosX(popupWidth - 118); - if (ImGui::Button("OK", btnSz)) { + if (ImGui::Button(ok.c_str(), btnSz)) { ImGui::CloseCurrentPopup(); popupOpen = false; out = PopupResponse::OK; } ImGui::SameLine(); - if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button("Cancel", btnSz)) { + if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button(cancel.c_str(), btnSz)) { ImGui::CloseCurrentPopup(); popupOpen = false; out = PopupResponse::Cancel; @@ -73,8 +77,11 @@ PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) { return out; } -PopupResponse PopupControlsOkCancel(bool &popupOpen) { - return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen); +PopupResponse PopupControlsOkCancel( + bool &popupOpen, + ox::CStringViewCR ok, + ox::CStringViewCR cancel) { + return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen, ok, cancel); } bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) { diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 84acad60..cd90d8a0 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; }