From d15a0df7dad1f96cbdb9e5042abe0f0668ef23e4 Mon Sep 17 00:00:00 2001
From: Gary Talent <gary@drinkingtea.net>
Date: Wed, 22 Jan 2025 01:04:25 -0600
Subject: [PATCH] [studio] Make reusable FileTreeModel

---
 src/olympic/studio/applib/src/CMakeLists.txt  |  1 -
 .../studio/applib/src/projectexplorer.cpp     | 40 +++++++--
 .../studio/applib/src/projectexplorer.hpp     | 15 +++-
 .../studio/applib/src/projecttreemodel.cpp    | 85 -------------------
 .../studio/applib/src/projecttreemodel.hpp    | 46 ----------
 src/olympic/studio/applib/src/studioapp.hpp   |  1 -
 .../modlib/include/studio/filetreemodel.hpp   | 51 +++++++++++
 src/olympic/studio/modlib/src/CMakeLists.txt  |  1 +
 .../studio/modlib/src/filetreemodel.cpp       | 55 ++++++++++++
 9 files changed, 153 insertions(+), 142 deletions(-)
 delete mode 100644 src/olympic/studio/applib/src/projecttreemodel.cpp
 delete mode 100644 src/olympic/studio/applib/src/projecttreemodel.hpp
 create mode 100644 src/olympic/studio/modlib/include/studio/filetreemodel.hpp
 create mode 100644 src/olympic/studio/modlib/src/filetreemodel.cpp

diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt
index 2127d8fe..f69e669b 100644
--- a/src/olympic/studio/applib/src/CMakeLists.txt
+++ b/src/olympic/studio/applib/src/CMakeLists.txt
@@ -9,7 +9,6 @@ add_library(
 		newmenu.cpp
 		newproject.cpp
 		projectexplorer.cpp
-		projecttreemodel.cpp
 		studioapp.cpp
 )
 target_compile_definitions(
diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp
index 464f6718..af527c87 100644
--- a/src/olympic/studio/applib/src/projectexplorer.cpp
+++ b/src/olympic/studio/applib/src/projectexplorer.cpp
@@ -10,18 +10,18 @@
 
 namespace studio {
 
-static ox::Result<ox::UniquePtr<ProjectTreeModel>> buildProjectTreeModel(
+static ox::Result<ox::UniquePtr<FileTreeModel>> buildProjectTreeModel(
 		ProjectExplorer &explorer,
 		ox::StringParam name,
 		ox::StringView path,
-		ProjectTreeModel *parent) noexcept {
+		FileTreeModel *parent) noexcept {
 	auto const fs = explorer.romFs();
 	OX_REQUIRE(stat, fs->stat(path));
-	auto out = ox::make_unique<ProjectTreeModel>(explorer, std::move(name), parent);
+	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<ProjectTreeModel>> outChildren;
+		ox::Vector<ox::UniquePtr<FileTreeModel>> outChildren;
 		for (auto const&childName : children) {
 			if (childName[0] != '.') {
 				auto const childPath = ox::sfmt("{}/{}", path, childName);
@@ -34,6 +34,7 @@ static ox::Result<ox::UniquePtr<ProjectTreeModel>> buildProjectTreeModel(
 	return out;
 }
 
+
 ProjectExplorer::ProjectExplorer(turbine::Context &ctx) noexcept: m_ctx(ctx) {
 }
 
@@ -47,7 +48,7 @@ void ProjectExplorer::draw(StudioContext &ctx) noexcept {
 	ImGui::EndChild();
 }
 
-void ProjectExplorer::setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept {
+void ProjectExplorer::setModel(ox::UPtr<FileTreeModel> &&model) noexcept {
 	m_treeModel = std::move(model);
 }
 
@@ -57,4 +58,33 @@ ox::Error ProjectExplorer::refreshProjectTreeModel(ox::StringViewCR) noexcept {
 	return {};
 }
 
+void ProjectExplorer::fileOpened(ox::StringViewCR path) const noexcept {
+	fileChosen.emit(path);
+}
+
+void ProjectExplorer::drawFileContextMenu(ox::StringViewCR path) const noexcept {
+	if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
+		if (ImGui::MenuItem("Delete")) {
+			deleteItem.emit(path);
+		}
+		ImGui::EndPopup();
+	}
+}
+
+void ProjectExplorer::drawDirContextMenu(ox::StringViewCR path) const noexcept {
+    if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
+        if (ImGui::MenuItem("Add Item")) {
+            addItem.emit(path);
+        }
+        if (ImGui::MenuItem("Add Directory")) {
+            addDir.emit(path);
+        }
+		if (ImGui::MenuItem("Delete")) {
+			deleteItem.emit(path);
+		}
+        ImGui::EndPopup();
+    }
+}
+
+
 }
diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp
index 8ef25761..73b6d583 100644
--- a/src/olympic/studio/applib/src/projectexplorer.hpp
+++ b/src/olympic/studio/applib/src/projectexplorer.hpp
@@ -7,14 +7,14 @@
 #include <ox/event/signal.hpp>
 #include <ox/std/memory.hpp>
 
+#include <studio/filetreemodel.hpp>
 #include <studio/widget.hpp>
-#include "projecttreemodel.hpp"
 
 namespace studio {
 
-class ProjectExplorer: public Widget {
+class ProjectExplorer final: public Widget, public FileExplorer {
 	private:
-		ox::UPtr<ProjectTreeModel> m_treeModel;
+		ox::UPtr<FileTreeModel> m_treeModel;
 		turbine::Context &m_ctx;
 
 	public:
@@ -28,7 +28,7 @@ class ProjectExplorer: public Widget {
 
 		void draw(StudioContext &ctx) noexcept override;
 
-		void setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept;
+		void setModel(ox::UPtr<FileTreeModel> &&model) noexcept;
 
 		ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept;
 
@@ -37,6 +37,13 @@ class ProjectExplorer: public Widget {
 			return rom(m_ctx);
 		}
 
+	protected:
+		void fileOpened(ox::StringViewCR path) const noexcept override;
+
+		void drawFileContextMenu(ox::StringViewCR path) const noexcept override;
+
+		void drawDirContextMenu(ox::StringViewCR path) const noexcept override;
+
 };
 
 }
diff --git a/src/olympic/studio/applib/src/projecttreemodel.cpp b/src/olympic/studio/applib/src/projecttreemodel.cpp
deleted file mode 100644
index cc7afd1a..00000000
--- a/src/olympic/studio/applib/src/projecttreemodel.cpp
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
- */
-
-#include <imgui.h>
-
-#include <studio/dragdrop.hpp>
-#include <studio/imguiutil.hpp>
-
-#include "projectexplorer.hpp"
-#include "projecttreemodel.hpp"
-
-namespace studio {
-
-ProjectTreeModel::ProjectTreeModel(
-	ProjectExplorer &explorer,
-	ox::StringParam name,
-	ProjectTreeModel *parent) noexcept:
-	m_explorer(explorer),
-	m_parent(parent),
-	m_name(std::move(name)) {
-}
-
-ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept:
-	m_explorer(other.m_explorer),
-	m_parent(other.m_parent),
-	m_name(std::move(other.m_name)),
-	m_children(std::move(other.m_children)) {
-}
-
-void ProjectTreeModel::draw(turbine::Context &tctx) const noexcept {
-	constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
-	if (!m_children.empty()) {
-		if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
-            drawDirContextMenu();
-			for (auto const&child : m_children) {
-				child->draw(tctx);
-			}
-			ImGui::TreePop();
-		} else {
-			ig::IDStackItem const idStackItem{m_name};
-            drawDirContextMenu();
-		}
-	} else {
-		auto const path = fullPath();
-		auto const name = ox::sfmt<ox::BasicString<255>>("{}##{}", m_name, path);
-		if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
-			if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
-				m_explorer.fileChosen.emit(path);
-			}
-			if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
-				if (ImGui::MenuItem("Delete")) {
-					m_explorer.deleteItem.emit(path);
-				}
-				ImGui::EndPopup();
-			}
-			ImGui::TreePop();
-			std::ignore = ig::dragDropSource([this] {
-				ImGui::Text("%s", m_name.c_str());
-				return ig::setDragDropPayload("FileRef", FileRef{fullPath<ox::String>()});
-			});
-		}
-	}
-}
-
-void ProjectTreeModel::setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept {
-	m_children = std::move(children);
-}
-
-void ProjectTreeModel::drawDirContextMenu() const noexcept {
-    if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
-        if (ImGui::MenuItem("Add Item")) {
-            m_explorer.addItem.emit(fullPath());
-        }
-        if (ImGui::MenuItem("Add Directory")) {
-            m_explorer.addDir.emit(fullPath());
-        }
-		if (ImGui::MenuItem("Delete")) {
-			m_explorer.deleteItem.emit(fullPath());
-		}
-        ImGui::EndPopup();
-    }
-}
-
-}
diff --git a/src/olympic/studio/applib/src/projecttreemodel.hpp b/src/olympic/studio/applib/src/projecttreemodel.hpp
deleted file mode 100644
index fdfc1fae..00000000
--- a/src/olympic/studio/applib/src/projecttreemodel.hpp
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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>
-
-namespace studio {
-
-class ProjectTreeModel {
-	private:
-		class ProjectExplorer &m_explorer;
-		ProjectTreeModel *m_parent = nullptr;
-		ox::String m_name;
-		ox::Vector<ox::UPtr<ProjectTreeModel>> m_children;
-
-	public:
-		explicit ProjectTreeModel(
-			ProjectExplorer &explorer, ox::StringParam name,
-			ProjectTreeModel *parent = nullptr) noexcept;
-
-		ProjectTreeModel(ProjectTreeModel &&other) noexcept;
-
-		void draw(turbine::Context &tctx) const noexcept;
-
-		void setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept;
-
-	private:
-		void drawDirContextMenu() const noexcept;
-
-		template<typename Str = ox::BasicString<255>>
-		[[nodiscard]]
-		Str fullPath() const noexcept {
-			if (m_parent) {
-				return m_parent->fullPath<Str>() + "/" + m_name;
-			}
-			return {};
-		}
-
-};
-
-}
diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp
index 199feac2..a10d8c88 100644
--- a/src/olympic/studio/applib/src/studioapp.hpp
+++ b/src/olympic/studio/applib/src/studioapp.hpp
@@ -19,7 +19,6 @@
 #include "newmenu.hpp"
 #include "newproject.hpp"
 #include "projectexplorer.hpp"
-#include "projecttreemodel.hpp"
 
 namespace studio {
 
diff --git a/src/olympic/studio/modlib/include/studio/filetreemodel.hpp b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp
new file mode 100644
index 00000000..d04c12cd
--- /dev/null
+++ b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp
@@ -0,0 +1,51 @@
+/*
+ * 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>
+
+namespace studio {
+
+class FileExplorer {
+
+	public:
+		virtual ~FileExplorer() = default;
+
+		virtual void fileOpened(ox::StringViewCR path) const noexcept = 0;
+
+		virtual void drawFileContextMenu(ox::StringViewCR path) const noexcept = 0;
+
+		virtual void drawDirContextMenu(ox::StringViewCR path) const noexcept = 0;
+
+};
+
+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::Vector<ox::UPtr<FileTreeModel>> m_children;
+
+	public:
+		virtual ~FileTreeModel() = default;
+
+		explicit FileTreeModel(
+			FileExplorer &explorer, ox::StringParam name,
+			FileTreeModel *parent = nullptr) noexcept;
+
+		FileTreeModel(FileTreeModel &&other) noexcept = default;
+
+		void draw(turbine::Context &tctx) const noexcept;
+
+		void setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept;
+
+};
+
+}
diff --git a/src/olympic/studio/modlib/src/CMakeLists.txt b/src/olympic/studio/modlib/src/CMakeLists.txt
index 9cb4ee94..795f7d72 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
+		filetreemodel.cpp
 		imguiutil.cpp
 		module.cpp
 		popup.cpp
diff --git a/src/olympic/studio/modlib/src/filetreemodel.cpp b/src/olympic/studio/modlib/src/filetreemodel.cpp
new file mode 100644
index 00000000..2992bee1
--- /dev/null
+++ b/src/olympic/studio/modlib/src/filetreemodel.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
+ */
+
+#include <imgui.h>
+
+#include <studio/dragdrop.hpp>
+#include <studio/imguiutil.hpp>
+
+#include <studio/filetreemodel.hpp>
+
+namespace studio {
+
+FileTreeModel::FileTreeModel(
+	FileExplorer &explorer,
+	ox::StringParam name,
+	FileTreeModel *parent) noexcept:
+	m_explorer{explorer},
+	m_parent{parent},
+	m_name{std::move(name)} {
+}
+
+void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
+	constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
+	if (!m_children.empty()) {
+		if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
+            m_explorer.drawDirContextMenu(m_fullPath);
+			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::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
+				m_explorer.fileOpened(m_fullPath);
+			}
+			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}});
+			});
+		}
+	}
+}
+
+void FileTreeModel::setChildren(ox::Vector<ox::UPtr<FileTreeModel>> children) noexcept {
+	m_children = std::move(children);
+}
+
+}