From e2f2a17315711383a128d3aa7c74579147584db0 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 23 Jan 2025 21:01:59 -0600 Subject: [PATCH] [studio] Add FilePickerPopup --- .../studio/applib/src/projectexplorer.cpp | 4 + .../studio/applib/src/projectexplorer.hpp | 2 + .../modlib/include/studio/filepickerpopup.hpp | 38 ++++++++++ .../modlib/include/studio/filetreemodel.hpp | 8 +- .../modlib/include/studio/imguiutil.hpp | 17 +++++ src/olympic/studio/modlib/src/CMakeLists.txt | 1 + .../studio/modlib/src/filepickerpopup.cpp | 75 +++++++++++++++++++ .../studio/modlib/src/filetreemodel.cpp | 14 +++- 8 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 src/olympic/studio/modlib/include/studio/filepickerpopup.hpp create mode 100644 src/olympic/studio/modlib/src/filepickerpopup.cpp diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index bdef1c08..286ab4d3 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -22,6 +22,10 @@ void ProjectExplorer::fileOpened(ox::StringViewCR path) const noexcept { fileChosen.emit(path); } +void ProjectExplorer::fileDeleted(ox::StringViewCR path) const noexcept { + deleteItem.emit(path); +} + void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) { if (ImGui::MenuItem("Delete")) { diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index 75eea21e..922fe42f 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -28,6 +28,8 @@ class ProjectExplorer final: public FileExplorer { protected: 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 dirContextMenu(ox::StringViewCR path) const noexcept override; diff --git a/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp b/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp new file mode 100644 index 00000000..d6d5b404 --- /dev/null +++ b/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp @@ -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 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 fileExts) noexcept; + + void refresh() noexcept; + + void open() noexcept; + + void close() noexcept; + + [[nodiscard]] + bool isOpen() const noexcept; + + ox::Optional draw(StudioContext &ctx) noexcept; + +}; + +} diff --git a/src/olympic/studio/modlib/include/studio/filetreemodel.hpp b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp index ed5cf0c2..ea92ad74 100644 --- a/src/olympic/studio/modlib/include/studio/filetreemodel.hpp +++ b/src/olympic/studio/modlib/include/studio/filetreemodel.hpp @@ -15,7 +15,7 @@ namespace studio { -constexpr void safeDelete(class FileTreeModel *m) noexcept; +constexpr void safeDelete(class FileTreeModel const *m) noexcept; class FileExplorer: public ox::SignalHandler { @@ -32,8 +32,6 @@ class FileExplorer: public ox::SignalHandler { m_kctx{kctx}, m_fileDraggable{fileDraggable} {} - virtual ~FileExplorer() = default; - void draw(StudioContext &ctx, ImVec2 const &sz) const noexcept; void setModel(ox::UPtr &&model, bool selectRoot = false) noexcept; @@ -45,6 +43,8 @@ class FileExplorer: public ox::SignalHandler { virtual void fileOpened(ox::StringViewCR path) const noexcept; + virtual void fileDeleted(ox::StringViewCR path) const noexcept; + void drawFileContextMenu(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; } diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index cf272261..e9a4d1d7 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -155,6 +155,23 @@ TextInput> InputText( return out; } +template +TextInput> InputTextWithHint( + ox::CStringViewCR label, + ox::CStringViewCR hint, + ox::StringViewCR currentText, + ImGuiInputTextFlags const flags = 0, + ImGuiInputTextCallback const callback = nullptr, + void *user_data = nullptr) noexcept { + TextInput> 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 bool InputText( ox::CStringViewCR label, diff --git a/src/olympic/studio/modlib/src/CMakeLists.txt b/src/olympic/studio/modlib/src/CMakeLists.txt index 795f7d72..da338b1e 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 + filepickerpopup.cpp filetreemodel.cpp imguiutil.cpp module.cpp diff --git a/src/olympic/studio/modlib/src/filepickerpopup.cpp b/src/olympic/studio/modlib/src/filepickerpopup.cpp new file mode 100644 index 00000000..f498797f --- /dev/null +++ b/src/olympic/studio/modlib/src/filepickerpopup.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include + +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 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{})); +} + +void FilePickerPopup::open() noexcept { + refresh(); + m_open = true; +} + +void FilePickerPopup::close() noexcept { + m_explorer.setModel(ox::UPtr{}); + m_open = false; +} + +bool FilePickerPopup::isOpen() const noexcept { + return m_open; +} + +ox::Optional FilePickerPopup::draw(StudioContext &ctx) noexcept { + ox::Optional 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; +} + +} diff --git a/src/olympic/studio/modlib/src/filetreemodel.cpp b/src/olympic/studio/modlib/src/filetreemodel.cpp index 549da57f..3b483a1c 100644 --- a/src/olympic/studio/modlib/src/filetreemodel.cpp +++ b/src/olympic/studio/modlib/src/filetreemodel.cpp @@ -39,6 +39,8 @@ ox::Optional FileExplorer::selectedPath() const { void FileExplorer::fileOpened(ox::StringViewCR) const noexcept {} +void FileExplorer::fileDeleted(ox::StringViewCR) const noexcept {} + void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept { ig::IDStackItem const idStackItem{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; if (!m_children.empty()) { auto const nodeOpen = ImGui::TreeNodeEx(m_name.c_str(), dirFlags | selected); - if (ImGui::IsItemClicked()) { + if (ImGui::IsItemActivated() || ImGui::IsItemClicked(1)) { m_explorer.setSelectedNode(this); } ig::IDStackItem const idStackItem{m_name}; @@ -97,11 +99,15 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept { } } else { 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); } - if (ImGui::IsItemClicked()) { - m_explorer.setSelectedNode(this); + if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)) { + m_explorer.fileDeleted(m_fullPath); } m_explorer.drawFileContextMenu(m_fullPath); ImGui::TreePop();