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 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 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) 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}") 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; 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 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 diff --git a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp index 30355112..8a6bdabb 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; +#if defined(__GNUC__) && __GNUC__ >= 14 +#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; +#if defined(__GNUC__) && __GNUC__ >= 14 +#pragma GCC diagnostic pop +#endif ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept; 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); } 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/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/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..b9452a1c --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.cpp @@ -0,0 +1,126 @@ +/* + * 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, + ox::Point const &pt1, + ox::Point const &pt2, + int const depth = 0) noexcept { + if (depth >= 4) { + return; + } + 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 - pt1, pt1, pt2, depth + 1); + dst = src; +} + +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, + ox::Point const &pt1, + ox::Point const &pt2, + int const depth = 0) noexcept { + if (depth >= 4) { + return; + } + 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 - pt1, pt1, pt2, depth + 1); + dst = src; +} + +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); + } + } +} + + +RotateCommand::RotateCommand( + TileSheet &img, + TileSheet::SubSheetIdx idx, + 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(ss, m_pt1, m_pt2); + break; + case Direction::Right: + 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(ss, m_pt1, m_pt2); + break; + case Direction::Right: + rotateLeft(ss, m_pt1, m_pt2); + 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..2418b1a1 --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/rotatecommand.hpp @@ -0,0 +1,47 @@ +/* + * 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; + 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; + + [[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..8b79ae42 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -246,15 +246,24 @@ 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{55, 16}; - if (ig::PushButton("Flip X", btnSz)) { - oxLogError(m_model.flipX()); - } - ImGui::SameLine(); - if (ig::PushButton("Flip Y", btnSz)) { - oxLogError(m_model.flipY()); + 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 954ef891..d64fa945 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 { @@ -171,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; @@ -179,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 { @@ -237,6 +241,28 @@ 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, 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, pt1, pt2, RotateCommand::Direction::Right)); +} + void TileSheetEditorModel::setSelection(studio::Selection const&sel) noexcept { m_selection.emplace(sel); m_updated = true; @@ -277,6 +303,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); @@ -328,6 +355,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 95cab4f4..f1c60a56 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; @@ -134,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: 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) { diff --git a/src/nostalgia/modules/gfx/src/tilesheet.cpp b/src/nostalgia/modules/gfx/src/tilesheet.cpp index fb4cc4e8..c67bcf13 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( +#if defined(__GNUC__) && __GNUC__ >= 14 +#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]); } +#if defined(__GNUC__) && __GNUC__ >= 14 +#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]]); } +#if defined(__GNUC__) && __GNUC__ >= 14 +#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); } +#if defined(__GNUC__) && __GNUC__ >= 14 +#pragma GCC diagnostic pop +#endif ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept { auto &parent = getSubSheet(ts, idx); 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 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 00000000..5308f217 Binary files /dev/null and b/src/nostalgia/studio/ns_logo.icns differ 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/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, ".")) { 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..3866a782 --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.cpp @@ -0,0 +1,81 @@ +/* + * 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 = ""; + m_errMsg = ""; + 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) 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({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: + accept(ctx); + break; + case ig::PopupResponse::Cancel: + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + +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); + } +} + +} diff --git a/src/olympic/studio/applib/src/makecopypopup.hpp b/src/olympic/studio/applib/src/makecopypopup.hpp new file mode 100644 index 00000000..18ce9684 --- /dev/null +++ b/src/olympic/studio/applib/src/makecopypopup.hpp @@ -0,0 +1,45 @@ +/* + * 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_errMsg; + 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) noexcept; + + private: + void accept(StudioContext const &ctx) 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 3cbe35a9..ea2b04bc 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -59,11 +59,13 @@ 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); 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 +136,8 @@ void StudioUI::draw() noexcept { for (auto const p : m_popups) { p->draw(m_sctx); } + m_closeFileConfirm.draw(m_sctx); + m_copyFilePopup.draw(m_sctx); } ImGui::End(); handleKeyInput(); @@ -214,7 +218,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(); @@ -229,7 +233,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(); @@ -237,21 +244,39 @@ 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; } } + 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 { @@ -328,6 +353,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(); @@ -393,13 +426,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(); } @@ -421,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); @@ -483,6 +516,28 @@ 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(); + } + return {}; +} + +ox::Error StudioUI::closeCurrentFile() noexcept { + for (auto &e : m_editors) { + if (m_activeEditor == e.get()) { + oxLogError(closeFile(e->itemPath())); + m_closeActiveTab = true; + break; + } + } + 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..6809102a 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -9,12 +9,14 @@ #include #include +#include #include #include #include #include "aboutpopup.hpp" #include "deleteconfirmation.hpp" +#include "makecopypopup.hpp" #include "newdir.hpp" #include "newmenu.hpp" #include "newproject.hpp" @@ -40,11 +42,14 @@ 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)}; 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; @@ -114,6 +119,12 @@ 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; + 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 0f71da88..542879e9 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 @@ -291,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/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/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index 12549785..dcb0f3fa 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, @@ -206,6 +225,60 @@ 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"); + switch (r) { + case PopupResponse::None: + break; + case PopupResponse::OK: + response.emit(r); + close(); + break; + case PopupResponse::Cancel: + response.emit(r); + close(); + break; + } + ImGui::EndPopup(); + } + break; + } +} + + bool s_mainWinHasFocus{}; bool mainWinHasFocus() noexcept { return s_mainWinHasFocus; 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)); 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 {}; }