From fb8d295fcbdc8b4427ac38b423fc29f21b645f0b Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 14:46:21 -0600 Subject: [PATCH 01/29] [nostalgia/core/studio/tilesheet] Add rotate functionality --- .../tilesheeteditor/commands/CMakeLists.txt | 1 + .../tilesheeteditor/commands/commands.hpp | 1 + .../commands/rmsubsheetcommand.cpp | 2 +- .../commands/rotatecommand.cpp | 99 +++++++++++++++++++ .../commands/rotatecommand.hpp | 38 +++++++ .../tilesheeteditor/tilesheeteditor-imgui.cpp | 14 ++- .../tilesheeteditor/tilesheeteditormodel.cpp | 11 +++ .../tilesheeteditor/tilesheeteditormodel.hpp | 4 + 8 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp create mode 100644 src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/CMakeLists.txt b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/CMakeLists.txt index f8f42c87..ee4fd9bc 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/CMakeLists.txt +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/CMakeLists.txt @@ -9,5 +9,6 @@ target_sources( inserttilescommand.cpp palettechangecommand.cpp rmsubsheetcommand.cpp + rotatecommand.cpp updatesubsheetcommand.cpp ) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/commands.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/commands.hpp index 4cdebe6b..17b98990 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/commands.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/commands.hpp @@ -17,6 +17,7 @@ enum class CommandId { DeleteTile, FlipX, FlipY, + Rotate, InsertTile, MoveSubSheet, UpdateSubSheet, diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rmsubsheetcommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rmsubsheetcommand.cpp index bb6011bb..bb11bb66 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rmsubsheetcommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rmsubsheetcommand.cpp @@ -6,7 +6,7 @@ namespace nostalgia::gfx { -gfx::RmSubSheetCommand::RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept: +RmSubSheetCommand::RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept: m_img(img), m_idx(std::move(idx)), m_parentIdx(m_idx) { diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp new file mode 100644 index 00000000..bad82472 --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp @@ -0,0 +1,99 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include "rotatecommand.hpp" + +namespace nostalgia::gfx { + +static void rotateLeft(TileSheet::SubSheet &ss, ox::Point const &pt, int const depth = 0) noexcept { + if (depth >= 4) { + return; + } + auto const h = ss.rows * TileHeight; + auto const dstPt = ox::Point{pt.y, h - 1 - pt.x}; + auto const srcIdx = ptToIdx(pt, ss.columns); + auto const dstIdx = ptToIdx(dstPt, ss.columns); + auto const src = ss.pixels[srcIdx]; + auto &dst = ss.pixels[dstIdx]; + rotateLeft(ss, dstPt, depth + 1); + dst = src; +} + +static void rotateLeft(TileSheet::SubSheet &ss) noexcept { + auto const w = ss.columns * TileWidth; + auto const h = ss.rows * TileHeight; + for (int x = 0; x < w / 2; ++x) { + for (int y = 0; y < h / 2; ++y) { + rotateLeft(ss, {w - 1 - y, x}); + } + } +} + +static void rotateRight(TileSheet::SubSheet &ss, ox::Point const &pt, int const depth = 0) noexcept { + if (depth >= 4) { + return; + } + auto const w = ss.columns * TileWidth; + auto const dstPt = ox::Point{w - 1 - pt.y, pt.x}; + auto const srcIdx = ptToIdx(pt, ss.columns); + auto const dstIdx = ptToIdx(dstPt, ss.columns); + auto const src = ss.pixels[srcIdx]; + auto &dst = ss.pixels[dstIdx]; + rotateRight(ss, dstPt, depth + 1); + dst = src; +} + +static void rotateRight(TileSheet::SubSheet &ss) noexcept { + auto const w = ss.columns * TileWidth; + auto const h = ss.rows * TileHeight; + for (int x = 0; x < w / 2; ++x) { + for (int y = 0; y < h / 2; ++y) { + rotateRight(ss, {w - 1 - y, x}); + } + } +} + + +RotateCommand::RotateCommand( + TileSheet &img, + TileSheet::SubSheetIdx idx, + Direction const dir) noexcept: + m_img(img), + m_idx(std::move(idx)), + m_dir{dir} { +} + +ox::Error RotateCommand::redo() noexcept { + switch (m_dir) { + case Direction::Left: + rotateLeft(getSubSheet(m_img, m_idx)); + break; + case Direction::Right: + rotateRight(getSubSheet(m_img, m_idx)); + break; + } + return {}; +} + +ox::Error RotateCommand::undo() noexcept { + switch (m_dir) { + case Direction::Left: + rotateRight(getSubSheet(m_img, m_idx)); + break; + case Direction::Right: + rotateLeft(getSubSheet(m_img, m_idx)); + break; + } + return {}; +} + +int RotateCommand::commandId() const noexcept { + return static_cast(CommandId::Rotate); +} + +TileSheet::SubSheetIdx const&RotateCommand::subsheetIdx() const noexcept { + return m_idx; +} + +} diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp new file mode 100644 index 00000000..e7a89772 --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp @@ -0,0 +1,38 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include "commands.hpp" + +namespace nostalgia::gfx { + +class RotateCommand: public TileSheetCommand { + public: + enum class Direction { + Right, + Left, + }; + + private: + TileSheet &m_img; + TileSheet::SubSheetIdx m_idx; + Direction const m_dir; + + public: + RotateCommand(TileSheet &img, TileSheet::SubSheetIdx idx, Direction dir) noexcept; + + ox::Error redo() noexcept final; + + ox::Error undo() noexcept final; + + [[nodiscard]] + int commandId() const noexcept final; + + [[nodiscard]] + TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override; + +}; + +} 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 fe4bece9..6b8fc250 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -248,14 +248,22 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { ImGui::EndChild(); ImGui::BeginChild("OperationsBox", {0, 32}, true); { - auto constexpr btnSz = ImVec2{55, 16}; - if (ig::PushButton("Flip X", btnSz)) { + auto constexpr btnSz = ImVec2{75, 16}; + if (ig::PushButton("Flip X", {55, 16})) { oxLogError(m_model.flipX()); } ImGui::SameLine(); - if (ig::PushButton("Flip Y", btnSz)) { + if (ig::PushButton("Flip Y", {55, 16})) { oxLogError(m_model.flipY()); } + ImGui::SameLine(); + if (ig::PushButton("Rotate Left", {80, 16})) { + oxLogError(m_model.rotateLeft()); + } + ImGui::SameLine(); + if (ig::PushButton("Rotate Right", {80, 16})) { + oxLogError(m_model.rotateRight()); + } } ImGui::EndChild(); auto const ySize = controlsSize.y - (38 + ig::BtnSz.y + 21); diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 954ef891..82f72b6f 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -24,6 +24,7 @@ #include "tilesheeteditormodel.hpp" #include "commands/movesubsheetcommand.hpp" +#include "commands/rotatecommand.hpp" namespace nostalgia::gfx { @@ -237,6 +238,16 @@ void TileSheetEditorModel::fill(ox::Point const&pt, int const palIdx) noexcept { } } +ox::Error TileSheetEditorModel::rotateLeft() noexcept { + return pushCommand(ox::make( + m_img, m_activeSubsSheetIdx, RotateCommand::Direction::Left)); +} + +ox::Error TileSheetEditorModel::rotateRight() noexcept { + return pushCommand(ox::make( + m_img, m_activeSubsSheetIdx, RotateCommand::Direction::Right)); +} + void TileSheetEditorModel::setSelection(studio::Selection const&sel) noexcept { m_selection.emplace(sel); m_updated = true; diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp index 95cab4f4..390c9aef 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp @@ -106,6 +106,10 @@ class TileSheetEditorModel: public ox::SignalHandler { void fill(ox::Point const&pt, int palIdx) noexcept; + ox::Error rotateLeft() noexcept; + + ox::Error rotateRight() noexcept; + void setSelection(studio::Selection const&sel) noexcept; void select(ox::Point const&pt) noexcept; From 1bc18e34a8faba90af727a1f348dff200a730427 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 20:22:20 -0600 Subject: [PATCH 02/29] [nostalgia/core/studio/tilesheet] Add ability to rotate a selection --- .../commands/rotatecommand.cpp | 81 ++++++++++++------- .../commands/rotatecommand.hpp | 9 +++ .../tilesheeteditor/tilesheeteditormodel.cpp | 16 +++- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp index bad82472..4b540fdf 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp @@ -6,50 +6,58 @@ namespace nostalgia::gfx { -static void rotateLeft(TileSheet::SubSheet &ss, ox::Point const &pt, int const depth = 0) noexcept { +static void rotateLeft( + TileSheet::SubSheet &ss, + ox::Point const &pt, + ox::Point const &pt1, + ox::Point const &pt2, + int const depth = 0) noexcept { if (depth >= 4) { return; } - auto const h = ss.rows * TileHeight; - auto const dstPt = ox::Point{pt.y, h - 1 - pt.x}; - auto const srcIdx = ptToIdx(pt, ss.columns); + auto const dstPt = ox::Point{pt1.x + pt.y, pt2.y - pt.x}; + auto const srcIdx = ptToIdx(pt + pt1, ss.columns); auto const dstIdx = ptToIdx(dstPt, ss.columns); auto const src = ss.pixels[srcIdx]; auto &dst = ss.pixels[dstIdx]; - rotateLeft(ss, dstPt, depth + 1); + rotateLeft(ss, dstPt - pt1, pt1, pt2, depth + 1); dst = src; } -static void rotateLeft(TileSheet::SubSheet &ss) noexcept { - auto const w = ss.columns * TileWidth; - auto const h = ss.rows * TileHeight; - for (int x = 0; x < w / 2; ++x) { - for (int y = 0; y < h / 2; ++y) { - rotateLeft(ss, {w - 1 - y, x}); +static void rotateLeft(TileSheet::SubSheet &ss, ox::Point const &pt1, ox::Point const &pt2) noexcept { + auto const w = pt2.x - pt1.x; + auto const h = pt2.y - pt1.y; + for (int x = 0; x <= w / 2; ++x) { + for (int y = 0; y <= h / 2; ++y) { + rotateLeft(ss, {x, y}, pt1, pt2); } } } -static void rotateRight(TileSheet::SubSheet &ss, ox::Point const &pt, int const depth = 0) noexcept { +static void rotateRight( + TileSheet::SubSheet &ss, + ox::Point const &pt, + ox::Point const &pt1, + ox::Point const &pt2, + int const depth = 0) noexcept { if (depth >= 4) { return; } - auto const w = ss.columns * TileWidth; - auto const dstPt = ox::Point{w - 1 - pt.y, pt.x}; - auto const srcIdx = ptToIdx(pt, ss.columns); + auto const dstPt = ox::Point{pt2.x - pt.y, pt1.y + pt.x}; + auto const srcIdx = ptToIdx(pt + pt1, ss.columns); auto const dstIdx = ptToIdx(dstPt, ss.columns); auto const src = ss.pixels[srcIdx]; auto &dst = ss.pixels[dstIdx]; - rotateRight(ss, dstPt, depth + 1); + rotateRight(ss, dstPt - pt1, pt1, pt2, depth + 1); dst = src; } -static void rotateRight(TileSheet::SubSheet &ss) noexcept { - auto const w = ss.columns * TileWidth; - auto const h = ss.rows * TileHeight; - for (int x = 0; x < w / 2; ++x) { - for (int y = 0; y < h / 2; ++y) { - rotateRight(ss, {w - 1 - y, x}); +static void rotateRight(TileSheet::SubSheet &ss, ox::Point const &pt1, ox::Point const &pt2) noexcept { + auto const w = pt2.x - pt1.x; + auto const h = pt2.y - pt1.y; + for (int x = 0; x <= w / 2; ++x) { + for (int y = 0; y <= h / 2; ++y) { + rotateRight(ss, {x, y}, pt1, pt2); } } } @@ -61,28 +69,47 @@ RotateCommand::RotateCommand( Direction const dir) noexcept: m_img(img), m_idx(std::move(idx)), + m_pt2{[this] { + auto &ss = getSubSheet(m_img, m_idx); + return ox::Point{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1}; + }()}, + m_dir{dir} { +} + +RotateCommand::RotateCommand( + TileSheet &img, + TileSheet::SubSheetIdx idx, + ox::Point const &pt1, + ox::Point const &pt2, + Direction const dir) noexcept: + m_img(img), + m_idx(std::move(idx)), + m_pt1{pt1}, + m_pt2{pt2}, m_dir{dir} { } ox::Error RotateCommand::redo() noexcept { + auto &ss = getSubSheet(m_img, m_idx); switch (m_dir) { case Direction::Left: - rotateLeft(getSubSheet(m_img, m_idx)); - break; + rotateLeft(ss, m_pt1, m_pt2); + break; case Direction::Right: - rotateRight(getSubSheet(m_img, m_idx)); + rotateRight(ss, m_pt1, m_pt2); break; } return {}; } ox::Error RotateCommand::undo() noexcept { + auto &ss = getSubSheet(m_img, m_idx); switch (m_dir) { case Direction::Left: - rotateRight(getSubSheet(m_img, m_idx)); + rotateRight(ss, m_pt1, m_pt2); break; case Direction::Right: - rotateLeft(getSubSheet(m_img, m_idx)); + rotateLeft(ss, m_pt1, m_pt2); break; } return {}; diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp index e7a89772..2418b1a1 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp @@ -18,11 +18,20 @@ class RotateCommand: public TileSheetCommand { private: TileSheet &m_img; TileSheet::SubSheetIdx m_idx; + ox::Point const m_pt1; + ox::Point const m_pt2; Direction const m_dir; public: RotateCommand(TileSheet &img, TileSheet::SubSheetIdx idx, Direction dir) noexcept; + RotateCommand( + TileSheet &img, + TileSheet::SubSheetIdx idx, + ox::Point const &pt1, + ox::Point const &pt2, + Direction dir) noexcept; + ox::Error redo() noexcept final; ox::Error undo() noexcept final; diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 82f72b6f..2d0588a3 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -239,13 +239,25 @@ void TileSheetEditorModel::fill(ox::Point const&pt, int const palIdx) noexcept { } ox::Error TileSheetEditorModel::rotateLeft() noexcept { + auto &ss = activeSubSheet(); + ox::Point pt1, pt2{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1}; + if (m_selection) { + pt1 = m_selection->a; + pt2 = m_selection->b; + } return pushCommand(ox::make( - m_img, m_activeSubsSheetIdx, RotateCommand::Direction::Left)); + m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Left)); } ox::Error TileSheetEditorModel::rotateRight() noexcept { + auto &ss = activeSubSheet(); + ox::Point pt1, pt2{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1}; + if (m_selection) { + pt1 = m_selection->a; + pt2 = m_selection->b; + } return pushCommand(ox::make( - m_img, m_activeSubsSheetIdx, RotateCommand::Direction::Right)); + m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Right)); } void TileSheetEditorModel::setSelection(studio::Selection const&sel) noexcept { From 105a1e5559e9d9b4ad162c614c8deae0bfec198e Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 20:43:01 -0600 Subject: [PATCH 03/29] [nostalgia/core/studio/tilesheet] Rework operation ctrls into a dropbox --- .../tilesheeteditor/tilesheeteditor-imgui.cpp | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 6b8fc250..48c7583b 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -246,23 +246,31 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { //ig::ComboBox("##Operations", ox::Array{"Operations"}, i); } ImGui::EndChild(); - ImGui::BeginChild("OperationsBox", {0, 32}, true); + ImGui::BeginChild("OperationsBox", {0, 35}, ImGuiWindowFlags_NoTitleBar); { - auto constexpr btnSz = ImVec2{75, 16}; - if (ig::PushButton("Flip X", {55, 16})) { - oxLogError(m_model.flipX()); - } - ImGui::SameLine(); - if (ig::PushButton("Flip Y", {55, 16})) { - oxLogError(m_model.flipY()); - } - ImGui::SameLine(); - if (ig::PushButton("Rotate Left", {80, 16})) { - oxLogError(m_model.rotateLeft()); - } - ImGui::SameLine(); - if (ig::PushButton("Rotate Right", {80, 16})) { - oxLogError(m_model.rotateRight()); + size_t i{}; + if (ig::ComboBox("##Operations", ox::SpanView{{ + ox::CStringView{"Operations"}, + ox::CStringView{"Flip X"}, + ox::CStringView{"Flip Y"}, + ox::CStringView{"Rotate Left"}, + ox::CStringView{"Rotate Right"}, + }}, i)) { + switch (i) { + case 1: + oxLogError(m_model.flipX()); + break; + case 2: + oxLogError(m_model.flipY()); + break; + case 3: + oxLogError(m_model.rotateLeft()); + break; + case 4: + oxLogError(m_model.rotateRight()); + break; + default:; + } } } ImGui::EndChild(); From 4728699585d59132148c48ac27a7dff8a4e9eed0 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 20:46:08 -0600 Subject: [PATCH 04/29] [studio] Add combobox that will take string views --- .../modlib/include/studio/imguiutil.hpp | 12 ++++++++++++ src/olympic/studio/modlib/src/imguiutil.cpp | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 0f71da88..32855a63 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -225,6 +225,18 @@ PopupResponse PopupControlsOkCancel( [[nodiscard]] bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); +/** + * + * @param lbl + * @param list + * @param selectedIdx + * @return true if new value selected, false otherwise + */ +bool ComboBox( + ox::CStringView lbl, + ox::SpanView list, + size_t &selectedIdx) noexcept; + /** * * @param lbl diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 12549785..4935dc8e 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -90,6 +90,25 @@ bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, return ImGui::BeginPopupModal(popupName.c_str(), &show, modalFlags); } +bool ComboBox( + ox::CStringView lbl, + ox::SpanView list, + size_t &selectedIdx) noexcept { + bool out{}; + auto const first = selectedIdx < list.size() ? list[selectedIdx].c_str() : ""; + if (ImGui::BeginCombo(lbl.c_str(), first, 0)) { + for (auto i = 0u; i < list.size(); ++i) { + const auto selected = (selectedIdx == i); + if (ImGui::Selectable(list[i].c_str(), selected) && selectedIdx != i) { + selectedIdx = i; + out = true; + } + } + ImGui::EndCombo(); + } + return out; +} + bool ComboBox( ox::CStringView lbl, ox::Span list, From cd1f4bdaa37d8f3e0f776c6339cdabbbd1918b3b Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 23:07:59 -0600 Subject: [PATCH 05/29] [studio] Add confirmation for closing file with unsaved changes --- src/olympic/studio/applib/src/studioapp.cpp | 42 +++++++++++---- src/olympic/studio/applib/src/studioapp.hpp | 4 ++ .../modlib/include/studio/imguiutil.hpp | 28 ++++++++++ src/olympic/studio/modlib/src/imguiutil.cpp | 53 +++++++++++++++++++ 4 files changed, 116 insertions(+), 11 deletions(-) diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 3cbe35a9..61a06f12 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -64,6 +64,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); m_newProject.finished.connect(this, &StudioUI::createOpenProject); m_newMenu.finished.connect(this, &StudioUI::openFile); + m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse); loadModules(); // open project and files auto const [config, err] = studio::readConfig(keelCtx(m_tctx)); @@ -134,6 +135,7 @@ void StudioUI::draw() noexcept { for (auto const p : m_popups) { p->draw(m_sctx); } + m_closeFileConfirm.draw(m_sctx); } ImGui::End(); handleKeyInput(); @@ -214,7 +216,7 @@ void StudioUI::drawTabs() noexcept { auto open = true; auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0; auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0; - auto const flags = unsavedChanges | selected; + auto const flags = unsavedChanges | selected | ImGuiTabItemFlags_NoAssumedClosure; if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) { if (m_activeEditor != e.get()) [[unlikely]] { m_activeEditor = e.get(); @@ -237,16 +239,20 @@ void StudioUI::drawTabs() noexcept { ImGui::EndTabItem(); } if (!open) { - e->close(); - if (m_activeEditor == (*it).get()) { - m_activeEditor = nullptr; - } - try { - OX_THROW_ERROR(m_editors.erase(it).moveTo(it)); - } catch (ox::Exception const&ex) { - oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.src.file_name(), ex.src.line()); - } catch (std::exception const&ex) { - oxErrf("Editor tab deletion failed: {}\n", ex.what()); + if (e->unsavedChanges()) { + m_closeFileConfirm.open(); + } else { + e->close(); + if (m_activeEditor == (*it).get()) { + m_activeEditor = nullptr; + } + try { + OX_THROW_ERROR(m_editors.erase(it).moveTo(it)); + } catch (ox::Exception const&ex) { + oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.src.file_name(), ex.src.line()); + } catch (std::exception const&ex) { + oxErrf("Editor tab deletion failed: {}\n", ex.what()); + } } } else { ++it; @@ -483,6 +489,20 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi return {}; } +ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { + if (response == ig::PopupResponse::OK && m_activeEditor) { + for (size_t i{}; auto &e : m_editors) { + if (m_activeEditor == e.get()) { + oxLogError(closeFile(e->itemPath())); + oxLogError(m_editors.erase(i).error); + break; + } + ++i; + } + } + return {}; +} + ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept { if (!m_openFiles.contains(path)) { return {}; diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 6dfc730f..9c413e1c 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -45,6 +46,7 @@ class StudioUI: public ox::SignalHandler { NewMenu m_newMenu{keelCtx(m_tctx)}; DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; + ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; RenameFile m_renameFile; NewProject m_newProject; AboutPopup m_aboutPopup; @@ -114,6 +116,8 @@ class StudioUI: public ox::SignalHandler { ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; + ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; + ox::Error closeFile(ox::StringViewCR path) noexcept; ox::Error queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index 32855a63..542879e9 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -303,6 +303,34 @@ class FilePicker { }; +class QuestionPopup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_title; + ox::String m_question; + + public: + ox::Signal response; + + QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; + + void open() noexcept; + + void close() noexcept; + + [[nodiscard]] + bool isOpen() const noexcept; + + void draw(StudioContext &ctx, ImVec2 const &sz = {}) noexcept; + +}; + [[nodiscard]] bool mainWinHasFocus() noexcept; diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 4935dc8e..8bab9d2a 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -225,6 +225,59 @@ void FilePicker::show() noexcept { m_show = true; } + +QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept: + m_title{std::move(title)}, + m_question{std::move(question)} { +} + +void QuestionPopup::open() noexcept { + m_stage = Stage::Opening; +} + +void QuestionPopup::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool QuestionPopup::isOpen() const noexcept { + return m_open; +} + +void QuestionPopup::draw(StudioContext &ctx, ImVec2 const &sz) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(m_title.c_str()); + m_stage = Stage::Open; + m_open = true; + [[fallthrough]]; + case Stage::Open: + centerNextWindow(ctx.tctx); + ImGui::SetNextWindowSize(static_cast(sz)); + constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { + ImGui::Text("%s", m_question.c_str()); + auto const r = PopupControlsOkCancel(m_open, "Yes", "No"); + response.emit(r); + switch (r) { + case PopupResponse::None: + break; + case PopupResponse::OK: + close(); + break; + case PopupResponse::Cancel: + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + + bool s_mainWinHasFocus{}; bool mainWinHasFocus() noexcept { return s_mainWinHasFocus; From 4461f99fa4b23ea15de98cd4539e63886255acd4 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 2 Feb 2025 23:13:15 -0600 Subject: [PATCH 06/29] [studio] Add Ctrl-W shortcut for closing active tab --- src/olympic/studio/applib/src/studioapp.cpp | 27 +++++++++++++++------ src/olympic/studio/applib/src/studioapp.hpp | 2 ++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 61a06f12..82df7b2d 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -334,6 +334,14 @@ void StudioUI::handleKeyInput() noexcept { if (m_activeEditor && m_activeEditor->pasteEnabled()) { m_activeEditor->paste(); } + } else if (ImGui::IsKeyPressed(ImGuiKey_W)) { + if (m_activeEditor) { + if (m_activeEditor->unsavedChanges()) { + m_closeFileConfirm.open(); + } else { + oxLogError(closeCurrentFile()); + } + } } else if (ImGui::IsKeyPressed(ImGuiKey_X)) { if (m_activeEditor && m_activeEditor->cutEnabled()) { m_activeEditor->cut(); @@ -491,14 +499,19 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { if (response == ig::PopupResponse::OK && m_activeEditor) { - for (size_t i{}; auto &e : m_editors) { - if (m_activeEditor == e.get()) { - oxLogError(closeFile(e->itemPath())); - oxLogError(m_editors.erase(i).error); - break; - } - ++i; + return closeCurrentFile(); + } + return {}; +} + +ox::Error StudioUI::closeCurrentFile() noexcept { + for (size_t i{}; auto &e : m_editors) { + if (m_activeEditor == e.get()) { + oxLogError(closeFile(e->itemPath())); + oxLogError(m_editors.erase(i).error); + break; } + ++i; } return {}; } diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 9c413e1c..3ac1b308 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -118,6 +118,8 @@ class StudioUI: public ox::SignalHandler { ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; + ox::Error closeCurrentFile() noexcept; + ox::Error closeFile(ox::StringViewCR path) noexcept; ox::Error queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept; From 4e068d628cf778c73305218c48ef7451b6bf2a86 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 00:19:14 -0600 Subject: [PATCH 07/29] [studio] Fix misrender flash on tab close --- src/olympic/studio/applib/src/studioapp.cpp | 29 ++++++++++++++++----- src/olympic/studio/applib/src/studioapp.hpp | 1 + 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 82df7b2d..944939de 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -231,7 +231,10 @@ void StudioUI::drawTabs() noexcept { if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] { m_activeEditor->onActivated(); } - if (open) [[likely]] { + if (m_closeActiveTab) [[unlikely]] { + ImGui::SetTabItemClosed(e->itemDisplayName().c_str()); + + } else if (open) [[likely]] { e->draw(m_sctx); } m_activeEditorOnLastDraw = e.get(); @@ -258,6 +261,20 @@ void StudioUI::drawTabs() noexcept { ++it; } } + if (m_closeActiveTab) [[unlikely]] { + if (m_activeEditor) { + auto const idx = find_if( + m_editors.begin(), m_editors.end(), + [this](ox::UPtr const &e) { + return m_activeEditor == e.get(); + }); + if (idx != m_editors.end()) { + oxLogError(m_editors.erase(idx.offset()).error); + } + m_activeEditor = nullptr; + } + m_closeActiveTab = false; + } } void StudioUI::loadEditorMaker(EditorMaker const&editorMaker) noexcept { @@ -407,13 +424,12 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne } ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept { - for (size_t i{}; auto &e : m_editors) { + for (auto &e : m_editors) { if (path == e->itemPath()) { - oxLogError(m_editors.erase(i).error); oxLogError(closeFile(path)); + m_closeActiveTab = true; break; } - ++i; } return m_projectExplorer.refreshProjectTreeModel(); } @@ -505,13 +521,12 @@ ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) no } ox::Error StudioUI::closeCurrentFile() noexcept { - for (size_t i{}; auto &e : m_editors) { + for (auto &e : m_editors) { if (m_activeEditor == e.get()) { oxLogError(closeFile(e->itemPath())); - oxLogError(m_editors.erase(i).error); + m_closeActiveTab = true; break; } - ++i; } return {}; } diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 3ac1b308..0ced3dca 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -41,6 +41,7 @@ class StudioUI: public ox::SignalHandler { BaseEditor *m_activeEditorOnLastDraw = nullptr; BaseEditor *m_activeEditor = nullptr; BaseEditor *m_activeEditorUpdatePending = nullptr; + bool m_closeActiveTab{}; ox::Vector> m_queuedMoves; ox::Vector> m_queuedDirMoves; NewMenu m_newMenu{keelCtx(m_tctx)}; From 0abadc185047923e23c5af19dcac233487354a4b Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 00:35:37 -0600 Subject: [PATCH 08/29] [studio] Fix QuestionPopup to only emit a response when there is a response --- src/olympic/studio/modlib/src/imguiutil.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 8bab9d2a..dcb0f3fa 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -260,14 +260,15 @@ void QuestionPopup::draw(StudioContext &ctx, ImVec2 const &sz) noexcept { if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { ImGui::Text("%s", m_question.c_str()); auto const r = PopupControlsOkCancel(m_open, "Yes", "No"); - response.emit(r); switch (r) { case PopupResponse::None: break; case PopupResponse::OK: + response.emit(r); close(); break; case PopupResponse::Cancel: + response.emit(r); close(); break; } From 671dd86206db857581d88a8e8a143848169379ee Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 02:01:40 -0600 Subject: [PATCH 09/29] [keel,studio] Add Make Copy option to ProjectExplorer --- src/olympic/keel/include/keel/asset.hpp | 2 + src/olympic/keel/src/asset.cpp | 16 ++++- src/olympic/studio/applib/src/CMakeLists.txt | 1 + .../studio/applib/src/makecopypopup.cpp | 70 +++++++++++++++++++ .../studio/applib/src/makecopypopup.hpp | 42 +++++++++++ .../studio/applib/src/projectexplorer.cpp | 3 + .../studio/applib/src/projectexplorer.hpp | 1 + src/olympic/studio/applib/src/studioapp.cpp | 7 ++ src/olympic/studio/applib/src/studioapp.hpp | 4 ++ .../studio/modlib/include/studio/project.hpp | 2 + src/olympic/studio/modlib/src/project.cpp | 8 +++ 11 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/olympic/studio/applib/src/makecopypopup.cpp create mode 100644 src/olympic/studio/applib/src/makecopypopup.hpp diff --git a/src/olympic/keel/include/keel/asset.hpp b/src/olympic/keel/include/keel/asset.hpp index 35d53e61..5b25a2af 100644 --- a/src/olympic/keel/include/keel/asset.hpp +++ b/src/olympic/keel/include/keel/asset.hpp @@ -15,6 +15,8 @@ constexpr auto K1HdrSz = 40; ox::Result readUuidHeader(ox::BufferView buff) noexcept; +ox::Result regenerateUuidHeader(ox::Buffer &buff) noexcept; + ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept { OX_RETURN_ERROR(write(writer, "K1;")); OX_RETURN_ERROR(uuid.toString(writer)); diff --git a/src/olympic/keel/src/asset.cpp b/src/olympic/keel/src/asset.cpp index d4d907ef..b7b9023f 100644 --- a/src/olympic/keel/src/asset.cpp +++ b/src/olympic/keel/src/asset.cpp @@ -8,15 +8,25 @@ namespace keel { ox::Result readUuidHeader(ox::BufferView buff) noexcept { if (buff.size() < K1HdrSz) [[unlikely]] { - return ox::Error(1, "Insufficient data to contain complete Keel header"); + return ox::Error{1, "Insufficient data to contain complete Keel header"}; } constexpr ox::StringView k1Hdr = "K1;"; - if (k1Hdr != ox::StringView(buff.data(), k1Hdr.bytes())) [[unlikely]] { - return ox::Error(2, "No Keel asset header data"); + if (k1Hdr != ox::StringView{buff.data(), k1Hdr.bytes()}) [[unlikely]] { + return ox::Error{2, "No Keel asset header data"}; } return ox::UUID::fromString(ox::StringView(&buff[k1Hdr.bytes()], 36)); } +ox::Result regenerateUuidHeader(ox::Buffer &buff) noexcept { + OX_RETURN_ERROR(readUuidHeader(buff)); + OX_REQUIRE(id, ox::UUID::generate()); + auto const str = id.toString(); + for (size_t i = 0; i < ox::UUIDStr::cap(); ++i) { + buff[i + 3] = str[i]; + } + return id; +} + ox::Result readAsset(ox::TypeStore &ts, ox::BufferView buff) noexcept { std::size_t offset = 0; if (!readUuidHeader(buff).error) { diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index 59f238e2..773f36cf 100644 --- a/src/olympic/studio/applib/src/CMakeLists.txt +++ b/src/olympic/studio/applib/src/CMakeLists.txt @@ -5,6 +5,7 @@ add_library( deleteconfirmation.cpp filedialogmanager.cpp main.cpp + makecopypopup.cpp newdir.cpp newmenu.cpp newproject.cpp diff --git a/src/olympic/studio/applib/src/makecopypopup.cpp b/src/olympic/studio/applib/src/makecopypopup.cpp new file mode 100644 index 00000000..db6c4f3b --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.cpp @@ -0,0 +1,70 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include "makecopypopup.hpp" + +namespace studio { + +ox::Error MakeCopyPopup::open(ox::StringViewCR path) noexcept { + m_stage = Stage::Opening; + OX_REQUIRE(idx, ox::findIdx(path.rbegin(), path.rend(), '/')); + m_srcPath = path; + m_dirPath = substr(path, 0, idx + 1); + m_title = sfmt("Copy {}", path); + m_fileName = ""; + return {}; +} + +void MakeCopyPopup::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool MakeCopyPopup::isOpen() const noexcept { + return m_open; +} + +void MakeCopyPopup::draw(StudioContext const &ctx, ImVec2 const &sz) noexcept { + switch (m_stage) { + case Stage::Closed: + break; + case Stage::Opening: + ImGui::OpenPopup(m_title.c_str()); + m_stage = Stage::Open; + m_open = true; + [[fallthrough]]; + case Stage::Open: + ig::centerNextWindow(ctx.tctx); + ImGui::SetNextWindowSize(sz); + constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + ig::InputText("Name", m_fileName); + bool open = true; + switch (ig::PopupControlsOkCancel(open)) { + case ig::PopupResponse::None: + break; + case ig::PopupResponse::OK: + { + auto const p = sfmt("{}{}", m_dirPath, m_fileName); + if (!ctx.project->exists(p)) { + makeCopy.emit(m_srcPath, p); + close(); + } + } + break; + case ig::PopupResponse::Cancel: + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + + +} \ No newline at end of file diff --git a/src/olympic/studio/applib/src/makecopypopup.hpp b/src/olympic/studio/applib/src/makecopypopup.hpp new file mode 100644 index 00000000..10313506 --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.hpp @@ -0,0 +1,42 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +namespace studio { + +class MakeCopyPopup { + private: + enum class Stage { + Closed, + Opening, + Open, + }; + Stage m_stage = Stage::Closed; + bool m_open{}; + ox::String m_title{"Copy File"}; + ox::String m_srcPath; + ox::String m_dirPath; + ox::IString<255> m_fileName; + + public: + ox::Signal makeCopy; + + ox::Error open(ox::StringViewCR path) noexcept; + + void close() noexcept; + + [[nodiscard]] + bool isOpen() const noexcept; + + void draw(StudioContext const &ctx, ImVec2 const &sz = {}) noexcept; + +}; + +} diff --git a/src/olympic/studio/applib/src/projectexplorer.cpp b/src/olympic/studio/applib/src/projectexplorer.cpp index 20ba0dfa..d3b374db 100644 --- a/src/olympic/studio/applib/src/projectexplorer.cpp +++ b/src/olympic/studio/applib/src/projectexplorer.cpp @@ -42,6 +42,9 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { if (ImGui::MenuItem("Rename")) { renameItem.emit(path); } + if (ImGui::MenuItem("Make Copy")) { + makeCopy.emit(path); + } ImGui::EndPopup(); } } diff --git a/src/olympic/studio/applib/src/projectexplorer.hpp b/src/olympic/studio/applib/src/projectexplorer.hpp index c0b4420a..74bcaef2 100644 --- a/src/olympic/studio/applib/src/projectexplorer.hpp +++ b/src/olympic/studio/applib/src/projectexplorer.hpp @@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer { ox::Signal addDir; ox::Signal deleteItem; ox::Signal renameItem; + ox::Signal makeCopy; ox::Signal moveDir; ox::Signal moveItem; diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 944939de..37ef95d6 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -59,6 +59,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce m_projectExplorer.addItem.connect(this, &StudioUI::addFile); m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); + m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg); m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove); m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove); m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); @@ -136,6 +137,7 @@ void StudioUI::draw() noexcept { p->draw(m_sctx); } m_closeFileConfirm.draw(m_sctx); + m_copyFilePopup.draw(m_sctx, {250, 0}); } ImGui::End(); handleKeyInput(); @@ -451,6 +453,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { m_sctx.project = m_project.get(); turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); + m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem); m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); @@ -513,6 +516,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi return {}; } +ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept { + return m_copyFilePopup.open(path); +} + ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { if (response == ig::PopupResponse::OK && m_activeEditor) { return closeCurrentFile(); diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 0ced3dca..6809102a 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -16,6 +16,7 @@ #include "aboutpopup.hpp" #include "deleteconfirmation.hpp" +#include "makecopypopup.hpp" #include "newdir.hpp" #include "newmenu.hpp" #include "newproject.hpp" @@ -48,6 +49,7 @@ class StudioUI: public ox::SignalHandler { DeleteConfirmation m_deleteConfirmation; NewDir m_newDirDialog; ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; + MakeCopyPopup m_copyFilePopup; RenameFile m_renameFile; NewProject m_newProject; AboutPopup m_aboutPopup; @@ -117,6 +119,8 @@ class StudioUI: public ox::SignalHandler { ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; + ox::Error makeCopyDlg(ox::StringViewCR path) noexcept; + ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; ox::Error closeCurrentFile() noexcept; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index 76489c68..73b0e2b7 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -92,6 +92,8 @@ class Project: public ox::SignalHandler { ox::Result stat(ox::StringViewCR path) const noexcept; + ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; + ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept; diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index dbf34dbb..c4cde1cc 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -97,6 +97,14 @@ ox::Result Project::stat(ox::StringViewCR path) const noexcept { return m_fs.stat(path); } +ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { + OX_REQUIRE_M(buff, loadBuff(src)); + OX_REQUIRE(id, keel::regenerateUuidHeader(buff)); + OX_RETURN_ERROR(writeBuff(dest, buff)); + createUuidMapping(m_kctx, dest, id); + return {}; +} + ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { OX_RETURN_ERROR(m_fs.move(src, dest)); OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest)); From d45ff05bcd3b1d6ce11d32ac83b4bddcdbf14da2 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 20:28:25 -0600 Subject: [PATCH 10/29] [ox/fs] Add new partial file read functions --- deps/ox/src/ox/fs/filesystem/filesystem.cpp | 45 +++++++++++++++++-- deps/ox/src/ox/fs/filesystem/filesystem.hpp | 38 +++++++++++++++- .../ox/src/ox/fs/filesystem/passthroughfs.cpp | 19 ++++++++ .../ox/src/ox/fs/filesystem/passthroughfs.hpp | 3 ++ 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/deps/ox/src/ox/fs/filesystem/filesystem.cpp b/deps/ox/src/ox/fs/filesystem/filesystem.cpp index 105e2b93..07e2c390 100644 --- a/deps/ox/src/ox/fs/filesystem/filesystem.cpp +++ b/deps/ox/src/ox/fs/filesystem/filesystem.cpp @@ -37,6 +37,30 @@ Error FileSystem::read(const FileAddress &addr, void *buffer, std::size_t size) } } +Result FileSystem::read(FileAddress const &addr, size_t const size) noexcept { + Result out; + out.value.resize(size); + switch (addr.type()) { + case FileAddressType::Inode: + OX_RETURN_ERROR(readFileInode(addr.getInode().value, out.value.data(), size)); + break; + case FileAddressType::ConstPath: + case FileAddressType::Path: + OX_RETURN_ERROR(readFilePath(StringView{addr.getPath().value}, out.value.data(), size)); + break; + default: + return ox::Error{1}; + } + return out; +} + +Result FileSystem::read(StringViewCR path, size_t const size) noexcept { + Result out; + out.value.resize(size); + OX_RETURN_ERROR(readFilePath(path, out.value.data(), size)); + return out; +} + Result FileSystem::read(const FileAddress &addr) noexcept { OX_REQUIRE(s, stat(addr)); Buffer buff(static_cast(s.size)); @@ -51,18 +75,33 @@ Result FileSystem::read(StringViewCR path) noexcept { return buff; } -Error FileSystem::read(const FileAddress &addr, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept { +Error FileSystem::read( + FileAddress const &addr, + std::size_t const readStart, + std::size_t const readSize, + void *buffer, + std::size_t *size) noexcept { switch (addr.type()) { case FileAddressType::Inode: - return read(addr.getInode().value, readStart, readSize, buffer, size); + return readFileInodeRange(addr.getInode().value, readStart, readSize, buffer, size); case FileAddressType::ConstPath: case FileAddressType::Path: - return ox::Error(2, "Unsupported for path lookups"); + return readFilePathRange(addr.getPath().value, readStart, readSize, buffer, size); default: return ox::Error(1); } } +Result FileSystem::read( + StringViewCR path, + std::size_t const readStart, + std::size_t const readSize, + Span buff) noexcept { + size_t szOut{buff.size()}; + OX_RETURN_ERROR(readFilePathRange(path, readStart, readSize, buff.data(), &szOut)); + return szOut; +} + Error FileSystem::write(const FileAddress &addr, const void *buffer, uint64_t size, FileType fileType) noexcept { switch (addr.type()) { case FileAddressType::Inode: diff --git a/deps/ox/src/ox/fs/filesystem/filesystem.hpp b/deps/ox/src/ox/fs/filesystem/filesystem.hpp index ebc83217..fc7459e9 100644 --- a/deps/ox/src/ox/fs/filesystem/filesystem.hpp +++ b/deps/ox/src/ox/fs/filesystem/filesystem.hpp @@ -41,6 +41,10 @@ class FileSystem { Error read(const FileAddress &addr, void *buffer, std::size_t size) noexcept; + Result read(FileAddress const &addr, size_t size) noexcept; + + Result read(StringViewCR path, size_t size) noexcept; + Result read(const FileAddress &addr) noexcept; Result read(StringViewCR path) noexcept; @@ -53,7 +57,24 @@ class FileSystem { return readFileInode(inode, buffer, buffSize); } - Error read(const FileAddress &addr, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept; + Error read( + FileAddress const &addr, + size_t readStart, + size_t readSize, + void *buffer, + size_t *size) noexcept; + + /** + * + * @param path + * @param readStart + * @param readSize + * @param buffer + * @param size + * @return error or number of bytes read + */ + Result read( + StringViewCR path, size_t readStart, size_t readSize, ox::Span buff) noexcept; virtual Result> ls(StringViewCR dir) const noexcept = 0; @@ -140,7 +161,10 @@ class FileSystem { virtual Error readFileInode(uint64_t inode, void *buffer, std::size_t size) noexcept = 0; - virtual Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept = 0; + virtual Error readFilePathRange( + StringViewCR path, size_t readStart, size_t readSize, void *buffer, size_t *buffSize) noexcept = 0; + + virtual Error readFileInodeRange(uint64_t inode, size_t readStart, size_t readSize, void *buffer, size_t *size) noexcept = 0; virtual Error removePath(StringViewCR path, bool recursive) noexcept = 0; @@ -211,6 +235,9 @@ class FileSystemTemplate: public MemFS { Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override; + Error readFilePathRange( + StringViewCR path, size_t readStart, size_t readSize, void *buffer, size_t *buffSize) noexcept override; + Error removePath(StringViewCR path, bool recursive) noexcept override; Result directAccessInode(uint64_t) const noexcept override; @@ -358,6 +385,13 @@ Error FileSystemTemplate::readFileInodeRange(uint64_t inod return m_fs.read(inode, readStart, readSize, reinterpret_cast(buffer), size); } +template +Error FileSystemTemplate::readFilePathRange( + StringViewCR path, size_t readStart, size_t readSize, void *buffer, size_t *buffSize) noexcept { + OX_REQUIRE(s, stat(path)); + return readFileInodeRange(s.inode, readStart, readSize, buffer, buffSize); +} + template Error FileSystemTemplate::removePath(StringViewCR path, bool recursive) noexcept { OX_REQUIRE(fd, fileSystemData()); diff --git a/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp b/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp index 365d5bb7..fbf960bd 100644 --- a/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp +++ b/deps/ox/src/ox/fs/filesystem/passthroughfs.cpp @@ -154,6 +154,25 @@ Error PassThroughFS::readFileInode(uint64_t, void*, std::size_t) noexcept { return ox::Error(1, "readFileInode(uint64_t, void*, std::size_t) is not supported by PassThroughFS"); } +Error PassThroughFS::readFilePathRange( + StringViewCR path, size_t const readStart, size_t readSize, void *buffer, size_t *buffSize) noexcept { + try { + std::ifstream file(m_path / stripSlash(path), std::ios::binary | std::ios::ate); + auto const size = static_cast(file.tellg()); + readSize = ox::min(readSize, size); + file.seekg(static_cast(readStart), std::ios::beg); + if (readSize > *buffSize) { + oxTracef("ox.fs.PassThroughFS.read.error", "Read failed: Buffer too small: {}", path); + return ox::Error{1}; + } + file.read(static_cast(buffer), static_cast(readSize)); + return {}; + } catch (std::fstream::failure const &f) { + oxTracef("ox.fs.PassThroughFS.read.error", "Read of {} failed: {}", path, f.what()); + return ox::Error{2}; + } +} + Error PassThroughFS::readFileInodeRange(uint64_t, std::size_t, std::size_t, void*, std::size_t*) noexcept { // unsupported return ox::Error(1, "read(uint64_t, std::size_t, std::size_t, void*, std::size_t*) is not supported by PassThroughFS"); diff --git a/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp b/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp index a424cefe..4d9f255a 100644 --- a/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp +++ b/deps/ox/src/ox/fs/filesystem/passthroughfs.hpp @@ -71,6 +71,9 @@ class PassThroughFS: public FileSystem { Error readFileInode(uint64_t inode, void *buffer, std::size_t size) noexcept override; + Error readFilePathRange( + StringViewCR path, size_t readStart, size_t readSize, void *buffer, size_t *buffSize) noexcept override; + Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override; Error removePath(StringViewCR path, bool recursive) noexcept override; From 8b22a8f3395fd46cc056b7f7ed70fd5ab9e4a7ca Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 20:29:06 -0600 Subject: [PATCH 11/29] [keel] Make buildUuidMap only read the first 40 bytes of each file --- src/olympic/keel/src/media.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/olympic/keel/src/media.cpp b/src/olympic/keel/src/media.cpp index 01e6f90d..c6982d21 100644 --- a/src/olympic/keel/src/media.cpp +++ b/src/olympic/keel/src/media.cpp @@ -53,10 +53,12 @@ static ox::Error buildUuidMap(Context &ctx, ox::StringViewCR path) noexcept { OX_REQUIRE_M(filePath, ox::join("/", ox::Array{path, f})); OX_REQUIRE(stat, ctx.rom->stat(filePath)); if (stat.fileType == ox::FileType::NormalFile) { - OX_REQUIRE(data, ctx.rom->read(filePath)); - auto const [hdr, err] = readAssetHeader(data); + ox::Array buff; + OX_RETURN_ERROR( + ctx.rom->read(filePath, 0, buff.size(), buff)); + auto const [uuid, err] = readUuidHeader(buff); if (!err) { - createUuidMapping(ctx, filePath, hdr.uuid); + createUuidMapping(ctx, filePath, uuid); } } else if (stat.fileType == ox::FileType::Directory) { if (!beginsWith(f, ".")) { From 4ef31762d0dacb1c395dbcde88be82ecd752cc89 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 22:43:02 -0600 Subject: [PATCH 12/29] [nostalgia/core/studio/tilesheet] Cleanup --- .../gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp index 4b540fdf..b9452a1c 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp @@ -94,7 +94,7 @@ ox::Error RotateCommand::redo() noexcept { switch (m_dir) { case Direction::Left: rotateLeft(ss, m_pt1, m_pt2); - break; + break; case Direction::Right: rotateRight(ss, m_pt1, m_pt2); break; From 4e27a4c1f578ab7996776ee4ac66cd9a43b337f4 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 22:43:20 -0600 Subject: [PATCH 13/29] [nostalgia/core/studio/tilesheet] Fix palette path display update --- .../gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 2d0588a3..127b5209 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -300,6 +300,7 @@ ox::Error TileSheetEditorModel::markUpdatedCmdId(studio::UndoCommand const*cmd) m_pal = keel::AssetRef{}; } m_palettePage = ox::min(pal().pages.size(), 0); + setPalPath(); paletteChanged.emit(); } auto const tsCmd = dynamic_cast(cmd); From b7202a2b0d631aea3100fd5bef21c9360722aa43 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 22:48:07 -0600 Subject: [PATCH 14/29] [nostalgia/player] Disable Keel mods on GBA --- src/nostalgia/player/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/nostalgia/player/CMakeLists.txt b/src/nostalgia/player/CMakeLists.txt index 331df8ba..922b4787 100644 --- a/src/nostalgia/player/CMakeLists.txt +++ b/src/nostalgia/player/CMakeLists.txt @@ -9,6 +9,7 @@ if(NOT WIN32) endif() if(COMMAND OBJCOPY_FILE) + set(LOAD_KEEL_MODS FALSE) set_target_properties(Nostalgia PROPERTIES LINK_FLAGS ${LINKER_FLAGS} @@ -17,8 +18,16 @@ if(COMMAND OBJCOPY_FILE) OBJCOPY_FILE(Nostalgia) #PADBIN_FILE(Nostalgia) +else() + set(LOAD_KEEL_MODS TRUE) endif() +target_compile_definitions( + Nostalgia PRIVATE + OLYMPIC_LOAD_KEEL_MODULES=$ + OLYMPIC_GUI_APP=1 +) + target_link_libraries( Nostalgia NostalgiaKeelModules From d39d552bd91e11213f42577e5c2be84cc823ac39 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 3 Feb 2025 23:29:26 -0600 Subject: [PATCH 15/29] [nostalgia/studio] Update icon to higher resolution --- src/nostalgia/studio/CMakeLists.txt | 2 +- src/nostalgia/studio/Info.plist | 6 +++--- src/nostalgia/studio/ns_logo.icns | Bin 0 -> 113363 bytes 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 src/nostalgia/studio/ns_logo.icns diff --git a/src/nostalgia/studio/CMakeLists.txt b/src/nostalgia/studio/CMakeLists.txt index 6f21a60f..3a5a25a9 100644 --- a/src/nostalgia/studio/CMakeLists.txt +++ b/src/nostalgia/studio/CMakeLists.txt @@ -33,7 +33,7 @@ endif() install( FILES - ns.icns + ns_logo.icns DESTINATION ${NOSTALGIA_DIST_RESOURCES}/icons ) diff --git a/src/nostalgia/studio/Info.plist b/src/nostalgia/studio/Info.plist index ef0935ea..b2bd9f7a 100644 --- a/src/nostalgia/studio/Info.plist +++ b/src/nostalgia/studio/Info.plist @@ -9,7 +9,7 @@ Nostalgia Studio CFBundleIconFile - icons/ns.icns + icons/ns_logo.icns CFBundleIdentifier net.drinkingtea.nostalgia.studio @@ -18,7 +18,7 @@ APPL CFBundleVersion - 0.0.0 + dev build LSMinimumSystemVersion 12.0.0 @@ -30,6 +30,6 @@ True NSHumanReadableCopyright - Copyright (c) 2016-2023 Gary Talent <gary@drinkingtea.net> + Copyright (c) 2016-2025 Gary Talent <gary@drinkingtea.net> diff --git a/src/nostalgia/studio/ns_logo.icns b/src/nostalgia/studio/ns_logo.icns new file mode 100644 index 0000000000000000000000000000000000000000..5308f21787370dc4a9e192a6b9d1bd25d1bd0eb0 GIT binary patch literal 113363 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 z zr)Y-W)MG1?twm-Y8w&3a_Bp=&k)h;NXho@ASJR|Q#FAK_+rXAJ1SlJtN^wFh1EdoP zRg)((+RyAjPhhlMeuI3|0GCbTL&;Ze%frt&L;+40qjPbY@P}*BJ(nX z{?)6|^_rI*UNlq(VZ=INs0p`I>O~kSz7Cw1G@_go#P({98@Onbdf)3{kq$`G8_?EL zz&T}&1C$(8GR6oJ!AN($21cZzQ^~U|B;Z*xQ!9q{#j7k8U!YiNc=QufDFGr#8}_+R zBjhDKR-7~9k`J@{X++jS4jv=jJgHel&a5%J(4d2BGGeUO5a$IPe=aa)l8Y1W2bz!N zMBCijp#rQHco=#d=soYV?EaEJr$a}$Cu06l;?v&b&|W%H~_zHd;FW=Q}~ zwk(t*JAqISL+K7tM31+uy14mvD^0_`mQB=DHy-ei0(r?r8d0RTIdD-PNerO~C84OOF<^~X?#M%zIpR?-cyAds>sxec8f5muW%DP6x`84l_|IN~J{|VsD1WFY znjP)C|NMksi&pHvDB~sM+Fpd+@`E7KBG?czTzae%0v>H|8<^D0~IRye*C171$+5fy%C#s*T|8EhLU~L=K0 z>m5og7*fYVZ)%*lHUzhs+0v(xtnnx0Wx|fn$Z% z%S&%4Ci`5{O9iT>F9;5z_ZEtJxBQ@rv`Fq{Gf9A9BFPsez`zQKko_3k$RNA`g8;gc zX&U$=v54Zaiq)$-QgU(oyJ!O{kTVM`hMSPr{UkvQyPNSN92o62_86faP3a7+Di3z^ zSNLzeZ`&XesJX1JL?u$>Z@t6{w+FUvm6g z3rna^kwu%Mey>v%(3QA&u?klL>9m2;t1$10$hlH;+ay?&Rj)b>fs%6q1s`tT!WWG2 z-X2MK4ubWbCHI_Mf?-6~b8%*T%j4CafbUZx0_ubF#aqN3rm=ki=xc?u-xJ?rXvF?w zomRaLi_+VU8FVUb8K8aBzO%3(CwqmR91%6rO)kgZT(}l&?pC^HI_D2s9f%>r7iPeaCsP)d^Wf|n|q{) zWEhxu3*5~Z@6)Df?9Y!zR6*#SXz5;W}Y_UO)7cal5 zwt}Wk1-PeK;@^8vCJ?1Q4e!L4R<31S3!M5EEG3#c@aFIHvn$n%>TV_JW93lmz$2X% z5Pij1bv^yew~6u9qrMc+CkX3Q0L8hHszwlp0<>a%%GiZ3Z~IRiXkbP=-Ntn%r`n@A zPQ2AqD;qw7aZ~75aNzG2w@x;u>+4c zVSDeqpr{ayX5r&j0UtcTE`qei@#_H}3TiZIJTrDq?bPI)JNxpe{aVc|;@$omxbM30 zTssHh?6iv4s&;$IPOB0m zW_e(3GP%^@D)rpfmIHR`-6u|4ft0t#*h`lmTe5CLqAwlai_j`y+DRjr(==B-y4Eb+ z0`j9LUH>@aee~gq*th0xLlzO8&JKDs%~nXMzPf$IQ0*+~I=&T!b{eeMQ19?@Aq^|L zkm!{%J14wy`D|~mZ);;y>EOGnLh>r}2r2ktb{0*;ASF&{xqPUPHHEGC%87XkYm`JF zo^E7uErR+BEDQM}E5{kxuhknqpaV7r1bQsh-m(e5jQ1IqXb;zc$=uqhp4%f>2r;dp zA`R^nKY)G#w#C0cMjpqN47qe6R{RDylz%AQV{wFPP;(F`rU!k~vKJnbc5jw;A!N)(I(kqfPKpG#}+N>yQDy!@FIF>+}uVE4e+g|U;G ztKtW=8?MpZ^p=W0vqEI@X_O;Ul|`TfuIe)%0;unHy$oo*$FY({DA&0bWicVK5LUHn zFQN)leT+8o_LD`glU|oqs`13+TU5;vIHefSVizE`P_!>AtdJw3ohZfJ?v1V|0(WcX z;Xu2x*`qX9O8SmjRg!V3sr|@gV>5t0GDW2Pe464&Dy$}Hy2Lb2ZvYNi%>XRx{o)K7 z@Y{xtTlsfZ zz&BtTS^y(VR=1U&&Gk@)Q7)hu{qGP%RuEc4XAq(sM~Y9*J(jw6Bxn513XGEFoKYO| zuW05(GL^jbT{14x*obmF`oWju5c{%Y0~0Ego>@QpybdM6{gGC*TK(UB$ftB8_rtp3 zRR9b7-U1j75QB#iwt)^HEeV}32s=Ua!VfmUdOBcn8!11Ub&((Cc{*i6&~RUKAIqi} z`_(tEqk;qvQY}FIu;n8Na1?i=v}_jcA%u9>KcC~l zLZI|oT!*eO?eIqmXe{K5$)54*Tb;#U!;&gUur_}nCUvtP^Ly%{CMfdpEFoe_RswMr zn5tS@J{fR~1&Bnj3Y>GrMa-w43Y6}+6UlC=luJ;VDLCGuZM{$1 zq89_)!`_sQ1QyXjfR#DMVFVk%)C(V5C<~eaT3pb>Dh6Ebz+~zl@#SA}4dgH3s-?IJ zA`twYJ_Q(jv_T~Xvu!a4GubjlW+K<7k{fFwv7>+GA zxPVbUc!gFvUNT!!_s=%>fL!{iOK<$6$*VNis)_RjDL_NFJsOed4Cw_lgE+zVM+Lcq zJMt=s+Ax;_fCgOf6cUU8!YF=uKDNgI<| zQSNDhgX-LltF9gkbhAf8^Gt`Fdl(4Ye_E;g;s}489oLvOckRMJZx+%TmZGP9hjJdc z+XHB$d)~s~W~zrqYsJPYwxE?&@-Q1)@nL02P{iGk5HTy9F#Cw`3=~~Rte$&=EF_(b zOk~o{=m~62_lac>Gn>@e#sRgOxs$Wg+Q3@?6@-F_!nx4{t+u@zFduavpu4)NLjW<^ z2H30u@d+BhQ}vPjy4Cp{MENUye$b?9!j8$wTDc)9d2iwHi@h#@iMv7k(c>m=_N{=W zH5=_Y}j7O@{2UcitvLx;Yis}t0i))B zehtvUwSlaGfkgq-*SaaC>5^!rz zyl1knhb{fYpiLVxFGx4KnNy7;1)*Bpu$*bu_c${4LGKiKL`MaTW_H68 z`+?bx84qG2%7~iwtBHuTV>YQC03B#@flxnx!R*gF5>^p}E4}nS7Oqgev4~Y|Q^?f+ z(`mY4Ky@I*eP2J$TzX{)dxo@hv%x2;@x2#Na>>LJZayM9H59*ZJxXI$p!8bZP_2zD zoc?XBNyeaM9*i<}3Rs?ZTAs>}*kbnl;7;9Kf<`aXD#MB98<<}PcFW-0t1i^;AIW); zhEf|CKDALC>)RiW5Gp1v`=w~ZC~-Y+*ZZoSXeIF`jTQgwo#bytI6uD~t$$t_kcJb6 zmo%VegTHUTpIb6}H{;d-8P!6>)Zl~I7XOqPx+JXv!fLLD5-aSx=gMf)GFIJ29?Irn zgacax7lv|^eq0mDj3E5A`oxK%PANRl=~D55Lj0!-J)FKv*VM4CSv$WhcGRw%6ErZD z0i0GM5}2LU^z^tGC|JHIj{Wr%OILKfVl_0Sn3A%=yX+BlKP~Uj$nEWDynEPft3;n0 z|Bjkq)WiMz!e2>wn-5goa3=htfp5a7s4GW)TL}9fXZMpu`@f4{wbGyD{Qq72NALZg zw*0Jp{olpE?B4&W#Q!f%@VBM%wxAkED=e&mDiQE=<)Ybz!gG#~Two_o@bL*YJ-udT zB)&~{8}O3&mCF~d1JCjs4-sMDU%!VBmw_jKzw1Wl_zK(h{Q!Qs;AC~h+0>NpC~z&p z#~z@bL*`^YaM+&-@$zmks*)s(iMq0;yT+6Dfkr>h@{?+%~trBZSd&h`&5Nz2Od4ycC`6GjDtYFUD zKSvVHM6Y?HBLz;H3%K!}g!;VP<@x5yThoSXP-w*W@8|XzI4J7I#->U<6?w6j&T z^G%m>!&d5f#SXD6q@;*L4nCj1Ru#J4_o$0$Yn#09>T0CQNa|i{?^!wEi4_I*5Wi$g zQQX^%tVT4qIXH5qsf})5U%$AzAbna7{)cJTHJj=Bg$46v2G{XfU|`G1iQ*W|y}e;2 zde<9v5n5h7e@qZjKB24o;^5!F%PnII(He<@H!6v9ozk;OF*7gSVn($;Y`f#% zQVzGi^@$jGhVRk$2veDMqQ+hkaVGpv3L08|@C}neCj5?hhs*?S zhg4NvmbftNYWLO77AO^N)N-_x z^Es*0d*?^zMUls>qEMd9^Njnquh!Fs9J|T4uiXeT3VwLgy}Ye0Rr`|oix(dsH;yU} z3tq^ro1z6@6m-(~aP_`0f9QKrq}U7MqXc~ax*{OA}``;rm+tG9V`@da5#+p!ai%!xaVai)&Y*;#c# z(;yMSESd4~s3$MY;}t{?GQSV>)mH61mYQ){xEw0Jd-s6{55!)-Q;u6)w9rS{%(uq` z_J-$uT`!3(E z`}PfUS}Js%Tup=6qcr2C9FTifA7wtS+}L{;BwNQl&$ zx(`)bj8u3Oz$7E<4_@Wiv8MOlTKQA`*trQOXN5YE+>V1bkFZxeUSP*zW#V+oKwgO4 zFwO%j9`QF;j}J09WM+E)F(*nOIO8i{_s#@UNb~if_-NN@`F@L8kH4}^<}UEvm?U>R zUvk=JI4U~!ARM@mKa1_P3sXM?>})1W_XUj;cZHEpr5%-)vOKx!uW~##NHs~|q_NBm zKFz7&IkCZK%$uVTU$&XRhR0H`lUryBtUSJ9Z_(3AGX5{;MP6T1f90x4*kbJahu8^c z;PWb9WPyfPjoD;m2eRK{+Bx>RycDlBerr=uLgBd7#e3rNH~C> z)iBs6p{R@hy?fgV3`%+uKm z02|0%Z+92J2gi?h^MeEgg@m_kvpVGO0+G!h~;n-tlmPO6&n%a(V!} z^WcuV7hKKP$=S)*$rI*u(*@?`x$&8D`}SmKhY5Fr Date: Mon, 3 Feb 2025 23:30:07 -0600 Subject: [PATCH 16/29] [studio] Give MakeCopy popup an error message for files that already exist --- .../studio/applib/src/makecopypopup.cpp | 33 ++++++++++++------- .../studio/applib/src/makecopypopup.hpp | 5 ++- src/olympic/studio/applib/src/studioapp.cpp | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/olympic/studio/applib/src/makecopypopup.cpp b/src/olympic/studio/applib/src/makecopypopup.cpp index db6c4f3b..3866a782 100644 --- a/src/olympic/studio/applib/src/makecopypopup.cpp +++ b/src/olympic/studio/applib/src/makecopypopup.cpp @@ -13,6 +13,7 @@ ox::Error MakeCopyPopup::open(ox::StringViewCR path) noexcept { m_dirPath = substr(path, 0, idx + 1); m_title = sfmt("Copy {}", path); m_fileName = ""; + m_errMsg = ""; return {}; } @@ -25,7 +26,7 @@ bool MakeCopyPopup::isOpen() const noexcept { return m_open; } -void MakeCopyPopup::draw(StudioContext const &ctx, ImVec2 const &sz) noexcept { +void MakeCopyPopup::draw(StudioContext const &ctx) noexcept { switch (m_stage) { case Stage::Closed: break; @@ -36,25 +37,26 @@ void MakeCopyPopup::draw(StudioContext const &ctx, ImVec2 const &sz) noexcept { [[fallthrough]]; case Stage::Open: ig::centerNextWindow(ctx.tctx); - ImGui::SetNextWindowSize(sz); - constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + ImGui::SetNextWindowSize({250, 0}); + constexpr auto modalFlags = + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize; if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); } ig::InputText("Name", m_fileName); + if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + accept(ctx); + } + ImGui::Text("%s", m_errMsg.c_str()); bool open = true; switch (ig::PopupControlsOkCancel(open)) { case ig::PopupResponse::None: break; case ig::PopupResponse::OK: - { - auto const p = sfmt("{}{}", m_dirPath, m_fileName); - if (!ctx.project->exists(p)) { - makeCopy.emit(m_srcPath, p); - close(); - } - } + accept(ctx); break; case ig::PopupResponse::Cancel: close(); @@ -66,5 +68,14 @@ void MakeCopyPopup::draw(StudioContext const &ctx, ImVec2 const &sz) noexcept { } } +void MakeCopyPopup::accept(StudioContext const &ctx) noexcept { + auto const p = sfmt("{}{}", m_dirPath, m_fileName); + if (!ctx.project->exists(p)) { + makeCopy.emit(m_srcPath, p); + close(); + } else { + m_errMsg = sfmt("{} already exists", p); + } +} -} \ No newline at end of file +} diff --git a/src/olympic/studio/applib/src/makecopypopup.hpp b/src/olympic/studio/applib/src/makecopypopup.hpp index 10313506..18ce9684 100644 --- a/src/olympic/studio/applib/src/makecopypopup.hpp +++ b/src/olympic/studio/applib/src/makecopypopup.hpp @@ -20,6 +20,7 @@ class MakeCopyPopup { }; Stage m_stage = Stage::Closed; bool m_open{}; + ox::String m_errMsg; ox::String m_title{"Copy File"}; ox::String m_srcPath; ox::String m_dirPath; @@ -35,8 +36,10 @@ class MakeCopyPopup { [[nodiscard]] bool isOpen() const noexcept; - void draw(StudioContext const &ctx, ImVec2 const &sz = {}) noexcept; + void draw(StudioContext const &ctx) noexcept; + private: + void accept(StudioContext const &ctx) noexcept; }; } diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 37ef95d6..ea2b04bc 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -137,7 +137,7 @@ void StudioUI::draw() noexcept { p->draw(m_sctx); } m_closeFileConfirm.draw(m_sctx); - m_copyFilePopup.draw(m_sctx, {250, 0}); + m_copyFilePopup.draw(m_sctx); } ImGui::End(); handleKeyInput(); From b4798fd2ab628804fd7798c1e36e425f2aa33806 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Wed, 5 Feb 2025 01:54:41 -0600 Subject: [PATCH 17/29] [nostalgia/gfx/studio/tilesheet] Make rotate only available for square subsheets or selections --- .../tilesheeteditor/tilesheeteditor-imgui.cpp | 39 ++++++++----------- .../tilesheeteditor/tilesheeteditormodel.cpp | 10 +++++ .../tilesheeteditor/tilesheeteditormodel.hpp | 3 ++ 3 files changed, 29 insertions(+), 23 deletions(-) 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 48c7583b..8b79ae42 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -248,29 +248,22 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept { ImGui::EndChild(); ImGui::BeginChild("OperationsBox", {0, 35}, ImGuiWindowFlags_NoTitleBar); { - size_t i{}; - if (ig::ComboBox("##Operations", ox::SpanView{{ - ox::CStringView{"Operations"}, - ox::CStringView{"Flip X"}, - ox::CStringView{"Flip Y"}, - ox::CStringView{"Rotate Left"}, - ox::CStringView{"Rotate Right"}, - }}, i)) { - switch (i) { - case 1: - oxLogError(m_model.flipX()); - break; - case 2: - oxLogError(m_model.flipY()); - break; - case 3: - oxLogError(m_model.rotateLeft()); - break; - case 4: - oxLogError(m_model.rotateRight()); - break; - default:; - } + if (ImGui::BeginCombo("##Operations", "Operations", 0)) { + if (ImGui::Selectable("Flip X", false)) { + oxLogError(m_model.flipX()); + } + if (ImGui::Selectable("Flip Y", false)) { + oxLogError(m_model.flipY()); + } + ImGui::BeginDisabled(!m_model.rotateEligible()); + if (ImGui::Selectable("Rotate Left", false)) { + oxLogError(m_model.rotateLeft()); + } + if (ImGui::Selectable("Rotate Right", false)) { + oxLogError(m_model.rotateRight()); + } + ImGui::EndDisabled(); + ImGui::EndCombo(); } } ImGui::EndChild(); diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 127b5209..52b91f40 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -352,6 +352,16 @@ ox::Error TileSheetEditorModel::flipY() noexcept { return pushCommand(ox::make(m_img, m_activeSubsSheetIdx, a, b)); } +bool TileSheetEditorModel::rotateEligible() const noexcept { + if (m_selection) { + auto const w = m_selection->b.x - m_selection->a.x; + auto const h = m_selection->b.y - m_selection->a.y; + return w == h; + } + auto const &ss = activeSubSheet(); + return ss.rows == ss.columns; +} + ox::Error TileSheetEditorModel::moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept { return pushCommand(ox::make(m_img, std::move(src), std::move(dst))); } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp index 390c9aef..f1c60a56 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp @@ -138,6 +138,9 @@ class TileSheetEditorModel: public ox::SignalHandler { ox::Error flipY() noexcept; + [[nodiscard]] + bool rotateEligible() const noexcept; + ox::Error moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept; private: From e002109829a3bcb484f64f4531ceb2badc472479 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Wed, 5 Feb 2025 20:26:03 -0600 Subject: [PATCH 18/29] [studio] Make undo/redo skip over obsolete commands --- src/olympic/studio/modlib/src/undostack.cpp | 31 ++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/olympic/studio/modlib/src/undostack.cpp b/src/olympic/studio/modlib/src/undostack.cpp index f6725017..81ef2f46 100644 --- a/src/olympic/studio/modlib/src/undostack.cpp +++ b/src/olympic/studio/modlib/src/undostack.cpp @@ -7,9 +7,7 @@ namespace studio { ox::Error UndoStack::push(ox::UPtr &&cmd) noexcept { - for (auto const i = m_stackIdx; i < m_stack.size();) { - std::ignore = m_stack.erase(i); - } + m_stack.resize(m_stackIdx); OX_RETURN_ERROR(cmd->redo()); redoTriggered.emit(cmd.get()); changeTriggered.emit(cmd.get()); @@ -25,22 +23,29 @@ ox::Error UndoStack::push(ox::UPtr &&cmd) noexcept { } ox::Error UndoStack::redo() noexcept { - if (m_stackIdx < m_stack.size()) { - auto &c = m_stack[m_stackIdx]; - OX_RETURN_ERROR(c->redo()); + while (m_stackIdx < m_stack.size()) { + auto const &c = m_stack[m_stackIdx]; ++m_stackIdx; - redoTriggered.emit(c.get()); - changeTriggered.emit(c.get()); + if (!c->isObsolete()) { + OX_RETURN_ERROR(c->redo()); + redoTriggered.emit(c.get()); + changeTriggered.emit(c.get()); + break; + } } return {}; } ox::Error UndoStack::undo() noexcept { - if (m_stackIdx) { - auto &c = m_stack[--m_stackIdx]; - OX_RETURN_ERROR(c->undo()); - undoTriggered.emit(c.get()); - changeTriggered.emit(c.get()); + while (m_stackIdx) { + --m_stackIdx; + auto const &c = m_stack[m_stackIdx]; + if (!c->isObsolete()) { + OX_RETURN_ERROR(c->undo()); + undoTriggered.emit(c.get()); + changeTriggered.emit(c.get()); + break; + } } return {}; } From 00638bc812e334777b5f070e746b90ed079215a4 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Wed, 5 Feb 2025 20:26:47 -0600 Subject: [PATCH 19/29] [nostalgia/gfx/studio/tilesheet] Mark DrawCommands as obsolete if no changes --- .../studio/tilesheeteditor/commands/drawcommand.cpp | 12 +++++++++--- .../studio/tilesheeteditor/commands/drawcommand.hpp | 2 ++ .../studio/tilesheeteditor/tilesheeteditormodel.cpp | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp index b84b0f6a..0b8f59a7 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp @@ -63,7 +63,7 @@ DrawCommand::DrawCommand( TileSheet &img, TileSheet::SubSheetIdx subSheetIdx, std::size_t idx, - int palIdx) noexcept: + int const palIdx) noexcept: m_img(img), m_subSheetIdx(std::move(subSheetIdx)), m_palIdx(palIdx) { @@ -75,7 +75,7 @@ DrawCommand::DrawCommand( TileSheet &img, TileSheet::SubSheetIdx subSheetIdx, ox::SpanView const&idxList, - int palIdx) noexcept: + int const palIdx) noexcept: m_img(img), m_subSheetIdx(std::move(subSheetIdx)), m_palIdx(palIdx) { @@ -123,7 +123,9 @@ void DrawCommand::lineUpdate(ox::Point a, ox::Point b) noexcept { for (int32_t i{}; i < range; ++i) { auto const idx = ptToIdx(x, y + i * mod, ss.columns * TileWidth); if (idx < ss.pixels.size()) { - m_changes.emplace_back(static_cast(idx), getPixel(ss, idx)); + if (m_palIdx != getPixel(ss, idx)) { + m_changes.emplace_back(static_cast(idx), getPixel(ss, idx)); + } } } }); @@ -154,4 +156,8 @@ TileSheet::SubSheetIdx const&DrawCommand::subsheetIdx() const noexcept { return m_subSheetIdx; } +void DrawCommand::finish() noexcept { + setObsolete(m_changes.empty()); +} + } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.hpp index a9945e72..84b21188 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.hpp @@ -52,6 +52,8 @@ class DrawCommand: public TileSheetCommand { [[nodiscard]] TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override; + void finish() noexcept; + }; } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 52b91f40..d64fa945 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -172,7 +172,7 @@ void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t cons if (m_ongoingDrawCommand) { m_ongoingDrawCommand->lineUpdate(m_lineStartPt, pt); m_updated = true; - } else if (getPixel(activeSubSheet, idx) != palIdx) { + } else { std::ignore = pushCommand(ox::make( m_img, m_activeSubsSheetIdx, idx, static_cast(palIdx))); m_lineStartPt = pt; @@ -180,7 +180,10 @@ void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t cons } void TileSheetEditorModel::endDrawCommand() noexcept { - m_ongoingDrawCommand = nullptr; + if (m_ongoingDrawCommand) { + m_ongoingDrawCommand->finish(); + m_ongoingDrawCommand = nullptr; + } } void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept { From 3089cd7afce53c7101b1ae6c4171ecaaea4793cd Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Fri, 7 Feb 2025 20:34:22 -0600 Subject: [PATCH 20/29] Change builder type to olympic --- .gitea/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 25898e4b..2338796a 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: nostalgia + runs-on: olympic steps: - name: Check out repository code uses: actions/checkout@v3 From 713aec887b6f6746a9a1a84d9135a91037f92d20 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Fri, 7 Feb 2025 20:38:44 -0600 Subject: [PATCH 21/29] [buildcore] Change mypy invokation --- deps/buildcore/base.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/buildcore/base.mk b/deps/buildcore/base.mk index ff56f632..136d29cc 100644 --- a/deps/buildcore/base.mk +++ b/deps/buildcore/base.mk @@ -93,7 +93,7 @@ purge: ${BC_CMD_RM_RF} compile_commands.json .PHONY: test test: build - ${BC_CMD_ENVRUN} mypy ${BC_VAR_SCRIPTS} + ${BC_CMD_ENVRUN} ${BC_CMD_PY3} -m mypy ${BC_VAR_SCRIPTS} ${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} test .PHONY: test-verbose test-verbose: build From df2c7e2b67410389e835c0a400fbde4e67c03933 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sat, 8 Feb 2025 18:10:49 -0600 Subject: [PATCH 22/29] [nostalgia] Update release notes --- release-notes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index e9c37a9c..76ef85aa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,7 +5,9 @@ * Add TileSheetV5. TileSheetV5 retains the bpp field for the sake of CompactTileSheet, but always store it pixel as 8 bpp for itself. * Add ability to move subsheets in the subsheet tree. -* Add Flip X and Flip Y button for TileSheet Editor. +* Add Flip X and Flip Y functionality to TileSheet Editor. +* Add rotate functionality to TileSheet Editor. +* Add draw line tool to TileSheet editor * Replace file picker combo boxes with a browse button and file picker, and support for dragging files from the project explorer. * Add ability to create directories. @@ -15,3 +17,5 @@ * Fix Palette Editor to ignore keyboard input when popups are open. * Palette Editor move color mechanism now uses drag and drop. * Add ability to reorder Palette pages. +* Add warning for closing a tab with unsaved changes. +* Add ability to close a tab with Ctrl/Cmd-W From 12bb7475fc41069de3239903d83d543e4b85ee45 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 18 Feb 2025 20:19:51 -0600 Subject: [PATCH 23/29] [nostalgia/gfx/studio/tilesheet] Adjust pixel line size on Windows --- .../gfx/src/studio/tilesheeteditor/tilesheetpixelgrid.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheetpixelgrid.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheetpixelgrid.cpp index 414aa316..5abb0b21 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheetpixelgrid.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheetpixelgrid.cpp @@ -22,7 +22,12 @@ ox::Error TileSheetGrid::buildShader() noexcept { } void TileSheetGrid::draw(bool update, ox::Vec2 const&scroll) noexcept { - glLineWidth(3 * m_pixelSizeMod * 0.5f); + // the lines just show up bigger on Windows for some reason + if constexpr(ox::defines::OS == ox::OS::Windows) { + glLineWidth(3 * m_pixelSizeMod * 0.25f); + } else { + glLineWidth(3 * m_pixelSizeMod * 0.5f); + } glUseProgram(m_shader); glBindVertexArray(m_bufferSet.vao); if (update) { From d62f913855f67d4aacd07d38de18160f3b17a954 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 18 Feb 2025 20:22:56 -0600 Subject: [PATCH 24/29] [nostalgia/gfx] Suppress some superfluous warnings --- .../gfx/include/nostalgia/gfx/tilesheet.hpp | 13 +++++++------ src/nostalgia/modules/gfx/src/tilesheet.cpp | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp index 30355112..fad20047 100644 --- a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp +++ b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp @@ -443,23 +443,24 @@ ox::Error resizeSubsheet(TileSheet::SubSheet &ss, ox::Size const&sz) noexcept; [[nodiscard]] TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubSheetIdx idx) noexcept; -[[nodiscard]] -TileSheet::SubSheet const&getSubSheet( - ox::SpanView const&idx, - std::size_t idxIt, - TileSheet::SubSheet const&pSubsheet) noexcept; - [[nodiscard]] TileSheet::SubSheet &getSubSheet( ox::SpanView const&idx, std::size_t idxIt, TileSheet::SubSheet &pSubsheet) noexcept; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdangling-reference" +#endif [[nodiscard]] TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, ox::SpanView const &idx) noexcept; [[nodiscard]] TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView const &idx) noexcept; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept; diff --git a/src/nostalgia/modules/gfx/src/tilesheet.cpp b/src/nostalgia/modules/gfx/src/tilesheet.cpp index fb4cc4e8..d17f2d99 100644 --- a/src/nostalgia/modules/gfx/src/tilesheet.cpp +++ b/src/nostalgia/modules/gfx/src/tilesheet.cpp @@ -187,7 +187,11 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe return validateSubSheetIdx(std::move(idx), 0, ts.subsheet); } -TileSheet::SubSheet const&getSubSheet( +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdangling-reference" +#endif +static TileSheet::SubSheet const&getSubSheet( ox::SpanView const &idx, std::size_t const idxIt, TileSheet::SubSheet const &pSubsheet) noexcept { @@ -200,6 +204,9 @@ TileSheet::SubSheet const&getSubSheet( } return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[currentIdx]); } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif TileSheet::SubSheet &getSubSheet( ox::SpanView const &idx, @@ -211,13 +218,20 @@ TileSheet::SubSheet &getSubSheet( return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]); } +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdangling-reference" +#endif TileSheet::SubSheet const&getSubSheet(TileSheet const &ts, ox::SpanView const &idx) noexcept { return gfx::getSubSheet(idx, 0, ts.subsheet); } -TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView const&idx) noexcept { +TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView const &idx) noexcept { return gfx::getSubSheet(idx, 0, ts.subsheet); } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept { auto &parent = getSubSheet(ts, idx); From a17abe46397f827ac1f07ec4842fb74d6e71555e Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 13 Feb 2025 23:30:31 -0600 Subject: [PATCH 25/29] [nfde] Up required CMake version --- deps/nfde/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/nfde/CMakeLists.txt b/deps/nfde/CMakeLists.txt index 1c4bf69a..3fd63498 100644 --- a/deps/nfde/CMakeLists.txt +++ b/deps/nfde/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.19) project(nativefiledialog-extended VERSION 1.1.1) set(nfd_ROOT_PROJECT OFF) From 5979e9885ea8f71f6f9db653a4d0e88aee361312 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 13 Feb 2025 23:37:19 -0600 Subject: [PATCH 26/29] [jsoncpp] Up required CMake version --- deps/ox/deps/jsoncpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/ox/deps/jsoncpp/CMakeLists.txt b/deps/ox/deps/jsoncpp/CMakeLists.txt index de7f3902..678e18fa 100644 --- a/deps/ox/deps/jsoncpp/CMakeLists.txt +++ b/deps/ox/deps/jsoncpp/CMakeLists.txt @@ -12,7 +12,7 @@ # CMake versions greater than the JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION policies will # continue to generate policy warnings "CMake Warning (dev)...Policy CMP0XXX is not set:" # -set(JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION "3.8.0") +set(JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION "3.13.2") set(JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION "3.13.2") cmake_minimum_required(VERSION ${JSONCPP_OLDEST_VALIDATED_POLICIES_VERSION}) if("${CMAKE_VERSION}" VERSION_LESS "${JSONCPP_NEWEST_VALIDATED_POLICIES_VERSION}") From fefb876fe7b12a21f22db36a603e862b93f22967 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 18 Feb 2025 20:33:29 -0600 Subject: [PATCH 27/29] [nostalgia/gfx] Add checks for GCC version for warning suppression --- .../modules/gfx/include/nostalgia/gfx/tilesheet.hpp | 4 ++-- src/nostalgia/modules/gfx/src/tilesheet.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp index fad20047..8a6bdabb 100644 --- a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp +++ b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp @@ -449,7 +449,7 @@ TileSheet::SubSheet &getSubSheet( std::size_t idxIt, TileSheet::SubSheet &pSubsheet) noexcept; -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdangling-reference" #endif @@ -458,7 +458,7 @@ TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, ox::SpanView [[nodiscard]] TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView const &idx) noexcept; -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic pop #endif diff --git a/src/nostalgia/modules/gfx/src/tilesheet.cpp b/src/nostalgia/modules/gfx/src/tilesheet.cpp index d17f2d99..c67bcf13 100644 --- a/src/nostalgia/modules/gfx/src/tilesheet.cpp +++ b/src/nostalgia/modules/gfx/src/tilesheet.cpp @@ -187,7 +187,7 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe return validateSubSheetIdx(std::move(idx), 0, ts.subsheet); } -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdangling-reference" #endif @@ -204,7 +204,7 @@ static TileSheet::SubSheet const&getSubSheet( } return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[currentIdx]); } -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic pop #endif @@ -218,7 +218,7 @@ TileSheet::SubSheet &getSubSheet( return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]); } -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdangling-reference" #endif @@ -229,7 +229,7 @@ TileSheet::SubSheet const&getSubSheet(TileSheet const &ts, ox::SpanView const &idx) noexcept { return gfx::getSubSheet(idx, 0, ts.subsheet); } -#ifdef __GNUC__ +#if defined(__GNUC__) && __GNUC__ >= 14 #pragma GCC diagnostic pop #endif From 998066d377d2fcee0ed90206415691006cc61de3 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 18 Feb 2025 21:46:41 -0600 Subject: [PATCH 28/29] [ox/std] Add comparison functions --- deps/ox/src/ox/std/utility.hpp | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/deps/ox/src/ox/std/utility.hpp b/deps/ox/src/ox/std/utility.hpp index 1c628e17..f1e55301 100644 --- a/deps/ox/src/ox/std/utility.hpp +++ b/deps/ox/src/ox/std/utility.hpp @@ -27,6 +27,48 @@ constexpr void swap(T &a, T &b) noexcept { b = std::move(temp); } +template +constexpr bool cmp_equal(T const t, U const u) noexcept { + if constexpr(ox::is_signed_v == ox::is_signed_v) { + return t == u; + } else if constexpr(ox::is_signed_v) { + return ox::Signed{t} == u; + } else { + return t == ox::Signed{u}; + } +} + +template +constexpr bool cmp_less(T const t, U const u) noexcept { + if constexpr(ox::is_signed_v == ox::is_signed_v) { + return t < u; + } else if constexpr(ox::is_signed_v) { + return ox::Signed{t} < u; + } else { + return t < ox::Signed{u}; + } +} + +template +constexpr bool cmp_not_equal(T const t, U const u) noexcept { + return !std::cmp_equal(t, u); +} + +template +constexpr bool cmp_greater(T const t, U const u) noexcept { + return std::cmp_less(u, t); +} + +template +constexpr bool cmp_less_equal(T const t, U const u) noexcept { + return !std::cmp_less(u, t); +} + +template +constexpr bool cmp_greater_equal(T const t, U const u) noexcept { + return !std::cmp_less(t, u); +} + } #endif From 2a8e3c2dc44642fd9fef6bc8b645ad966f0277da Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Tue, 18 Feb 2025 23:01:15 -0600 Subject: [PATCH 29/29] [nostalgia/gfx] Remove unnecessary cast --- src/nostalgia/modules/gfx/src/opengl/gfx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nostalgia/modules/gfx/src/opengl/gfx.cpp b/src/nostalgia/modules/gfx/src/opengl/gfx.cpp index bed68f7a..e6e26d4b 100644 --- a/src/nostalgia/modules/gfx/src/opengl/gfx.cpp +++ b/src/nostalgia/modules/gfx/src/opengl/gfx.cpp @@ -449,7 +449,7 @@ static void setSprite( ++i; }; if (!s.flipX) { - for (auto yIt = 0; yIt < static_cast(dim.y); ++yIt) { + for (auto yIt = 0u; yIt < dim.y; ++yIt) { for (auto xIt = 0u; xIt < dim.x; ++xIt) { set(static_cast(xIt), static_cast(yIt), s.enabled); }