[studio] Add a file explorer to NewMenu to choose where new files go
All checks were successful
Build / build (push) Successful in 4m16s

This commit is contained in:
Gary Talent 2025-01-22 23:11:08 -06:00
parent d15a0df7da
commit 1cc1d561e2
13 changed files with 329 additions and 112 deletions

View File

@ -12,9 +12,8 @@
namespace studio { namespace studio {
NewMenu::NewMenu() noexcept { NewMenu::NewMenu(keel::Context &kctx) noexcept: m_kctx{kctx} {
setTitle("New Item"); setTitle("New Item");
setSize({280, 180});
} }
void NewMenu::open() noexcept { void NewMenu::open() noexcept {
@ -23,6 +22,11 @@ void NewMenu::open() noexcept {
m_itemName = ""; m_itemName = "";
m_typeName = ""; m_typeName = "";
m_path = ""; m_path = "";
m_explorer.setModel(buildFileTreeModel(
m_explorer,
[](ox::StringViewCR, ox::FileStat const&s) {
return s.fileType == ox::FileType::Directory;
}).or_value(ox::UPtr<FileTreeModel>{}));
} }
void NewMenu::openPath(ox::StringParam path) noexcept { void NewMenu::openPath(ox::StringParam path) noexcept {
@ -49,8 +53,8 @@ void NewMenu::draw(StudioContext &sctx) noexcept {
case Stage::NewItemType: case Stage::NewItemType:
drawNewItemType(sctx); drawNewItemType(sctx);
break; break;
case Stage::NewItemName: case Stage::NewItemPath:
drawNewItemName(sctx); drawNewItemPath(sctx);
break; break;
case Stage::NewItemTemplate: case Stage::NewItemTemplate:
drawNewItemTemplate(sctx); drawNewItemTemplate(sctx);
@ -79,32 +83,45 @@ void NewMenu::installItemTemplate(ox::UPtr<ItemTemplate> &tmplt) noexcept {
} }
void NewMenu::drawNewItemType(StudioContext const&sctx) noexcept { void NewMenu::drawNewItemType(StudioContext const&sctx) noexcept {
setSize({280, 180});
drawWindow(sctx.tctx, m_open, [this] { drawWindow(sctx.tctx, m_open, [this] {
ig::ListBox("Item Type", [&](size_t const i) -> ox::CStringView { ig::ListBox("Item Type", [&](size_t const i) -> ox::CStringView {
return m_types[i]->typeDisplayName(); return m_types[i]->typeDisplayName();
}, m_types.size(), m_selectedType, {200, 100}); }, m_types.size(), m_selectedType, {200, 100});
auto const&im = *m_types[m_selectedType]; auto const&im = *m_types[m_selectedType];
drawFirstPageButtons(im.itemTemplates().size() == 1 ? 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] { drawWindow(sctx.tctx, m_open, [this] {
auto const&templates = auto const&templates =
m_types[m_selectedType]->itemTemplates(); m_types[m_selectedType]->itemTemplates();
ig::ListBox("Template", [&](size_t const i) -> ox::CStringView { ig::ListBox("Template", [&](size_t const i) -> ox::CStringView {
return templates[i]->name(); return templates[i]->name();
}, templates.size(), m_selectedTemplate, {200, 100}); }, 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] { drawWindow(sctx.tctx, m_open, [this, &sctx] {
if (m_selectedType < m_types.size()) { if (m_selectedType < m_types.size()) {
ig::InputText("Name", m_itemName); 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); drawLastPageButtons(sctx);
}); });
} }
@ -165,8 +182,10 @@ void NewMenu::finish(StudioContext &sctx) noexcept {
return; return;
} }
auto const&im = *m_types[m_selectedType]; auto const&im = *m_types[m_selectedType];
auto const path = m_path.len() ? if (auto p = m_explorer.selectedPath()) {
im.itemPath(m_itemName, m_path) : im.itemPath(m_itemName); m_path = std::move(*p);
}
auto const path = sfmt("{}/{}.{}", m_path, m_itemName, im.fileExt());
if (sctx.project->exists(path)) { if (sctx.project->exists(path)) {
oxLogError(ox::Error{1, "New file error: file already exists"}); oxLogError(ox::Error{1, "New file error: file already exists"});
return; return;

View File

@ -8,6 +8,7 @@
#include <ox/event/signal.hpp> #include <ox/event/signal.hpp>
#include <ox/std/string.hpp> #include <ox/std/string.hpp>
#include <studio/filetreemodel.hpp>
#include <studio/itemmaker.hpp> #include <studio/itemmaker.hpp>
#include <studio/popup.hpp> #include <studio/popup.hpp>
@ -19,7 +20,7 @@ class NewMenu final: public Popup {
Closed, Closed,
Opening, Opening,
NewItemType, NewItemType,
NewItemName, NewItemPath,
NewItemTemplate, NewItemTemplate,
}; };
@ -28,16 +29,18 @@ class NewMenu final: public Popup {
private: private:
Stage m_stage = Stage::Closed; Stage m_stage = Stage::Closed;
keel::Context &m_kctx;
ox::String m_typeName; ox::String m_typeName;
ox::IString<255> m_itemName; ox::IString<255> m_itemName;
ox::String m_path; ox::String m_path;
ox::Vector<ox::UPtr<studio::ItemMaker>> m_types; ox::Vector<ox::UPtr<ItemMaker>> m_types;
FileExplorer m_explorer{m_kctx};
size_t m_selectedType = 0; size_t m_selectedType = 0;
size_t m_selectedTemplate = 0; size_t m_selectedTemplate = 0;
bool m_open = false; bool m_open = false;
public: public:
NewMenu() noexcept; NewMenu(keel::Context &kctx) noexcept;
void openPath(ox::StringParam path) noexcept; void openPath(ox::StringParam path) noexcept;
@ -72,9 +75,9 @@ class NewMenu final: public Popup {
private: private:
void drawNewItemType(StudioContext const&sctx) noexcept; 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; void drawButtons(Stage prev, Stage next) noexcept;

View File

@ -10,50 +10,12 @@
namespace studio { namespace studio {
static ox::Result<ox::UniquePtr<FileTreeModel>> buildProjectTreeModel( ProjectExplorer::ProjectExplorer(keel::Context &kctx) noexcept:
ProjectExplorer &explorer, FileExplorer{kctx, true} {
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<FileTreeModel>(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<ox::UniquePtr<FileTreeModel>> 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<FileTreeModel> &&model) noexcept {
m_treeModel = std::move(model);
} }
ox::Error ProjectExplorer::refreshProjectTreeModel(ox::StringViewCR) noexcept { 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)); setModel(std::move(model));
return {}; return {};
} }
@ -62,7 +24,7 @@ void ProjectExplorer::fileOpened(ox::StringViewCR path) const noexcept {
fileChosen.emit(path); 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::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Delete")) { if (ImGui::MenuItem("Delete")) {
deleteItem.emit(path); 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::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Add Item")) { if (ImGui::MenuItem("Add Item")) {
addItem.emit(path); addItem.emit(path);
@ -86,5 +48,4 @@ void ProjectExplorer::drawDirContextMenu(ox::StringViewCR path) const noexcept {
} }
} }
} }

View File

@ -12,10 +12,7 @@
namespace studio { namespace studio {
class ProjectExplorer final: public Widget, public FileExplorer { class ProjectExplorer final: public FileExplorer {
private:
ox::UPtr<FileTreeModel> m_treeModel;
turbine::Context &m_ctx;
public: public:
// slots // slots
@ -24,25 +21,16 @@ class ProjectExplorer final: public Widget, public FileExplorer {
ox::Signal<ox::Error(ox::StringViewCR)> addDir; ox::Signal<ox::Error(ox::StringViewCR)> addDir;
ox::Signal<ox::Error(ox::StringViewCR)> deleteItem; ox::Signal<ox::Error(ox::StringViewCR)> deleteItem;
explicit ProjectExplorer(turbine::Context &ctx) noexcept; explicit ProjectExplorer(keel::Context &kctx) noexcept;
void draw(StudioContext &ctx) noexcept override;
void setModel(ox::UPtr<FileTreeModel> &&model) noexcept;
ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept; ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept;
[[nodiscard]]
ox::FileSystem *romFs() noexcept {
return rom(m_ctx);
}
protected: protected:
void fileOpened(ox::StringViewCR path) const noexcept override; 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;
}; };

View File

@ -50,7 +50,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
m_sctx(*this, ctx), m_sctx(*this, ctx),
m_tctx(ctx), m_tctx(ctx),
m_projectDataDir(std::move(projectDataDir)), m_projectDataDir(std::move(projectDataDir)),
m_projectExplorer(m_tctx), m_projectExplorer(keelCtx(m_tctx)),
m_newProject(m_projectDataDir), m_newProject(m_projectDataDir),
m_aboutPopup(m_tctx) { m_aboutPopup(m_tctx) {
turbine::setApplicationData(m_tctx, &m_sctx); turbine::setApplicationData(m_tctx, &m_sctx);
@ -120,7 +120,8 @@ void StudioUI::draw() noexcept {
ig::s_mainWinHasFocus = ImGui::IsWindowFocused( ig::s_mainWinHasFocus = ImGui::IsWindowFocused(
ImGuiFocusedFlags_RootAndChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); ImGuiFocusedFlags_RootAndChildWindows | ImGuiFocusedFlags_NoPopupHierarchy);
if (m_showProjectExplorer) { if (m_showProjectExplorer) {
m_projectExplorer.draw(m_sctx); auto const v = ImGui::GetContentRegionAvail();
m_projectExplorer.draw(m_sctx, {300, v.y});
ImGui::SameLine(); ImGui::SameLine();
} }
drawTabBar(); drawTabBar();

View File

@ -39,7 +39,7 @@ class StudioUI: public ox::SignalHandler {
BaseEditor *m_activeEditorOnLastDraw = nullptr; BaseEditor *m_activeEditorOnLastDraw = nullptr;
BaseEditor *m_activeEditor = nullptr; BaseEditor *m_activeEditor = nullptr;
BaseEditor *m_activeEditorUpdatePending = nullptr; BaseEditor *m_activeEditorUpdatePending = nullptr;
NewMenu m_newMenu; NewMenu m_newMenu{keelCtx(m_tctx)};
DeleteConfirmation m_deleteConfirmation; DeleteConfirmation m_deleteConfirmation;
NewDir m_newDirDialog; NewDir m_newDirDialog;
NewProject m_newProject; NewProject m_newProject;

View File

@ -4,41 +4,96 @@
#pragma once #pragma once
#include <algorithm>
#include <ox/std/memory.hpp> #include <ox/std/memory.hpp>
#include <ox/std/string.hpp> #include <ox/std/string.hpp>
#include <turbine/context.hpp> #include <turbine/context.hpp>
#include "widget.hpp"
namespace studio { 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<FileTreeModel> m_treeModel;
public: public:
explicit FileExplorer(keel::Context &kctx, bool const fileDraggable = false) noexcept:
m_kctx{kctx},
m_fileDraggable{fileDraggable} {}
virtual ~FileExplorer() = default; 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<FileTreeModel> &&model, bool selectRoot = false) noexcept;
virtual void drawDirContextMenu(ox::StringViewCR path) const noexcept = 0; ox::Error setSelectedPath(ox::StringViewCR path) noexcept;
[[nodiscard]]
ox::Optional<ox::String> 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<bool> setSelectedPath(ox::StringViewCR path, FileTreeModel const&node) noexcept;
}; };
class FileTreeModel { class FileTreeModel {
private: private:
FileExplorer &m_explorer; FileExplorer &m_explorer;
FileTreeModel *m_parent = nullptr;
ox::String m_name; ox::String m_name;
ox::String m_fullPath{m_parent ? m_parent->m_fullPath + "/" + m_name : ox::String{}}; ox::String m_fullPath;
ox::String m_imguiNodeName{ox::sfmt("{}##{}", m_name, m_fullPath)};
ox::Vector<ox::UPtr<FileTreeModel>> m_children; ox::Vector<ox::UPtr<FileTreeModel>> m_children;
ox::FileType const m_fileType{};
public: public:
virtual ~FileTreeModel() = default;
explicit FileTreeModel( explicit FileTreeModel(
FileExplorer &explorer, ox::StringParam name, FileExplorer &explorer,
FileTreeModel *parent = nullptr) noexcept; ox::StringParam name,
ox::FileType fileType,
FileTreeModel const *parent = nullptr) noexcept;
virtual ~FileTreeModel() = default;
FileTreeModel(FileTreeModel &&other) noexcept = default; FileTreeModel(FileTreeModel &&other) noexcept = default;
@ -46,6 +101,38 @@ class FileTreeModel {
void setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept; void setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept;
[[nodiscard]]
ox::Vector<ox::UPtr<FileTreeModel>> 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<ox::UPtr<FileTreeModel>> buildFileTreeModel(
FileExplorer &explorer,
ox::StringParam name,
ox::StringViewCR path,
FileTreeModel *parent = nullptr,
std::function<bool(ox::StringViewCR, ox::FileStat const&)> const&filter =
[](ox::StringViewCR, ox::FileStat const&) {return true;},
bool showEmptyDirs = true) noexcept;
ox::Result<ox::UPtr<FileTreeModel>> buildFileTreeModel(
FileExplorer &explorer,
std::function<bool(ox::StringViewCR, ox::FileStat const&)> const&filter =
[](ox::StringViewCR, ox::FileStat const&) {return true;},
bool showEmptyDirs = true) noexcept;
} }

View File

@ -95,9 +95,9 @@ class ItemMaker {
public: public:
constexpr ItemMaker( constexpr ItemMaker(
ox::StringParam pName, ox::StringParam pName,
ox::StringParam pParentDir, ox::StringViewCR pParentDir,
ox::StringParam pFileExt) noexcept: ox::StringParam pFileExt) noexcept:
m_parentDir{std::move(pParentDir)}, m_parentDir{sfmt("/{}", pParentDir)},
m_fileExt{std::move(pFileExt)}, m_fileExt{std::move(pFileExt)},
m_typeDisplayName{std::move(pName)} { m_typeDisplayName{std::move(pName)} {
} }
@ -128,6 +128,11 @@ class ItemMaker {
return m_templates; return m_templates;
} }
[[nodiscard]]
ox::String const&fileExt() const noexcept {
return m_fileExt;
}
[[nodiscard]] [[nodiscard]]
ox::String const&defaultPath() const noexcept { ox::String const&defaultPath() const noexcept {
return m_parentDir; return m_parentDir;
@ -140,7 +145,7 @@ class ItemMaker {
[[nodiscard]] [[nodiscard]]
ox::String itemPath(ox::StringViewCR pName) const noexcept { 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]] [[nodiscard]]
@ -196,12 +201,12 @@ class ItemMakerT final: public ItemMaker {
public: public:
constexpr ItemMakerT( constexpr ItemMakerT(
ox::StringParam pDisplayName, ox::StringParam pDisplayName,
ox::StringParam pParentDir, ox::StringViewCR pParentDir,
ox::StringParam fileExt, ox::StringParam fileExt,
ox::ClawFormat const pFmt = ox::ClawFormat::Metal) noexcept: ox::ClawFormat const pFmt = ox::ClawFormat::Metal) noexcept:
ItemMaker( ItemMaker(
std::move(pDisplayName), std::move(pDisplayName),
std::move(pParentDir), pParentDir,
std::move(fileExt)), std::move(fileExt)),
m_fmt{pFmt} { m_fmt{pFmt} {
installTemplate(ox::make_unique<ItemTemplateT<T>>()); installTemplate(ox::make_unique<ItemTemplateT<T>>());
@ -209,13 +214,13 @@ class ItemMakerT final: public ItemMaker {
constexpr ItemMakerT( constexpr ItemMakerT(
ox::StringParam pDisplayName, ox::StringParam pDisplayName,
ox::StringParam pParentDir, ox::StringViewCR pParentDir,
ox::StringParam fileExt, ox::StringParam fileExt,
T const&pItem, T const&pItem,
ox::ClawFormat const pFmt) noexcept: ox::ClawFormat const pFmt) noexcept:
ItemMaker( ItemMaker(
std::move(pDisplayName), std::move(pDisplayName),
std::move(pParentDir), pParentDir,
std::move(fileExt)), std::move(fileExt)),
m_fmt{pFmt} { m_fmt{pFmt} {
installTemplate(ox::make_unique<ItemTemplateT<T>>("Default", std::move(pItem))); installTemplate(ox::make_unique<ItemTemplateT<T>>("Default", std::move(pItem)));
@ -223,13 +228,13 @@ class ItemMakerT final: public ItemMaker {
constexpr ItemMakerT( constexpr ItemMakerT(
ox::StringParam pDisplayName, ox::StringParam pDisplayName,
ox::StringParam pParentDir, ox::StringViewCR pParentDir,
ox::StringParam fileExt, ox::StringParam fileExt,
T &&pItem, T &&pItem,
ox::ClawFormat const pFmt) noexcept: ox::ClawFormat const pFmt) noexcept:
ItemMaker( ItemMaker(
std::move(pDisplayName), std::move(pDisplayName),
std::move(pParentDir), pParentDir,
std::move(fileExt)), std::move(fileExt)),
m_fmt{pFmt} { m_fmt{pFmt} {
installTemplate(ox::make_unique<ItemTemplateT<T>>("Default", std::move(pItem))); installTemplate(ox::make_unique<ItemTemplateT<T>>("Default", std::move(pItem)));

View File

@ -0,0 +1,16 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
#include <studio/filetreemodel.hpp>
namespace studio {
}

View File

@ -9,6 +9,7 @@
#include <studio/dragdrop.hpp> #include <studio/dragdrop.hpp>
#include <studio/editor.hpp> #include <studio/editor.hpp>
#include <studio/filedialog.hpp> #include <studio/filedialog.hpp>
#include <studio/filetreemodel.hpp>
#include <studio/imguiutil.hpp> #include <studio/imguiutil.hpp>
#include <studio/module.hpp> #include <studio/module.hpp>
#include <studio/itemmaker.hpp> #include <studio/itemmaker.hpp>

View File

@ -2,6 +2,7 @@ add_library(
Studio Studio
configio.cpp configio.cpp
editor.cpp editor.cpp
projectfilepicker.cpp
filetreemodel.cpp filetreemodel.cpp
imguiutil.cpp imguiutil.cpp
module.cpp module.cpp

View File

@ -11,35 +11,101 @@
namespace studio { 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<FileTreeModel> &&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<ox::String> FileExplorer::selectedPath() const {
if (m_selected) {
return ox::Optional<ox::String>{ox::in_place, m_selected->path()};
}
return ox::Optional<ox::String>{};
}
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<bool> 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( FileTreeModel::FileTreeModel(
FileExplorer &explorer, FileExplorer &explorer,
ox::StringParam name, ox::StringParam name,
FileTreeModel *parent) noexcept: ox::FileType const fileType,
FileTreeModel const *const parent) noexcept:
m_explorer{explorer}, 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 { void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
auto const selected = m_explorer.selected(this) ? ImGuiTreeNodeFlags_Selected : 0;
if (!m_children.empty()) { if (!m_children.empty()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) { 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); m_explorer.drawDirContextMenu(m_fullPath);
if (nodeOpen) {
for (auto const&child : m_children) { for (auto const&child : m_children) {
child->draw(tctx); child->draw(tctx);
} }
ImGui::TreePop(); ImGui::TreePop();
} else {
ig::IDStackItem const idStackItem{m_name};
m_explorer.drawDirContextMenu(m_fullPath);
} }
} else { } 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)) { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
m_explorer.fileOpened(m_fullPath); m_explorer.fileOpened(m_fullPath);
} }
if (ImGui::IsItemClicked()) {
m_explorer.setSelectedNode(this);
}
m_explorer.drawFileContextMenu(m_fullPath); m_explorer.drawFileContextMenu(m_fullPath);
ImGui::TreePop(); ImGui::TreePop();
if (m_explorer.fileDraggable()) {
std::ignore = ig::dragDropSource([this] { std::ignore = ig::dragDropSource([this] {
ImGui::Text("%s", m_name.c_str()); ImGui::Text("%s", m_name.c_str());
return ig::setDragDropPayload("FileRef", FileRef{ox::String{m_fullPath}}); return ig::setDragDropPayload("FileRef", FileRef{ox::String{m_fullPath}});
@ -47,9 +113,60 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
} }
} }
} }
}
void FileTreeModel::setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept { void FileTreeModel::setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept {
m_children = std::move(children); m_children = std::move(children);
} }
bool FileTreeModel::isEmptyDir() const noexcept {
return m_children.empty() && m_fileType == ox::FileType::Directory;
}
ox::Result<ox::UPtr<FileTreeModel>> buildFileTreeModel(
FileExplorer &explorer,
ox::StringParam name,
ox::StringViewCR path,
FileTreeModel *parent,
std::function<bool(ox::StringViewCR, ox::FileStat const&)> const &filter,
bool const showEmptyDirs) noexcept {
auto const&fs = explorer.romFs();
OX_REQUIRE(stat, fs.stat(path));
auto out = ox::make_unique<FileTreeModel>(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<ox::UPtr<FileTreeModel>> 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<ox::UPtr<FileTreeModel>> buildFileTreeModel(
FileExplorer &explorer,
std::function<bool(ox::StringViewCR, ox::FileStat const&)> const&filter,
bool const showEmptyDirs) noexcept {
return buildFileTreeModel(explorer, "Project", "/", nullptr, filter, showEmptyDirs);
}
} }

View File

@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/filetreemodel.hpp>
#include <studio/projectfilepicker.hpp>
namespace studio {
class ProjectFilePicker: public FileExplorer {
public:
explicit ProjectFilePicker(keel::Context &kctx) noexcept: FileExplorer{kctx} {}
protected:
};
}