[studio] Add FilePickerPopup
This commit is contained in:
		| @@ -22,6 +22,10 @@ void ProjectExplorer::fileOpened(ox::StringViewCR path) const noexcept { | |||||||
| 	fileChosen.emit(path); | 	fileChosen.emit(path); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void ProjectExplorer::fileDeleted(ox::StringViewCR path) const noexcept { | ||||||
|  | 	deleteItem.emit(path); | ||||||
|  | } | ||||||
|  |  | ||||||
| void ProjectExplorer::fileContextMenu(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")) { | ||||||
|   | |||||||
| @@ -28,6 +28,8 @@ class ProjectExplorer final: public FileExplorer { | |||||||
| 	protected: | 	protected: | ||||||
| 		void fileOpened(ox::StringViewCR path) const noexcept override; | 		void fileOpened(ox::StringViewCR path) const noexcept override; | ||||||
|  |  | ||||||
|  | 		void fileDeleted(ox::StringViewCR path) const noexcept override; | ||||||
|  |  | ||||||
| 		void fileContextMenu(ox::StringViewCR path) const noexcept override; | 		void fileContextMenu(ox::StringViewCR path) const noexcept override; | ||||||
|  |  | ||||||
| 		void dirContextMenu(ox::StringViewCR path) const noexcept override; | 		void dirContextMenu(ox::StringViewCR path) const noexcept override; | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								src/olympic/studio/modlib/include/studio/filepickerpopup.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/olympic/studio/modlib/include/studio/filepickerpopup.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "popup.hpp" | ||||||
|  | #include "filetreemodel.hpp" | ||||||
|  |  | ||||||
|  | namespace studio { | ||||||
|  |  | ||||||
|  | class FilePickerPopup { | ||||||
|  |  | ||||||
|  | 	private: | ||||||
|  | 		ox::String m_name; | ||||||
|  | 		FileExplorer m_explorer; | ||||||
|  | 		ox::Vector<ox::String> const m_fileExts; | ||||||
|  | 		bool m_open{}; | ||||||
|  |  | ||||||
|  | 	public: | ||||||
|  | 		explicit FilePickerPopup(ox::StringParam name, keel::Context &kctx, ox::StringParam fileExt) noexcept; | ||||||
|  |  | ||||||
|  | 		explicit FilePickerPopup(ox::StringParam name, keel::Context &kctx, ox::Vector<ox::String> fileExts) noexcept; | ||||||
|  |  | ||||||
|  | 		void refresh() noexcept; | ||||||
|  |  | ||||||
|  | 		void open() noexcept; | ||||||
|  |  | ||||||
|  | 		void close() noexcept; | ||||||
|  |  | ||||||
|  | 		[[nodiscard]] | ||||||
|  | 		bool isOpen() const noexcept; | ||||||
|  |  | ||||||
|  | 		ox::Optional<ox::String> draw(StudioContext &ctx) noexcept; | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -15,7 +15,7 @@ | |||||||
|  |  | ||||||
| namespace studio { | namespace studio { | ||||||
|  |  | ||||||
| constexpr void safeDelete(class FileTreeModel *m) noexcept; | constexpr void safeDelete(class FileTreeModel const *m) noexcept; | ||||||
|  |  | ||||||
| class FileExplorer: public ox::SignalHandler { | class FileExplorer: public ox::SignalHandler { | ||||||
|  |  | ||||||
| @@ -32,8 +32,6 @@ class FileExplorer: public ox::SignalHandler { | |||||||
| 			m_kctx{kctx}, | 			m_kctx{kctx}, | ||||||
| 			m_fileDraggable{fileDraggable} {} | 			m_fileDraggable{fileDraggable} {} | ||||||
|  |  | ||||||
| 		virtual ~FileExplorer() = default; |  | ||||||
|  |  | ||||||
| 		void draw(StudioContext &ctx, ImVec2 const &sz) const noexcept; | 		void draw(StudioContext &ctx, ImVec2 const &sz) const noexcept; | ||||||
|  |  | ||||||
| 		void setModel(ox::UPtr<FileTreeModel> &&model, bool selectRoot = false) noexcept; | 		void setModel(ox::UPtr<FileTreeModel> &&model, bool selectRoot = false) noexcept; | ||||||
| @@ -45,6 +43,8 @@ class FileExplorer: public ox::SignalHandler { | |||||||
|  |  | ||||||
| 		virtual void fileOpened(ox::StringViewCR path) const noexcept; | 		virtual void fileOpened(ox::StringViewCR path) const noexcept; | ||||||
|  |  | ||||||
|  | 		virtual void fileDeleted(ox::StringViewCR path) const noexcept; | ||||||
|  |  | ||||||
| 		void drawFileContextMenu(ox::CStringViewCR path) const noexcept; | 		void drawFileContextMenu(ox::CStringViewCR path) const noexcept; | ||||||
|  |  | ||||||
| 		void drawDirContextMenu(ox::CStringViewCR path) const noexcept; | 		void drawDirContextMenu(ox::CStringViewCR path) const noexcept; | ||||||
| @@ -116,7 +116,7 @@ class FileTreeModel { | |||||||
|  |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| constexpr void safeDelete(FileTreeModel *m) noexcept { | constexpr void safeDelete(FileTreeModel const *m) noexcept { | ||||||
| 	delete m; | 	delete m; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -155,6 +155,23 @@ TextInput<ox::IString<MaxChars>> InputText( | |||||||
| 	return out; | 	return out; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | template<size_t MaxChars = 50> | ||||||
|  | TextInput<ox::IString<MaxChars>> InputTextWithHint( | ||||||
|  | 		ox::CStringViewCR label, | ||||||
|  | 		ox::CStringViewCR hint, | ||||||
|  | 		ox::StringViewCR currentText, | ||||||
|  | 		ImGuiInputTextFlags const flags = 0, | ||||||
|  | 		ImGuiInputTextCallback const callback = nullptr, | ||||||
|  | 		void *user_data = nullptr) noexcept { | ||||||
|  | 	TextInput<ox::IString<MaxChars>> out = {.text = currentText}; | ||||||
|  | 	out.changed = ImGui::InputTextWithHint( | ||||||
|  | 			label.c_str(), hint.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data); | ||||||
|  | 	if (out.changed) { | ||||||
|  | 		std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str())); | ||||||
|  | 	} | ||||||
|  | 	return out; | ||||||
|  | } | ||||||
|  |  | ||||||
| template<size_t StrCap> | template<size_t StrCap> | ||||||
| bool InputText( | bool InputText( | ||||||
| 	ox::CStringViewCR label, | 	ox::CStringViewCR label, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ add_library( | |||||||
| 	Studio | 	Studio | ||||||
| 		configio.cpp | 		configio.cpp | ||||||
| 		editor.cpp | 		editor.cpp | ||||||
|  | 		filepickerpopup.cpp | ||||||
| 		filetreemodel.cpp | 		filetreemodel.cpp | ||||||
| 		imguiutil.cpp | 		imguiutil.cpp | ||||||
| 		module.cpp | 		module.cpp | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								src/olympic/studio/modlib/src/filepickerpopup.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/olympic/studio/modlib/src/filepickerpopup.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #include <studio/imguiutil.hpp> | ||||||
|  |  | ||||||
|  | #include <studio/filepickerpopup.hpp> | ||||||
|  |  | ||||||
|  | namespace studio { | ||||||
|  |  | ||||||
|  | FilePickerPopup::FilePickerPopup( | ||||||
|  | 	ox::StringParam name, | ||||||
|  | 	keel::Context &kctx, | ||||||
|  | 	ox::StringParam fileExt) noexcept: | ||||||
|  | 	m_name{std::move(name)}, | ||||||
|  | 	m_explorer{kctx}, | ||||||
|  | 	m_fileExts{std::move(fileExt)} { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | FilePickerPopup::FilePickerPopup( | ||||||
|  | 	ox::StringParam name, | ||||||
|  | 	keel::Context &kctx, | ||||||
|  | 	ox::Vector<ox::String> fileExts) noexcept: | ||||||
|  | 	m_name{std::move(name)}, | ||||||
|  | 	m_explorer{kctx}, | ||||||
|  | 	m_fileExts{std::move(fileExts)} { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FilePickerPopup::refresh() noexcept { | ||||||
|  | 	m_explorer.setModel(buildFileTreeModel( | ||||||
|  | 		m_explorer, | ||||||
|  | 		[this](ox::StringViewCR path, ox::FileStat const &s) { | ||||||
|  | 			auto const [ext, err] = fileExt(path); | ||||||
|  | 			return | ||||||
|  | 				s.fileType == ox::FileType::Directory || | ||||||
|  | 				(s.fileType == ox::FileType::NormalFile && !err && m_fileExts.contains(ext)); | ||||||
|  | 		}, | ||||||
|  | 		false).or_value(ox::UPtr<FileTreeModel>{})); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FilePickerPopup::open() noexcept { | ||||||
|  | 	refresh(); | ||||||
|  | 	m_open = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FilePickerPopup::close() noexcept { | ||||||
|  | 	m_explorer.setModel(ox::UPtr<FileTreeModel>{}); | ||||||
|  | 	m_open = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool FilePickerPopup::isOpen() const noexcept { | ||||||
|  | 	return m_open; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ox::Optional<ox::String> FilePickerPopup::draw(StudioContext &ctx) noexcept { | ||||||
|  | 	ox::Optional<ox::String> out; | ||||||
|  | 	if (!m_open) { | ||||||
|  | 		return out; | ||||||
|  | 	} | ||||||
|  | 	if (ig::BeginPopup(ctx.tctx, m_name, m_open, {380, 340})) { | ||||||
|  | 		auto const vp = ImGui::GetContentRegionAvail(); | ||||||
|  | 		m_explorer.draw(ctx, {vp.x, vp.y - 30}); | ||||||
|  | 		if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) { | ||||||
|  | 			auto p = m_explorer.selectedPath(); | ||||||
|  | 			if (p) { | ||||||
|  | 				out.emplace(*p); | ||||||
|  | 			} | ||||||
|  | 			close(); | ||||||
|  | 		} | ||||||
|  | 		ImGui::EndPopup(); | ||||||
|  | 	} | ||||||
|  | 	return out; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -39,6 +39,8 @@ ox::Optional<ox::String> FileExplorer::selectedPath() const { | |||||||
|  |  | ||||||
| void FileExplorer::fileOpened(ox::StringViewCR) const noexcept {} | void FileExplorer::fileOpened(ox::StringViewCR) const noexcept {} | ||||||
|  |  | ||||||
|  | void FileExplorer::fileDeleted(ox::StringViewCR) const noexcept {} | ||||||
|  |  | ||||||
| void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept { | void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept { | ||||||
| 	ig::IDStackItem const idStackItem{path}; | 	ig::IDStackItem const idStackItem{path}; | ||||||
| 	fileContextMenu(path); | 	fileContextMenu(path); | ||||||
| @@ -84,7 +86,7 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept { | |||||||
| 	auto const selected = m_explorer.selected(this) ? ImGuiTreeNodeFlags_Selected : 0; | 	auto const selected = m_explorer.selected(this) ? ImGuiTreeNodeFlags_Selected : 0; | ||||||
| 	if (!m_children.empty()) { | 	if (!m_children.empty()) { | ||||||
| 		auto const nodeOpen = ImGui::TreeNodeEx(m_name.c_str(), dirFlags | selected); | 		auto const nodeOpen = ImGui::TreeNodeEx(m_name.c_str(), dirFlags | selected); | ||||||
| 		if (ImGui::IsItemClicked()) { | 		if (ImGui::IsItemActivated() || ImGui::IsItemClicked(1)) { | ||||||
| 			m_explorer.setSelectedNode(this); | 			m_explorer.setSelectedNode(this); | ||||||
| 		} | 		} | ||||||
| 		ig::IDStackItem const idStackItem{m_name}; | 		ig::IDStackItem const idStackItem{m_name}; | ||||||
| @@ -97,11 +99,15 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept { | |||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf | selected)) { | 		if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf | selected)) { | ||||||
| 			if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { | 			if (ImGui::IsItemActivated() || ImGui::IsItemClicked(1)) { | ||||||
|  | 				m_explorer.setSelectedNode(this); | ||||||
|  | 			} | ||||||
|  | 			if ((ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) || | ||||||
|  | 			    (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter))) { | ||||||
| 				m_explorer.fileOpened(m_fullPath); | 				m_explorer.fileOpened(m_fullPath); | ||||||
| 			} | 			} | ||||||
| 			if (ImGui::IsItemClicked()) { | 			if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)) { | ||||||
| 				m_explorer.setSelectedNode(this); | 				m_explorer.fileDeleted(m_fullPath); | ||||||
| 			} | 			} | ||||||
| 			m_explorer.drawFileContextMenu(m_fullPath); | 			m_explorer.drawFileContextMenu(m_fullPath); | ||||||
| 			ImGui::TreePop(); | 			ImGui::TreePop(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user