diff --git a/src/olympic/studio/applib/src/newmenu.cpp b/src/olympic/studio/applib/src/newmenu.cpp index a0b5be55..91296383 100644 --- a/src/olympic/studio/applib/src/newmenu.cpp +++ b/src/olympic/studio/applib/src/newmenu.cpp @@ -12,9 +12,8 @@ namespace studio { -NewMenu::NewMenu() noexcept { +NewMenu::NewMenu(keel::Context &kctx) noexcept: m_kctx{kctx} { setTitle("New Item"); - setSize({280, 180}); } void NewMenu::open() noexcept { @@ -23,6 +22,11 @@ void NewMenu::open() noexcept { m_itemName = ""; m_typeName = ""; m_path = ""; + m_explorer.setModel(buildFileTreeModel( + m_explorer, + [](ox::StringViewCR, ox::FileStat const&s) { + return s.fileType == ox::FileType::Directory; + }).or_value(ox::UPtr{})); } void NewMenu::openPath(ox::StringParam path) noexcept { @@ -49,8 +53,8 @@ void NewMenu::draw(StudioContext &sctx) noexcept { case Stage::NewItemType: drawNewItemType(sctx); break; - case Stage::NewItemName: - drawNewItemName(sctx); + case Stage::NewItemPath: + drawNewItemPath(sctx); break; case Stage::NewItemTemplate: drawNewItemTemplate(sctx); @@ -79,32 +83,45 @@ void NewMenu::installItemTemplate(ox::UPtr &tmplt) noexcept { } void NewMenu::drawNewItemType(StudioContext const&sctx) noexcept { + setSize({280, 180}); 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); + Stage::NewItemPath : Stage::NewItemTemplate); + if (m_stage == Stage::NewItemPath) { + if (m_path.len() == 0) { + m_path = im.defaultPath(); + } + std::ignore = m_explorer.setSelectedPath(m_path); + } }); } -void NewMenu::drawNewItemTemplate(StudioContext &sctx) noexcept { +void NewMenu::drawNewItemTemplate(StudioContext const&sctx) noexcept { + setSize({280, 180}); 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); + drawButtons(Stage::NewItemType, Stage::NewItemPath); }); } -void NewMenu::drawNewItemName(StudioContext &sctx) noexcept { +void NewMenu::drawNewItemPath(StudioContext &sctx) noexcept { + setSize({380, 340}); drawWindow(sctx.tctx, m_open, [this, &sctx] { if (m_selectedType < m_types.size()) { ig::InputText("Name", m_itemName); } + ImGui::NewLine(); + ImGui::Text("Path"); + auto const vp = ImGui::GetContentRegionAvail(); + m_explorer.draw(sctx, {vp.x, vp.y - 50}); drawLastPageButtons(sctx); }); } @@ -165,8 +182,10 @@ void NewMenu::finish(StudioContext &sctx) noexcept { return; } auto const&im = *m_types[m_selectedType]; - auto const path = m_path.len() ? - im.itemPath(m_itemName, m_path) : im.itemPath(m_itemName); + if (auto p = m_explorer.selectedPath()) { + m_path = std::move(*p); + } + auto const path = sfmt("{}/{}.{}", m_path, m_itemName, im.fileExt()); if (sctx.project->exists(path)) { 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 30ebb685..3059c405 100644 --- a/src/olympic/studio/applib/src/newmenu.hpp +++ b/src/olympic/studio/applib/src/newmenu.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -19,7 +20,7 @@ class NewMenu final: public Popup { Closed, Opening, NewItemType, - NewItemName, + NewItemPath, NewItemTemplate, }; @@ -28,16 +29,18 @@ class NewMenu final: public Popup { private: Stage m_stage = Stage::Closed; + keel::Context &m_kctx; ox::String m_typeName; ox::IString<255> m_itemName; ox::String m_path; - ox::Vector> m_types; + ox::Vector> m_types; + FileExplorer m_explorer{m_kctx}; size_t m_selectedType = 0; size_t m_selectedTemplate = 0; bool m_open = false; public: - NewMenu() noexcept; + NewMenu(keel::Context &kctx) noexcept; void openPath(ox::StringParam path) noexcept; @@ -72,9 +75,9 @@ class NewMenu final: public Popup { private: void drawNewItemType(StudioContext const&sctx) noexcept; - void drawNewItemName(StudioContext &sctx) noexcept; + void drawNewItemPath(StudioContext &sctx) noexcept; - void drawNewItemTemplate(StudioContext &sctx) noexcept; + void drawNewItemTemplate(StudioContext const &sctx) noexcept; void drawButtons(Stage prev, Stage next) noexcept; diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index af527c87..48b45330 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -10,50 +10,12 @@ namespace studio { -static ox::Result> buildProjectTreeModel( - ProjectExplorer &explorer, - ox::StringParam name, - ox::StringView path, - FileTreeModel *parent) noexcept { - auto const fs = explorer.romFs(); - OX_REQUIRE(stat, fs->stat(path)); - 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()); - ox::Vector> outChildren; - for (auto const&childName : children) { - if (childName[0] != '.') { - auto const childPath = ox::sfmt("{}/{}", path, childName); - OX_REQUIRE_M(child, buildProjectTreeModel(explorer, childName, childPath, out.get())); - outChildren.emplace_back(std::move(child)); - } - } - out->setChildren(std::move(outChildren)); - } - return out; -} - - -ProjectExplorer::ProjectExplorer(turbine::Context &ctx) noexcept: m_ctx(ctx) { -} - -void ProjectExplorer::draw(StudioContext &ctx) noexcept { - auto const viewport = ImGui::GetContentRegionAvail(); - ImGui::BeginChild("ProjectExplorer", ImVec2(300, viewport.y), true); - ImGui::SetNextItemOpen(true); - if (m_treeModel) { - m_treeModel->draw(ctx.tctx); - } - ImGui::EndChild(); -} - -void ProjectExplorer::setModel(ox::UPtr &&model) noexcept { - m_treeModel = std::move(model); +ProjectExplorer::ProjectExplorer(keel::Context &kctx) noexcept: + FileExplorer{kctx, true} { } ox::Error ProjectExplorer::refreshProjectTreeModel(ox::StringViewCR) noexcept { - OX_REQUIRE_M(model, buildProjectTreeModel(*this, "Project", "/", nullptr)); + OX_REQUIRE_M(model, buildFileTreeModel(*this, "Project", "/", nullptr)); setModel(std::move(model)); return {}; } @@ -62,7 +24,7 @@ void ProjectExplorer::fileOpened(ox::StringViewCR path) const noexcept { fileChosen.emit(path); } -void ProjectExplorer::drawFileContextMenu(ox::StringViewCR path) const noexcept { +void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) { if (ImGui::MenuItem("Delete")) { deleteItem.emit(path); @@ -71,7 +33,7 @@ void ProjectExplorer::drawFileContextMenu(ox::StringViewCR path) const noexcept } } -void ProjectExplorer::drawDirContextMenu(ox::StringViewCR path) const noexcept { +void ProjectExplorer::dirContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) { if (ImGui::MenuItem("Add Item")) { addItem.emit(path); @@ -86,5 +48,4 @@ void ProjectExplorer::drawDirContextMenu(ox::StringViewCR path) const noexcept { } } - } diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index 73b6d583..75eea21e 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -12,10 +12,7 @@ namespace studio { -class ProjectExplorer final: public Widget, public FileExplorer { - private: - ox::UPtr m_treeModel; - turbine::Context &m_ctx; +class ProjectExplorer final: public FileExplorer { public: // slots @@ -24,25 +21,16 @@ class ProjectExplorer final: public Widget, public FileExplorer { ox::Signal addDir; ox::Signal deleteItem; - explicit ProjectExplorer(turbine::Context &ctx) noexcept; - - void draw(StudioContext &ctx) noexcept override; - - void setModel(ox::UPtr &&model) noexcept; + explicit ProjectExplorer(keel::Context &kctx) noexcept; ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept; - [[nodiscard]] - ox::FileSystem *romFs() noexcept { - return rom(m_ctx); - } - protected: void fileOpened(ox::StringViewCR path) const noexcept override; - void drawFileContextMenu(ox::StringViewCR path) const noexcept override; + void fileContextMenu(ox::StringViewCR path) const noexcept override; - void drawDirContextMenu(ox::StringViewCR path) const noexcept override; + void dirContextMenu(ox::StringViewCR path) const noexcept override; }; diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index f7e354b5..6cf063e3 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -50,7 +50,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_sctx(*this, ctx), m_tctx(ctx), m_projectDataDir(std::move(projectDataDir)), - m_projectExplorer(m_tctx), + m_projectExplorer(keelCtx(m_tctx)), m_newProject(m_projectDataDir), m_aboutPopup(m_tctx) { turbine::setApplicationData(m_tctx, &m_sctx); @@ -120,7 +120,8 @@ void StudioUI::draw() noexcept { ig::s_mainWinHasFocus = ImGui::IsWindowFocused( ImGuiFocusedFlags_RootAndChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); if (m_showProjectExplorer) { - m_projectExplorer.draw(m_sctx); + auto const v = ImGui::GetContentRegionAvail(); + m_projectExplorer.draw(m_sctx, {300, v.y}); ImGui::SameLine(); } drawTabBar(); diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index a10d8c88..6eee9f4f 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -39,7 +39,7 @@ class StudioUI: public ox::SignalHandler { BaseEditor *m_activeEditorOnLastDraw = nullptr; BaseEditor *m_activeEditor = nullptr; BaseEditor *m_activeEditorUpdatePending = nullptr; - NewMenu m_newMenu; + NewMenu m_newMenu{keelCtx(m_tctx)}; DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; NewProject m_newProject; diff --git a/src/olympic/studio/modlib/include/studio/filetreemodel.hpp b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp index d04c12cd..ed5cf0c2 100644 --- a/src/olympic/studio/modlib/include/studio/filetreemodel.hpp +++ b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp @@ -4,41 +4,96 @@ #pragma once +#include + #include #include #include +#include "widget.hpp" + namespace studio { -class FileExplorer { +constexpr void safeDelete(class FileTreeModel *m) noexcept; + +class FileExplorer: public ox::SignalHandler { + + friend class FileTreeModel; + + private: + keel::Context &m_kctx; + class FileTreeModel const *m_selected{}; + bool const m_fileDraggable{}; + ox::UPtr m_treeModel; public: + explicit FileExplorer(keel::Context &kctx, bool const fileDraggable = false) noexcept: + m_kctx{kctx}, + m_fileDraggable{fileDraggable} {} + virtual ~FileExplorer() = default; - virtual void fileOpened(ox::StringViewCR path) const noexcept = 0; + void draw(StudioContext &ctx, ImVec2 const &sz) const noexcept; - virtual void drawFileContextMenu(ox::StringViewCR path) const noexcept = 0; + void setModel(ox::UPtr &&model, bool selectRoot = false) noexcept; - virtual void drawDirContextMenu(ox::StringViewCR path) const noexcept = 0; + ox::Error setSelectedPath(ox::StringViewCR path) noexcept; + + [[nodiscard]] + ox::Optional selectedPath() const; + + virtual void fileOpened(ox::StringViewCR path) const noexcept; + + void drawFileContextMenu(ox::CStringViewCR path) const noexcept; + + void drawDirContextMenu(ox::CStringViewCR path) const noexcept; + + [[nodiscard]] + ox::FileSystem &romFs() const noexcept { + return *m_kctx.rom; + } + + protected: + virtual void fileContextMenu(ox::StringViewCR path) const noexcept; + + virtual void dirContextMenu(ox::StringViewCR path) const noexcept; + + void setSelectedNode(FileTreeModel const *const node) noexcept { + m_selected = node; + } + + [[nodiscard]] + bool selected(FileTreeModel const *const node) const noexcept { + return m_selected == node; + } + + [[nodiscard]] + bool fileDraggable() const noexcept { + return m_fileDraggable; + } + + private: + ox::Result setSelectedPath(ox::StringViewCR path, FileTreeModel const&node) noexcept; }; class FileTreeModel { private: FileExplorer &m_explorer; - FileTreeModel *m_parent = nullptr; ox::String m_name; - ox::String m_fullPath{m_parent ? m_parent->m_fullPath + "/" + m_name : ox::String{}}; - ox::String m_imguiNodeName{ox::sfmt("{}##{}", m_name, m_fullPath)}; + ox::String m_fullPath; ox::Vector> m_children; + ox::FileType const m_fileType{}; public: - virtual ~FileTreeModel() = default; - explicit FileTreeModel( - FileExplorer &explorer, ox::StringParam name, - FileTreeModel *parent = nullptr) noexcept; + FileExplorer &explorer, + ox::StringParam name, + ox::FileType fileType, + FileTreeModel const *parent = nullptr) noexcept; + + virtual ~FileTreeModel() = default; FileTreeModel(FileTreeModel &&other) noexcept = default; @@ -46,6 +101,38 @@ class FileTreeModel { void setChildren(ox::Vector> children) noexcept; + [[nodiscard]] + ox::Vector> const&children() const noexcept { + return m_children; + } + + [[nodiscard]] + bool isEmptyDir() const noexcept; + + [[nodiscard]] + ox::String const &path() const noexcept { + return m_fullPath; + } + }; +constexpr void safeDelete(FileTreeModel *m) noexcept { + delete m; +} + +ox::Result> buildFileTreeModel( + FileExplorer &explorer, + ox::StringParam name, + ox::StringViewCR path, + FileTreeModel *parent = nullptr, + std::function const&filter = + [](ox::StringViewCR, ox::FileStat const&) {return true;}, + bool showEmptyDirs = true) noexcept; + +ox::Result> buildFileTreeModel( + FileExplorer &explorer, + std::function const&filter = + [](ox::StringViewCR, ox::FileStat const&) {return true;}, + bool showEmptyDirs = true) noexcept; + } diff --git a/src/olympic/studio/modlib/include/studio/itemmaker.hpp b/src/olympic/studio/modlib/include/studio/itemmaker.hpp index 85a9e6ce..5aa6db19 100644 --- a/src/olympic/studio/modlib/include/studio/itemmaker.hpp +++ b/src/olympic/studio/modlib/include/studio/itemmaker.hpp @@ -95,9 +95,9 @@ class ItemMaker { public: constexpr ItemMaker( ox::StringParam pName, - ox::StringParam pParentDir, + ox::StringViewCR pParentDir, ox::StringParam pFileExt) noexcept: - m_parentDir{std::move(pParentDir)}, + m_parentDir{sfmt("/{}", pParentDir)}, m_fileExt{std::move(pFileExt)}, m_typeDisplayName{std::move(pName)} { } @@ -128,6 +128,11 @@ class ItemMaker { return m_templates; } + [[nodiscard]] + ox::String const&fileExt() const noexcept { + return m_fileExt; + } + [[nodiscard]] ox::String const&defaultPath() const noexcept { return m_parentDir; @@ -140,7 +145,7 @@ class ItemMaker { [[nodiscard]] ox::String itemPath(ox::StringViewCR pName) const noexcept { - return ox::sfmt("/{}/{}.{}", m_parentDir, pName, m_fileExt); + return ox::sfmt("{}/{}.{}", m_parentDir, pName, m_fileExt); } [[nodiscard]] @@ -196,12 +201,12 @@ class ItemMakerT final: public ItemMaker { public: constexpr ItemMakerT( ox::StringParam pDisplayName, - ox::StringParam pParentDir, + ox::StringViewCR pParentDir, ox::StringParam fileExt, ox::ClawFormat const pFmt = ox::ClawFormat::Metal) noexcept: ItemMaker( std::move(pDisplayName), - std::move(pParentDir), + pParentDir, std::move(fileExt)), m_fmt{pFmt} { installTemplate(ox::make_unique>()); @@ -209,13 +214,13 @@ class ItemMakerT final: public ItemMaker { constexpr ItemMakerT( ox::StringParam pDisplayName, - ox::StringParam pParentDir, + ox::StringViewCR pParentDir, ox::StringParam fileExt, T const&pItem, ox::ClawFormat const pFmt) noexcept: ItemMaker( std::move(pDisplayName), - std::move(pParentDir), + pParentDir, std::move(fileExt)), m_fmt{pFmt} { installTemplate(ox::make_unique>("Default", std::move(pItem))); @@ -223,13 +228,13 @@ class ItemMakerT final: public ItemMaker { constexpr ItemMakerT( ox::StringParam pDisplayName, - ox::StringParam pParentDir, + ox::StringViewCR pParentDir, ox::StringParam fileExt, T &&pItem, ox::ClawFormat const pFmt) noexcept: ItemMaker( std::move(pDisplayName), - std::move(pParentDir), + pParentDir, std::move(fileExt)), m_fmt{pFmt} { installTemplate(ox::make_unique>("Default", std::move(pItem))); diff --git a/src/olympic/studio/modlib/include/studio/projectfilepicker.hpp b/src/olympic/studio/modlib/include/studio/projectfilepicker.hpp new file mode 100644 index 00000000..95b22c95 --- /dev/null +++ b/src/olympic/studio/modlib/include/studio/projectfilepicker.hpp @@ -0,0 +1,16 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace studio { + +} \ No newline at end of file diff --git a/src/olympic/studio/modlib/include/studio/studio.hpp b/src/olympic/studio/modlib/include/studio/studio.hpp index cd747473..b9c20e4c 100644 --- a/src/olympic/studio/modlib/include/studio/studio.hpp +++ b/src/olympic/studio/modlib/include/studio/studio.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/src/olympic/studio/modlib/src/CMakeLists.txt b/src/olympic/studio/modlib/src/CMakeLists.txt index 795f7d72..b2463895 100644 --- a/src/olympic/studio/modlib/src/CMakeLists.txt +++ b/src/olympic/studio/modlib/src/CMakeLists.txt @@ -2,6 +2,7 @@ add_library( Studio configio.cpp editor.cpp + projectfilepicker.cpp filetreemodel.cpp imguiutil.cpp module.cpp diff --git a/src/olympic/studio/modlib/src/filetreemodel.cpp b/src/olympic/studio/modlib/src/filetreemodel.cpp index 2992bee1..549da57f 100644 --- a/src/olympic/studio/modlib/src/filetreemodel.cpp +++ b/src/olympic/studio/modlib/src/filetreemodel.cpp @@ -11,39 +11,106 @@ namespace studio { +void FileExplorer::draw(StudioContext &ctx, ImVec2 const &sz) const noexcept { + ImGui::BeginChild("ProjectExplorer", sz, true); + ImGui::SetNextItemOpen(true); + if (m_treeModel) { + m_treeModel->draw(ctx.tctx); + } + ImGui::EndChild(); +} + +void FileExplorer::setModel(ox::UPtr &&model, bool const selectRoot) noexcept { + m_treeModel = std::move(model); + setSelectedNode(selectRoot ? m_treeModel.get() : nullptr); +} + +ox::Error FileExplorer::setSelectedPath(ox::StringViewCR path) noexcept { + return setSelectedPath(path, *m_treeModel).error; +} + +[[nodiscard]] +ox::Optional FileExplorer::selectedPath() const { + if (m_selected) { + return ox::Optional{ox::in_place, m_selected->path()}; + } + return ox::Optional{}; +} + +void FileExplorer::fileOpened(ox::StringViewCR) const noexcept {} + +void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept { + ig::IDStackItem const idStackItem{path}; + fileContextMenu(path); +} + +void FileExplorer::drawDirContextMenu(ox::CStringViewCR path) const noexcept { + ig::IDStackItem const idStackItem{path}; + dirContextMenu(path); +} + +void FileExplorer::fileContextMenu(ox::StringViewCR) const noexcept {} + +void FileExplorer::dirContextMenu(ox::StringViewCR) const noexcept {} + +ox::Result FileExplorer::setSelectedPath( + ox::StringViewCR path, FileTreeModel const&node) noexcept { + if (path == node.path()) { + m_selected = &node; + return {}; + } + for (auto &c : node.children()) { + OX_REQUIRE(done, setSelectedPath(path, *c)); + if (done) { + break; + } + } + return {}; +} + + FileTreeModel::FileTreeModel( FileExplorer &explorer, ox::StringParam name, - FileTreeModel *parent) noexcept: + ox::FileType const fileType, + FileTreeModel const *const parent) noexcept: m_explorer{explorer}, - m_parent{parent}, - m_name{std::move(name)} { -} + m_name{std::move(name)}, + m_fullPath{parent ? sfmt("{}/{}", parent->m_fullPath, m_name) : ox::String{}}, + m_fileType{fileType} {} void FileTreeModel::draw(turbine::Context &tctx) const noexcept { constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + auto const selected = m_explorer.selected(this) ? ImGuiTreeNodeFlags_Selected : 0; if (!m_children.empty()) { - if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) { - m_explorer.drawDirContextMenu(m_fullPath); + auto const nodeOpen = ImGui::TreeNodeEx(m_name.c_str(), dirFlags | selected); + if (ImGui::IsItemClicked()) { + m_explorer.setSelectedNode(this); + } + ig::IDStackItem const idStackItem{m_name}; + m_explorer.drawDirContextMenu(m_fullPath); + if (nodeOpen) { for (auto const&child : m_children) { child->draw(tctx); } ImGui::TreePop(); - } else { - ig::IDStackItem const idStackItem{m_name}; - m_explorer.drawDirContextMenu(m_fullPath); } } else { - if (ImGui::TreeNodeEx(m_imguiNodeName.c_str(), ImGuiTreeNodeFlags_Leaf)) { + if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf | selected)) { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { m_explorer.fileOpened(m_fullPath); } + if (ImGui::IsItemClicked()) { + m_explorer.setSelectedNode(this); + } m_explorer.drawFileContextMenu(m_fullPath); ImGui::TreePop(); - std::ignore = ig::dragDropSource([this] { - ImGui::Text("%s", m_name.c_str()); - return ig::setDragDropPayload("FileRef", FileRef{ox::String{m_fullPath}}); - }); + if (m_explorer.fileDraggable()) { + std::ignore = ig::dragDropSource([this] { + ImGui::Text("%s", m_name.c_str()); + return ig::setDragDropPayload("FileRef", FileRef{ox::String{m_fullPath}}); + }); + } } } } @@ -52,4 +119,54 @@ void FileTreeModel::setChildren(ox::Vector> children) no m_children = std::move(children); } +bool FileTreeModel::isEmptyDir() const noexcept { + return m_children.empty() && m_fileType == ox::FileType::Directory; +} + + +ox::Result> buildFileTreeModel( + FileExplorer &explorer, + ox::StringParam name, + ox::StringViewCR path, + FileTreeModel *parent, + std::function const &filter, + bool const showEmptyDirs) noexcept { + auto const&fs = explorer.romFs(); + OX_REQUIRE(stat, fs.stat(path)); + auto out = ox::make_unique(explorer, std::move(name), stat.fileType, parent); + if (stat.fileType == ox::FileType::Directory) { + OX_REQUIRE_M(children, fs.ls(path)); + std::sort(children.begin(), children.end()); + ox::Vector> outChildren; + for (auto const&childName : children) { + if (childName[0] != '.') { + auto const childPath = ox::sfmt("{}/{}", path, childName); + OX_REQUIRE(childStat, fs.stat(childPath)); + if (filter(childPath, childStat)) { + OX_REQUIRE_M(child, buildFileTreeModel( + explorer, + childName, + childPath, + out.get(), + filter, + showEmptyDirs)); + auto const emptyDir = child->isEmptyDir(); + if (!emptyDir || showEmptyDirs) { + outChildren.emplace_back(std::move(child)); + } + } + } + } + out->setChildren(std::move(outChildren)); + } + return out; +} + +ox::Result> buildFileTreeModel( + FileExplorer &explorer, + std::function const&filter, + bool const showEmptyDirs) noexcept { + return buildFileTreeModel(explorer, "Project", "/", nullptr, filter, showEmptyDirs); +} + } diff --git a/src/olympic/studio/modlib/src/projectfilepicker.cpp b/src/olympic/studio/modlib/src/projectfilepicker.cpp new file mode 100644 index 00000000..dbccb7df --- /dev/null +++ b/src/olympic/studio/modlib/src/projectfilepicker.cpp @@ -0,0 +1,18 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include +#include + +namespace studio { + +class ProjectFilePicker: public FileExplorer { + + public: + explicit ProjectFilePicker(keel::Context &kctx) noexcept: FileExplorer{kctx} {} + + protected: +}; + +}