diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp index 41c82e8..e58bb1b 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -88,11 +88,11 @@ static ox::Error toPngFile( TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringParam path): Editor(std::move(path)), - m_sctx(sctx), - m_tctx(m_sctx.tctx), - m_view(m_sctx, itemPath(), *undoStack()), - m_model(m_view.model()) { - std::ignore = setPaletteSelection(); + m_sctx{sctx}, + m_tctx{m_sctx.tctx}, + m_palPicker{"Palette Chooser", keelCtx(sctx), FileExt_npal}, + m_view{m_sctx, itemPath(), *undoStack()}, + m_model{m_view.model()} { // connect signal/slots m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet); m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng); @@ -132,10 +132,10 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) { if (key == turbine::Key::Escape) { m_subsheetEditor.close(); m_exportMenu.close(); + m_palPicker.close(); } - auto const popupOpen = m_subsheetEditor.isOpen() || m_exportMenu.isOpen(); auto const pal = m_model.pal(); - if (!popupOpen) { + if (ig::mainWinHasFocus() && !m_palPathFocused) { auto const colorCnt = gfx::colorCnt(pal, m_model.palettePage()); if (key == turbine::Key::Alpha_D) { m_tool = TileSheetTool::Draw; @@ -183,9 +183,8 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) { } void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { - auto const popupOpen = m_subsheetEditor.isOpen() || m_exportMenu.isOpen(); - if (!popupOpen && m_tool == TileSheetTool::Select) { - if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) { + if (ig::mainWinHasFocus() && m_tool == TileSheetTool::Select) { + if (ImGui::IsKeyDown(ImGuiKey_ModCtrl) && !m_palPathFocused) { if (ImGui::IsKeyPressed(ImGuiKey_A)) { auto const&img = m_model.activeSubSheet(); m_model.setSelection({{}, {img.columns * TileWidth - 1, img.rows * TileHeight - 1}}); @@ -277,6 +276,11 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { ImGui::EndChild(); m_subsheetEditor.draw(m_tctx); m_exportMenu.draw(m_tctx); + if (auto pal = m_palPicker.draw(m_sctx)) { + if (*pal != m_model.palPath()) { + oxLogError(m_model.setPalette(*pal)); + } + } } void TileSheetEditorImGui::drawSubsheetSelector( @@ -437,20 +441,26 @@ void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept { } void TileSheetEditorImGui::drawPaletteMenu() noexcept { + ig::IDStackItem const idStackItem{"PaletteMenu"}; auto constexpr comboWidthSub = 62; ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - comboWidthSub); auto constexpr palTags = ImGuiInputTextFlags_ReadOnly; - if (ig::InputText("Palette", m_selectedPalette, palTags)) { - oxLogError(m_model.setPalette(m_selectedPalette)); + if (ig::InputTextWithHint("##Palette", "Path to Palette", m_model.palPath(), palTags)) { + oxLogError(m_model.setPalette(m_model.palPath())); } + m_palPathFocused = ImGui::IsItemFocused(); if (ig::DragDropTarget const dragDropTarget; dragDropTarget) { auto const [ref, err] = ig::getDragDropPayload("FileRef"); if (!err && endsWith(ref.path, FileExt_npal)) { - if (ref.path != m_selectedPalette) { + if (ref.path != m_model.palPath()) { oxLogError(m_model.setPalette(ref.path)); } } } + ImGui::SameLine(); + if (ImGui::Button("Browse")) { + m_palPicker.open(); + } auto const pages = m_model.pal().pages.size(); if (pages > 1) { ig::IndentStackItem const indentStackItem{20}; @@ -521,7 +531,6 @@ ox::Error TileSheetEditorImGui::updateActiveSubsheet(ox::StringView const&name, } ox::Error TileSheetEditorImGui::setPaletteSelection() noexcept { - m_selectedPalette = m_model.palPath(); return {}; } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp index f3490b3..8fd58a5 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp @@ -9,6 +9,7 @@ #include #include +#include #include "tilesheetpixelgrid.hpp" #include "tilesheetpixels.hpp" @@ -46,17 +47,18 @@ class TileSheetEditorImGui: public studio::Editor { constexpr bool isOpen() const noexcept { return m_show; } }; static constexpr float s_palViewWidth = 300; - ox::String m_selectedPalette; studio::StudioContext &m_sctx; turbine::Context &m_tctx; ox::Vector m_paletteList; SubSheetEditor m_subsheetEditor; ExportMenu m_exportMenu; + studio::FilePickerPopup m_palPicker; glutils::FrameBuffer m_framebuffer; TileSheetEditorView m_view; TileSheetEditorModel &m_model; ox::Vec2 m_prevMouseDownPos; TileSheetTool m_tool = TileSheetTool::Draw; + bool m_palPathFocused{}; public: TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringParam path); diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 04cb839..44fcfdd 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -4,8 +4,6 @@ #include -#include -#include #include #include @@ -86,9 +84,9 @@ void TileSheetEditorModel::copy() { auto cb = ox::make_unique(); iterateSelectionRows(*m_selection, [&](int const x, int const y) { auto pt = ox::Point{x, y}; - const auto&s = activeSubSheet(); - const auto idx = gfx::idx(s, pt); - const auto c = getPixel(s, idx); + auto const&s = activeSubSheet(); + auto const idx = gfx::idx(s, pt); + auto const c = getPixel(s, idx); pt -= m_selection->a; cb->addPixel(pt, c); }); @@ -149,11 +147,11 @@ size_t TileSheetEditorModel::palettePage() const noexcept { } void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const palIdx) noexcept { - const auto &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx); + auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx); if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) { return; } - const auto idx = gfx::idx(activeSubSheet, pt); + auto const idx = gfx::idx(activeSubSheet, pt); if (m_ongoingDrawCommand) { m_updated = m_updated || m_ongoingDrawCommand->append(idx); } else if (getPixel(activeSubSheet, idx) != palIdx) { @@ -255,14 +253,16 @@ bool TileSheetEditorModel::updated() const noexcept { ox::Error TileSheetEditorModel::markUpdatedCmdId(studio::UndoCommand const*cmd) noexcept { m_updated = true; - const auto cmdId = cmd->commandId(); + auto const cmdId = cmd->commandId(); if (static_cast(cmdId) == CommandId::PaletteChange) { - OX_RETURN_ERROR(readObj(keelCtx(m_tctx), m_img.defaultPalette).moveTo(m_pal)); - m_palettePage = ox::min(m_pal->pages.size(), 0); + if (readObj(keelCtx(m_tctx), m_img.defaultPalette).moveTo(m_pal)) { + m_pal = keel::AssetRef{}; + } + m_palettePage = ox::min(pal().pages.size(), 0); paletteChanged.emit(); } - auto tsCmd = dynamic_cast(cmd); - auto idx = validateSubSheetIdx(m_img, tsCmd->subsheetIdx()); + auto const tsCmd = dynamic_cast(cmd); + auto const idx = validateSubSheetIdx(m_img, tsCmd->subsheetIdx()); if (idx != m_activeSubsSheetIdx) { setActiveSubsheet(idx); } diff --git a/src/olympic/studio/applib/src/newmenu.cpp b/src/olympic/studio/applib/src/newmenu.cpp index 0447958..e9f48df 100644 --- a/src/olympic/studio/applib/src/newmenu.cpp +++ b/src/olympic/studio/applib/src/newmenu.cpp @@ -21,7 +21,7 @@ void NewMenu::open() noexcept { m_selectedType = 0; m_itemName = ""; m_typeName = ""; - m_path = ""; + m_useDefaultPath = true; m_explorer.setModel(buildFileTreeModel( m_explorer, [](ox::StringViewCR, ox::FileStat const&s) { @@ -29,9 +29,10 @@ void NewMenu::open() noexcept { }).or_value(ox::UPtr{})); } -void NewMenu::openPath(ox::StringParam path) noexcept { +void NewMenu::openPath(ox::StringViewCR path) noexcept { open(); - m_path = std::move(path); + m_useDefaultPath = false; + std::ignore = m_explorer.setSelectedPath(path); } void NewMenu::close() noexcept { @@ -93,10 +94,9 @@ void NewMenu::drawNewItemType(StudioContext const&sctx) noexcept { drawFirstPageButtons(im.itemTemplates().size() == 1 ? Stage::NewItemTransitioningToPath : Stage::NewItemTemplate); if (m_stage == Stage::NewItemTransitioningToPath || m_stage == Stage::NewItemTemplate) { - if (m_path.len() == 0) { - m_path = im.defaultPath(); + if (m_useDefaultPath) { + std::ignore = m_explorer.setSelectedPath(im.defaultPath()); } - std::ignore = m_explorer.setSelectedPath(m_path); } }); } @@ -192,11 +192,9 @@ void NewMenu::finish(StudioContext &sctx) noexcept { oxLogError(ox::Error{1, "New file error: no file name"}); return; } - auto const&im = *m_types[m_selectedType]; - if (auto p = m_explorer.selectedPath()) { - m_path = std::move(*p); - } - auto const path = sfmt("{}/{}.{}", m_path, m_itemName, im.fileExt()); + auto const &im = *m_types[m_selectedType]; + auto const path = sfmt("{}/{}.{}", + m_explorer.selectedPath().or_value(ox::String{}), m_itemName, im.fileExt()); if (sctx.project->exists(path)) { oxLogError(ox::Error{1, "New file error: file already exists"}); return; diff --git a/src/olympic/studio/applib/src/newmenu.hpp b/src/olympic/studio/applib/src/newmenu.hpp index 51c949d..82587ae 100644 --- a/src/olympic/studio/applib/src/newmenu.hpp +++ b/src/olympic/studio/applib/src/newmenu.hpp @@ -34,7 +34,7 @@ class NewMenu final: public Popup { keel::Context &m_kctx; ox::String m_typeName; ox::IString<255> m_itemName; - ox::String m_path; + bool m_useDefaultPath{}; ox::Vector> m_types; FileExplorer m_explorer{m_kctx}; size_t m_selectedType = 0; @@ -44,7 +44,7 @@ class NewMenu final: public Popup { public: NewMenu(keel::Context &kctx) noexcept; - void openPath(ox::StringParam path) noexcept; + void openPath(ox::StringViewCR path) noexcept; void open() noexcept override; diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index bdef1c0..286ab4d 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 75eea21..922fe42 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/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 6cf063e..05d4542 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -225,7 +225,9 @@ void StudioUI::drawTabs() noexcept { if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] { m_activeEditor->onActivated(); } - e->draw(m_sctx); + if (open) [[likely]] { + e->draw(m_sctx); + } m_activeEditorOnLastDraw = e.get(); } ImGui::EndTabItem(); 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 0000000..d6d5b40 --- /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 ed5cf0c..ea92ad7 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 cf27226..e9a4d1d 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 795f7d7..da338b1 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 0000000..f498797 --- /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 549da57..3b483a1 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();