Compare commits

...

25 Commits

Author SHA1 Message Date
df2c7e2b67 [nostalgia] Update release notes
All checks were successful
Build / build (push) Successful in 1m26s
2025-02-08 18:10:49 -06:00
713aec887b [buildcore] Change mypy invokation
All checks were successful
Build / build (push) Successful in 1m28s
2025-02-07 20:38:44 -06:00
3089cd7afc Change builder type to olympic
Some checks failed
Build / build (push) Has been cancelled
2025-02-07 20:34:22 -06:00
00638bc812 [nostalgia/gfx/studio/tilesheet] Mark DrawCommands as obsolete if no changes
All checks were successful
Build / build (push) Successful in 3m37s
2025-02-05 20:26:47 -06:00
e002109829 [studio] Make undo/redo skip over obsolete commands 2025-02-05 20:26:03 -06:00
b4798fd2ab [nostalgia/gfx/studio/tilesheet] Make rotate only available for square subsheets or selections
All checks were successful
Build / build (push) Successful in 3m39s
2025-02-05 01:54:41 -06:00
3c804bf62a [studio] Give MakeCopy popup an error message for files that already exist
All checks were successful
Build / build (push) Successful in 3m36s
2025-02-03 23:30:07 -06:00
d39d552bd9 [nostalgia/studio] Update icon to higher resolution 2025-02-03 23:29:26 -06:00
b7202a2b0d [nostalgia/player] Disable Keel mods on GBA
All checks were successful
Build / build (push) Successful in 3m34s
2025-02-03 22:48:07 -06:00
4e27a4c1f5 [nostalgia/core/studio/tilesheet] Fix palette path display update
All checks were successful
Build / build (push) Successful in 3m37s
2025-02-03 22:43:20 -06:00
4ef31762d0 [nostalgia/core/studio/tilesheet] Cleanup 2025-02-03 22:43:02 -06:00
8b22a8f339 [keel] Make buildUuidMap only read the first 40 bytes of each file 2025-02-03 20:29:06 -06:00
d45ff05bcd [ox/fs] Add new partial file read functions 2025-02-03 20:28:25 -06:00
671dd86206 [keel,studio] Add Make Copy option to ProjectExplorer
All checks were successful
Build / build (push) Successful in 3m46s
2025-02-03 02:01:40 -06:00
0abadc1850 [studio] Fix QuestionPopup to only emit a response when there is a response
All checks were successful
Build / build (push) Successful in 3m36s
2025-02-03 00:35:37 -06:00
4e068d628c [studio] Fix misrender flash on tab close 2025-02-03 00:19:14 -06:00
4461f99fa4 [studio] Add Ctrl-W shortcut for closing active tab
All checks were successful
Build / build (push) Successful in 3m36s
2025-02-02 23:13:15 -06:00
cd1f4bdaa3 [studio] Add confirmation for closing file with unsaved changes 2025-02-02 23:07:59 -06:00
4728699585 [studio] Add combobox that will take string views
All checks were successful
Build / build (push) Successful in 3m47s
2025-02-02 20:46:08 -06:00
105a1e5559 [nostalgia/core/studio/tilesheet] Rework operation ctrls into a dropbox
Some checks failed
Build / build (push) Failing after 1m1s
2025-02-02 20:43:01 -06:00
1bc18e34a8 [nostalgia/core/studio/tilesheet] Add ability to rotate a selection 2025-02-02 20:22:20 -06:00
fb8d295fcb [nostalgia/core/studio/tilesheet] Add rotate functionality 2025-02-02 14:46:21 -06:00
804d78e116 [nostalgia/gfx/studio] Cleanup
All checks were successful
Build / build (push) Successful in 3m32s
2025-02-01 15:14:24 -06:00
5351e9aa0a [nostalgia/core/studio/tilesheet] Add line drawing tool
All checks were successful
Build / build (push) Successful in 3m36s
2025-02-01 14:14:09 -06:00
b5954f15c5 [studio] Restore context menu for root dir, but exclude Delete
All checks were successful
Build / build (push) Successful in 3m33s
2025-01-29 18:47:48 -06:00
38 changed files with 887 additions and 72 deletions

View File

@ -4,7 +4,7 @@ on: [push]
jobs:
build:
runs-on: nostalgia
runs-on: olympic
steps:
- name: Check out repository code
uses: actions/checkout@v3

View File

@ -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

View File

@ -37,6 +37,30 @@ Error FileSystem::read(const FileAddress &addr, void *buffer, std::size_t size)
}
}
Result<Buffer> FileSystem::read(FileAddress const &addr, size_t const size) noexcept {
Result<Buffer> 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<Buffer> FileSystem::read(StringViewCR path, size_t const size) noexcept {
Result<Buffer> out;
out.value.resize(size);
OX_RETURN_ERROR(readFilePath(path, out.value.data(), size));
return out;
}
Result<Buffer> FileSystem::read(const FileAddress &addr) noexcept {
OX_REQUIRE(s, stat(addr));
Buffer buff(static_cast<std::size_t>(s.size));
@ -51,18 +75,33 @@ Result<Buffer> 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<size_t> FileSystem::read(
StringViewCR path,
std::size_t const readStart,
std::size_t const readSize,
Span<char> 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:

View File

@ -41,6 +41,10 @@ class FileSystem {
Error read(const FileAddress &addr, void *buffer, std::size_t size) noexcept;
Result<Buffer> read(FileAddress const &addr, size_t size) noexcept;
Result<Buffer> read(StringViewCR path, size_t size) noexcept;
Result<Buffer> read(const FileAddress &addr) noexcept;
Result<Buffer> 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<size_t> read(
StringViewCR path, size_t readStart, size_t readSize, ox::Span<char> buff) noexcept;
virtual Result<Vector<String>> 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<const char*> directAccessInode(uint64_t) const noexcept override;
@ -358,6 +385,13 @@ Error FileSystemTemplate<FileStore, Directory>::readFileInodeRange(uint64_t inod
return m_fs.read(inode, readStart, readSize, reinterpret_cast<uint8_t*>(buffer), size);
}
template<typename FileStore, typename Directory>
Error FileSystemTemplate<FileStore, Directory>::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<typename FileStore, typename Directory>
Error FileSystemTemplate<FileStore, Directory>::removePath(StringViewCR path, bool recursive) noexcept {
OX_REQUIRE(fd, fileSystemData());

View File

@ -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<size_t>(file.tellg());
readSize = ox::min(readSize, size);
file.seekg(static_cast<off_t>(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<char*>(buffer), static_cast<std::streamsize>(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");

View File

@ -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;

View File

@ -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

View File

@ -9,5 +9,6 @@ target_sources(
inserttilescommand.cpp
palettechangecommand.cpp
rmsubsheetcommand.cpp
rotatecommand.cpp
updatesubsheetcommand.cpp
)

View File

@ -17,6 +17,7 @@ enum class CommandId {
DeleteTile,
FlipX,
FlipY,
Rotate,
InsertTile,
MoveSubSheet,
UpdateSubSheet,

View File

@ -2,15 +2,68 @@
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <cmath>
#include "drawcommand.hpp"
namespace nostalgia::gfx {
constexpr void iterateLine(ox::Point const &a, ox::Point const &b, auto const &f) noexcept {
auto const rise = b.y - a.y;
auto const run = b.x - a.x;
auto const slope = static_cast<double>(rise) / static_cast<double>(run);
auto y = static_cast<double>(a.y);
auto const w = abs(run);
if (rise > 0) {
if (run > 0) {
auto constexpr xmod = 1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(ceil(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else if (run < 0) {
auto constexpr xmod = -1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(floor(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else {
f(a.x, a.y, rise);
}
} else if (rise < 0) {
if (run > 0) {
auto constexpr xmod = 1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(floor(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else if (run < 0) {
auto constexpr xmod = -1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(ceil(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else {
f(a.x, a.y, rise - 1);
}
} else {
auto const xmod = run > 0 ? 1 : -1;
for (int32_t i = 0; i < w; ++i) {
f(a.x + i * xmod, static_cast<int32_t>(y), 1);
}
}
}
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) {
@ -21,8 +74,8 @@ DrawCommand::DrawCommand(
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
int palIdx) noexcept:
ox::SpanView<std::size_t> const&idxList,
int const palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
@ -32,11 +85,12 @@ DrawCommand::DrawCommand(
}
}
bool DrawCommand::append(std::size_t idx) noexcept {
bool DrawCommand::append(std::size_t const idx) noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
if (m_changes.back().value->idx != idx && getPixel(subsheet, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](auto const&c) {
auto existing = find_if(
m_changes.cbegin(), m_changes.cend(), [idx](auto const&c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
@ -48,7 +102,7 @@ bool DrawCommand::append(std::size_t idx) noexcept {
return false;
}
bool DrawCommand::append(ox::Vector<std::size_t> const&idxList) noexcept {
bool DrawCommand::append(ox::SpanView<std::size_t> const&idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
@ -56,6 +110,28 @@ bool DrawCommand::append(ox::Vector<std::size_t> const&idxList) noexcept {
return out;
}
void DrawCommand::lineUpdate(ox::Point a, ox::Point b) noexcept {
std::ignore = undo();
m_changes.clear();
auto &ss = getSubSheet(m_img, m_subSheetIdx);
if (a.x < b.x) { ++b.x; }
if (a.y < b.y) { ++b.y; }
if (a.x > b.x) { --b.x; }
iterateLine(a, b, [this, &ss](int32_t const x, int32_t const y, int32_t const h) {
int const mod = h < 0 ? -1 : 1;
auto const range = h * mod;
for (int32_t i{}; i < range; ++i) {
auto const idx = ptToIdx(x, y + i * mod, ss.columns * TileWidth);
if (idx < ss.pixels.size()) {
if (m_palIdx != getPixel(ss, idx)) {
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(ss, idx));
}
}
}
});
std::ignore = redo();
}
ox::Error DrawCommand::redo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (auto const&c : m_changes) {
@ -80,4 +156,8 @@ TileSheet::SubSheetIdx const&DrawCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
void DrawCommand::finish() noexcept {
setObsolete(m_changes.empty());
}
}

View File

@ -33,12 +33,14 @@ class DrawCommand: public TileSheetCommand {
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
ox::SpanView<std::size_t> const&idxList,
int palIdx) noexcept;
bool append(std::size_t idx) noexcept;
bool append(ox::Vector<std::size_t> const&idxList) noexcept;
bool append(ox::SpanView<std::size_t> const&idxList) noexcept;
void lineUpdate(ox::Point a, ox::Point b) noexcept;
ox::Error redo() noexcept final;
@ -50,6 +52,8 @@ class DrawCommand: public TileSheetCommand {
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
void finish() noexcept;
};
}
}

View File

@ -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) {

View File

@ -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<int>(CommandId::Rotate);
}
TileSheet::SubSheetIdx const&RotateCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -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;
};
}

View File

@ -152,6 +152,12 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key const key, bool const do
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_E) {
m_tool = TileSheetTool::Line;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_S) {
m_tool = TileSheetTool::Select;
setCopyEnabled(true);
@ -214,7 +220,7 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
ImGui::BeginChild("Controls", {s_palViewWidth - 8, paneSize.y}, true);
{
auto const controlsSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ToolBox", {168, 32}, true);
ImGui::BeginChild("ToolBox", {0, 32}, true);
{
auto const btnSz = ImVec2{45, 14};
if (ImGui::Selectable("Select", m_tool == TileSheetTool::Select, 0, btnSz)) {
@ -230,18 +236,34 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
m_tool = TileSheetTool::Fill;
m_model.clearSelection();
}
ImGui::SameLine();
if (ImGui::Selectable("Line", m_tool == TileSheetTool::Line, 0, btnSz)) {
m_tool = TileSheetTool::Line;
m_model.clearSelection();
}
//ImGui::SameLine();
//size_t i{};
//ig::ComboBox("##Operations", ox::Array<ox::CStringView, 1>{"Operations"}, i);
}
ImGui::EndChild();
ImGui::SameLine();
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();
@ -448,6 +470,9 @@ void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
case TileSheetTool::Fill:
m_view.clickFill(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Line:
m_view.clickLine(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Select:
m_view.clickSelect(fbSize, clickPos(winPos, mousePos));
break;

View File

@ -24,6 +24,7 @@
#include "tilesheeteditormodel.hpp"
#include "commands/movesubsheetcommand.hpp"
#include "commands/rotatecommand.hpp"
namespace nostalgia::gfx {
@ -140,11 +141,15 @@ size_t TileSheetEditorModel::palettePage() const noexcept {
return m_palettePage;
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const palIdx) noexcept {
void TileSheetEditorModel::drawCommand(ox::Point const &pt, std::size_t const palIdx) noexcept {
if (m_lastDrawUpdatePt == pt) {
return;
}
auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
m_lastDrawUpdatePt = pt;
auto const idx = gfx::idx(activeSubSheet, pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
@ -154,8 +159,31 @@ void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const pal
}
}
void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t const palIdx) noexcept {
if (m_lastDrawUpdatePt == pt) {
return;
}
auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
m_lastDrawUpdatePt = pt;
auto const idx = gfx::idx(activeSubSheet, pt);
if (m_ongoingDrawCommand) {
m_ongoingDrawCommand->lineUpdate(m_lineStartPt, pt);
m_updated = true;
} else {
std::ignore = pushCommand(ox::make<DrawCommand>(
m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
m_lineStartPt = pt;
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
if (m_ongoingDrawCommand) {
m_ongoingDrawCommand->finish();
m_ongoingDrawCommand = nullptr;
}
}
void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept {
@ -213,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<RotateCommand>(
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<RotateCommand>(
m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Right));
}
void TileSheetEditorModel::setSelection(studio::Selection const&sel) noexcept {
m_selection.emplace(sel);
m_updated = true;
@ -253,6 +303,7 @@ ox::Error TileSheetEditorModel::markUpdatedCmdId(studio::UndoCommand const*cmd)
m_pal = keel::AssetRef<Palette>{};
}
m_palettePage = ox::min<size_t>(pal().pages.size(), 0);
setPalPath();
paletteChanged.emit();
}
auto const tsCmd = dynamic_cast<TileSheetCommand const*>(cmd);
@ -304,6 +355,16 @@ ox::Error TileSheetEditorModel::flipY() noexcept {
return pushCommand(ox::make<FlipYCommand>(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<MoveSubSheetCommand>(m_img, std::move(src), std::move(dst)));
}

View File

@ -34,6 +34,8 @@ class TileSheetEditorModel: public ox::SignalHandler {
class DrawCommand *m_ongoingDrawCommand = nullptr;
studio::SelectionTracker m_selTracker;
ox::Optional<studio::Selection> m_selection;
ox::Point m_lineStartPt;
ox::Point m_lastDrawUpdatePt;
bool m_updated = false;
public:
@ -71,6 +73,8 @@ class TileSheetEditorModel: public ox::SignalHandler {
void drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept;
void drawLineCommand(ox::Point const&pt, std::size_t palIdx) noexcept;
void endDrawCommand() noexcept;
void addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept;
@ -102,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;
@ -130,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:

View File

@ -11,7 +11,8 @@
namespace nostalgia::gfx {
TileSheetEditorView::TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack):
TileSheetEditorView::TileSheetEditorView(
studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack):
m_model(sctx, path, undoStack),
m_pixelsDrawer(m_model) {
glBindVertexArray(0);
@ -76,6 +77,11 @@ void TileSheetEditorView::clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clic
m_model.drawCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickLine(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.drawLineCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.select(pt);
@ -90,6 +96,7 @@ void TileSheetEditorView::releaseMouseButton(TileSheetTool tool) noexcept {
switch (tool) {
case TileSheetTool::Draw:
case TileSheetTool::Fill:
case TileSheetTool::Line:
m_model.endDrawCommand();
break;
case TileSheetTool::Select:

View File

@ -23,6 +23,7 @@ enum class TileSheetTool {
Select,
Draw,
Fill,
Line,
};
[[nodiscard]]
@ -34,6 +35,8 @@ constexpr auto toString(TileSheetTool t) noexcept {
return "Draw";
case TileSheetTool::Fill:
return "Fill";
case TileSheetTool::Line:
return "Line";
case TileSheetTool::None:
return "None";
}
@ -53,7 +56,7 @@ class TileSheetEditorView: public ox::SignalHandler {
std::size_t m_palIdx = 0;
public:
TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack);
TileSheetEditorView(studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack);
~TileSheetEditorView() override = default;
@ -65,6 +68,8 @@ class TileSheetEditorView: public ox::SignalHandler {
void clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickLine(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;

View File

@ -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=$<BOOL:${LOAD_KEEL_MODS}>
OLYMPIC_GUI_APP=1
)
target_link_libraries(
Nostalgia
NostalgiaKeelModules

View File

@ -33,7 +33,7 @@ endif()
install(
FILES
ns.icns
ns_logo.icns
DESTINATION
${NOSTALGIA_DIST_RESOURCES}/icons
)

View File

@ -9,7 +9,7 @@
<string>Nostalgia Studio</string>
<key>CFBundleIconFile</key>
<string>icons/ns.icns</string>
<string>icons/ns_logo.icns</string>
<key>CFBundleIdentifier</key>
<string>net.drinkingtea.nostalgia.studio</string>
@ -18,7 +18,7 @@
<string>APPL</string>
<key>CFBundleVersion</key>
<string>0.0.0</string>
<string>dev build</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
@ -30,6 +30,6 @@
<string>True</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) 2016-2023 Gary Talent &lt;gary@drinkingtea.net&gt;</string>
<string>Copyright (c) 2016-2025 Gary Talent &lt;gary@drinkingtea.net&gt;</string>
</dict>
</plist>

Binary file not shown.

View File

@ -15,6 +15,8 @@ constexpr auto K1HdrSz = 40;
ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept;
ox::Result<ox::UUID> 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));

View File

@ -8,15 +8,25 @@ namespace keel {
ox::Result<ox::UUID> 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<ox::UUID> 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<ox::ModelObject> readAsset(ox::TypeStore &ts, ox::BufferView buff) noexcept {
std::size_t offset = 0;
if (!readUuidHeader(buff).error) {

View File

@ -53,10 +53,12 @@ static ox::Error buildUuidMap(Context &ctx, ox::StringViewCR path) noexcept {
OX_REQUIRE_M(filePath, ox::join("/", ox::Array<ox::StringView, 2>{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<char, K1HdrSz> 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, ".")) {

View File

@ -5,6 +5,7 @@ add_library(
deleteconfirmation.cpp
filedialogmanager.cpp
main.cpp
makecopypopup.cpp
newdir.cpp
newmenu.cpp
newproject.cpp

View File

@ -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);
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/context.hpp>
#include <studio/imguiutil.hpp>
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<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> 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;
};
}

View File

@ -42,19 +42,22 @@ 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();
}
}
void ProjectExplorer::dirContextMenu(ox::StringViewCR path) const noexcept {
if (path.len() && ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Add Item")) {
addItem.emit(path);
}
if (ImGui::MenuItem("Add Directory")) {
addDir.emit(path);
}
if (ImGui::MenuItem("Delete")) {
if (path.len() && ImGui::MenuItem("Delete")) {
deleteItem.emit(path);
}
ImGui::EndPopup();

View File

@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer {
ox::Signal<ox::Error(ox::StringViewCR)> addDir;
ox::Signal<ox::Error(ox::StringViewCR)> deleteItem;
ox::Signal<ox::Error(ox::StringViewCR)> renameItem;
ox::Signal<ox::Error(ox::StringViewCR)> makeCopy;
ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveDir;
ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveItem;

View File

@ -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<StudioConfig>(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<BaseEditor> 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 {};

View File

@ -9,12 +9,14 @@
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
#include <studio/imguiutil.hpp>
#include <studio/module.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#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<ox::Pair<ox::String>> m_queuedMoves;
ox::Vector<ox::Pair<ox::String>> 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;

View File

@ -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<ox::CStringView> 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<ox::Error(ig::PopupResponse)> 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;

View File

@ -92,6 +92,8 @@ class Project: public ox::SignalHandler {
ox::Result<ox::FileStat> 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;

View File

@ -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<ox::CStringView> 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<const ox::String> 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<ImVec2>(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;

View File

@ -97,6 +97,14 @@ ox::Result<ox::FileStat> 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));

View File

@ -7,9 +7,7 @@
namespace studio {
ox::Error UndoStack::push(ox::UPtr<UndoCommand> &&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<UndoCommand> &&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 {};
}