[studio] Add a file explorer to NewMenu to choose where new files go
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build / build (push) Successful in 4m16s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build / build (push) Successful in 4m16s
				
			This commit is contained in:
		@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								src/olympic/studio/modlib/src/projectfilepicker.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/olympic/studio/modlib/src/projectfilepicker.cpp
									
									
									
									
									
										Normal 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:
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user