From 7371df429534f264c179684412f6197f7968ebfa Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sat, 21 Jun 2025 08:48:13 -0500 Subject: [PATCH] Squashed 'deps/nostalgia/' changes from e78c4050..857587c1 857587c1 [studio] Cleanup eb3d53c9 [studio] Cleanup 14d58f3f [studio] Fix Navigation shortcuts for non-Mac systems 5f239790 [studio,nostalgia/gfx/studio/tilesheet] Fix copy/cut/paste enablement when there is no selection 58e0ecb4 [studio] Make FilePickerPopup accept on double click of a file 8838bf42 [studio] Fix to properly copy file that has the same name as deleted file bddc544d [nostalgia] Update release notes a9437191 [studio,turbine] Add support for mouse back/forward buttons 9d8da7cc [ox/std] Make strToInt return error for empty string 394b568e [studio] Add Back/Forward navigation 78e9f70d [nostalgia] Update release notes 12e5623f [ox/logconn] Add exception handling for logger thread cfdfb0a8 [studio] Fix file deletion to close file even if not active 56e66530 [studio] Cleanup 7415ce4b [nostalgia/gfx/studio] Cleanup 05f42150 [olympic] Add new loc command to Makefile 8ea2bc69 [nostalgia] Update release notes c7809241 [studio] Add [DEBUG] tag to About in debug builds 8c538560 [nostalgia/gfx/studio/palette] Make RGB key shortcuts work when color channel inputs are focused c3e75bdb [nostalgia/gfx/studio/tilesheet] Cleanup git-subtree-dir: deps/nostalgia git-subtree-split: 857587c18b4695eacd31457e3c30b4971b4e46e8 --- Makefile | 22 +++ deps/ox/src/ox/logconn/logconn.cpp | 31 ++-- deps/ox/src/ox/std/stringview.hpp | 5 +- release-notes.md | 8 + .../commands/removepagecommand.cpp | 2 +- .../commands/updatecolorcommand.cpp | 14 +- .../paletteeditor/paletteeditor-imgui.cpp | 33 +++-- .../commands/deletetilescommand.cpp | 27 ++-- .../commands/deletetilescommand.hpp | 8 +- .../tilesheeteditor/commands/drawcommand.cpp | 8 +- .../commands/inserttilescommand.cpp | 28 ++-- .../commands/inserttilescommand.hpp | 8 +- .../commands/updatesubsheetcommand.cpp | 1 - .../commands/updatesubsheetcommand.hpp | 2 +- .../tilesheeteditor/tilesheeteditor-imgui.cpp | 2 + .../tilesheeteditor/tilesheeteditormodel.cpp | 52 ++++--- .../tilesheeteditor/tilesheeteditormodel.hpp | 5 +- src/olympic/studio/applib/src/app.cpp | 8 +- .../studio/applib/src/popups/about.cpp | 4 + .../studio/applib/src/popups/newmenu.cpp | 4 +- .../studio/applib/src/popups/newmenu.hpp | 2 +- src/olympic/studio/applib/src/studioui.cpp | 137 ++++++++++++------ src/olympic/studio/applib/src/studioui.hpp | 10 +- .../studio/modlib/include/studio/context.hpp | 32 +++- .../modlib/include/studio/filepickerpopup.hpp | 10 +- .../modlib/include/studio/imguiutil.hpp | 26 ++-- .../modlib/include/studio/itemmaker.hpp | 6 +- src/olympic/studio/modlib/src/CMakeLists.txt | 1 + src/olympic/studio/modlib/src/context.cpp | 69 +++++++++ src/olympic/studio/modlib/src/editor.cpp | 18 ++- .../studio/modlib/src/filepickerpopup.cpp | 28 +++- src/olympic/studio/modlib/src/imguiutil.cpp | 26 ++-- src/olympic/studio/modlib/src/project.cpp | 2 +- .../turbine/include/turbine/turbine.hpp | 5 +- src/olympic/turbine/src/gba/turbine.cpp | 4 +- src/olympic/turbine/src/glfw/context.hpp | 1 + src/olympic/turbine/src/glfw/turbine.cpp | 36 +++-- util/scripts/loc.py | 59 ++++++++ 38 files changed, 528 insertions(+), 216 deletions(-) create mode 100644 src/olympic/studio/modlib/src/context.cpp create mode 100755 util/scripts/loc.py diff --git a/Makefile b/Makefile index bcbfdad..6356f07 100644 --- a/Makefile +++ b/Makefile @@ -52,3 +52,25 @@ configure-gba: .PHONY: configure-gba-debug configure-gba-debug: ${BC_CMD_SETUP_BUILD} --toolchain=deps/gbabuildcore/cmake/modules/GBA.cmake --target=gba --current_build=0 --build_type=debug --build_root=${BC_VAR_BUILD_PATH} + +.PHONY: loc +loc: + ${BC_PY3} util/scripts/loc.py \ + --search-dirs \ + src \ + deps/ox/src \ + deps/buildcore \ + deps/gbabuildcore \ + deps/glutils \ + deps/teagba \ + --include-exts \ + .cpp \ + .hpp \ + .py \ + .s \ + .cmake \ + --exclude-paths \ + deps/teagba/src/gba_crt0.s \ + src/olympic/studio/applib/src/font.cpp \ + src/olympic/studio/applib/src/font.hpp \ + src/nostalgia/studio/icondata.cpp diff --git a/deps/ox/src/ox/logconn/logconn.cpp b/deps/ox/src/ox/logconn/logconn.cpp index f027dab..d101e61 100644 --- a/deps/ox/src/ox/logconn/logconn.cpp +++ b/deps/ox/src/ox/logconn/logconn.cpp @@ -91,23 +91,28 @@ ox::Error LoggerConn::sendInit(const InitTraceMsg &msg) noexcept { } void LoggerConn::msgSend() noexcept { - while (true) { - std::unique_lock lk(m_waitMut); - m_waitCond.wait(lk); - if (!m_running) { - break; - } - std::lock_guard const buffLk(m_buffMut); + try { while (true) { - Array tmp; - const auto read = m_buff.read(tmp.data(), tmp.size()); - if (!read) { + std::unique_lock lk(m_waitMut); + m_waitCond.wait(lk); + if (!m_running) { break; } - oxAssert(read <= tmp.size(), "logger trying to read too much data"); - //std::printf("LoggerConn: sending %lu bytes\n", read); - std::ignore = send(tmp.data(), read); + std::lock_guard const buffLk(m_buffMut); + while (true) { + Array tmp; + const auto read = m_buff.read(tmp.data(), tmp.size()); + if (!read) { + break; + } + oxAssert(read <= tmp.size(), "logger trying to read too much data"); + //std::printf("LoggerConn: sending %lu bytes\n", read); + std::ignore = send(tmp.data(), read); + } } + } catch (std::exception const &e) { + oxErrf("Exception in logger thread: {}\n", e.what()); + oxAssert(false, "logger thread exception"); } } diff --git a/deps/ox/src/ox/std/stringview.hpp b/deps/ox/src/ox/std/stringview.hpp index 6a2221e..6fc955c 100644 --- a/deps/ox/src/ox/std/stringview.hpp +++ b/deps/ox/src/ox/std/stringview.hpp @@ -104,13 +104,16 @@ constexpr ox::Result strToInt(StringViewCR str) noexcept { OX_ALLOW_UNSAFE_BUFFERS_BEGIN int total = 0; int multiplier = 1; + if (str.len() == 0) [[unlikely]] { + return Error{1, "Empty string passed to strToInt"}; + } for (auto i = static_cast(str.len()) - 1; i != -1; --i) { auto s = static_cast(i); if (str[s] >= '0' && str[s] <= '9') { total += (str[s] - '0') * multiplier; multiplier *= 10; } else { - return ox::Error(1); + return ox::Error{1}; } } return total; diff --git a/release-notes.md b/release-notes.md index 377c1c1..f3888fb 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,8 +1,16 @@ # d2025.06.0 * Add ability to remember recent projects in config +* Add navigation support (back and forward) +* Fix file deletion to close file even if not active +* Fix file copy to work when creating a copy with the name of a previously + deleted file +* Fix copy/cut/paste enablement when there is no selection +* Make file picker popup accept on double click of a file * PaletteEditor: Add RGB key shortcuts for focusing color channels * PaletteEditor: Add color preview to color editor +* PaletteEditor: Make RGB key shortcuts work when color channel inputs are + focused # d2025.05.2 diff --git a/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/removepagecommand.cpp b/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/removepagecommand.cpp index 1376b0f..2aa47af 100644 --- a/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/removepagecommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/removepagecommand.cpp @@ -8,7 +8,7 @@ namespace nostalgia::gfx { -RemovePageCommand::RemovePageCommand(Palette &pal, size_t idx) noexcept: +RemovePageCommand::RemovePageCommand(Palette &pal, size_t const idx) noexcept: m_pal(pal), m_idx(idx) {} diff --git a/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/updatecolorcommand.cpp b/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/updatecolorcommand.cpp index b1c9eb6..0947119 100644 --- a/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/updatecolorcommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/paletteeditor/commands/updatecolorcommand.cpp @@ -9,13 +9,13 @@ namespace nostalgia::gfx { UpdateColorCommand::UpdateColorCommand( Palette &pal, - size_t page, - size_t idx, - Color16 newColor): - m_pal(pal), - m_page(page), - m_idx(idx), - m_altColor(newColor) { + size_t const page, + size_t const idx, + Color16 const newColor): + m_pal{pal}, + m_page{page}, + m_idx{idx}, + m_altColor{newColor} { if (color(m_pal, m_page, m_idx) == newColor) { throw studio::NoChangesException(); } diff --git a/src/nostalgia/modules/gfx/src/studio/paletteeditor/paletteeditor-imgui.cpp b/src/nostalgia/modules/gfx/src/studio/paletteeditor/paletteeditor-imgui.cpp index af8d051..8a6d38e 100644 --- a/src/nostalgia/modules/gfx/src/studio/paletteeditor/paletteeditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/paletteeditor/paletteeditor-imgui.cpp @@ -302,11 +302,12 @@ void PaletteEditorImGui::drawColorEditor() noexcept { int const a = alpha16(c); auto const newName = ig::InputText<50>( "Name", m_pal.colorNames[m_selectedColorRow]); - bool inputFocused = ImGui::IsItemFocused(); + auto const nameInputFocused = ImGui::IsItemFocused(); ImGui::Separator(); - colorInput("Red", r, inputFocused, FocusCmd::Red); - colorInput("Green", g, inputFocused, FocusCmd::Green); - colorInput("Blue", b, inputFocused, FocusCmd::Blue); + bool colorInFocused{}; + colorInput("Red", r, colorInFocused, FocusCmd::Red); + colorInput("Green", g, colorInFocused, FocusCmd::Green); + colorInput("Blue", b, colorInFocused, FocusCmd::Blue); // color preview { ImGui::PushStyleColor( @@ -320,20 +321,24 @@ void PaletteEditorImGui::drawColorEditor() noexcept { std::ignore = pushCommand( m_pal, m_page, m_selectedColorRow); } - if (ig::mainWinHasFocus() && !inputFocused && !ImGui::IsKeyDown(ImGuiKey_ModCtrl)) { + if (ig::mainWinHasFocus() && !ImGui::IsKeyDown(ImGuiKey_ModCtrl)) { if (!ImGui::IsKeyDown(ImGuiKey_ModAlt)) { - numShortcuts(m_selectedColorRow, largestPage(m_pal)); + if (!nameInputFocused && !colorInFocused) { + numShortcuts(m_selectedColorRow, largestPage(m_pal)); + } } else { numShortcuts(m_page, m_pal.pages.size()); } - if (ImGui::IsKeyDown(ImGuiKey_R)) { - m_focusCmd = FocusCmd::Red; - } - if (ImGui::IsKeyDown(ImGuiKey_G)) { - m_focusCmd = FocusCmd::Green; - } - if (ImGui::IsKeyDown(ImGuiKey_B)) { - m_focusCmd = FocusCmd::Blue; + if (!nameInputFocused) { + if (ImGui::IsKeyDown(ImGuiKey_R)) { + m_focusCmd = FocusCmd::Red; + } + if (ImGui::IsKeyDown(ImGuiKey_G)) { + m_focusCmd = FocusCmd::Green; + } + if (ImGui::IsKeyDown(ImGuiKey_B)) { + m_focusCmd = FocusCmd::Blue; + } } } auto const newColor = color16(r, g, b, a); diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.cpp index 9940b18..9350df0 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.cpp @@ -13,20 +13,19 @@ DeleteTilesCommand::DeleteTilesCommand( TileSheet::SubSheetIdx idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept: - m_img(img), - m_idx(std::move(idx)) { - constexpr unsigned bytesPerTile = PixelsPerTile; - m_deletePos = tileIdx * bytesPerTile; - m_deleteSz = tileCnt * bytesPerTile; - m_deletedPixels.resize(m_deleteSz); - // copy pixels to be erased - { - auto &s = getSubSheet(m_img, m_idx); - auto dst = m_deletedPixels.begin(); - auto src = s.pixels.begin() + m_deletePos; - ox::copy_n(src, m_deleteSz, dst); - } -} + m_img{img}, + m_idx{std::move(idx)}, + m_deletePos{tileIdx * PixelsPerTile}, + m_deleteSz{tileCnt * PixelsPerTile}, + m_deletedPixels{[this] { + ox::Vector deletedPixels(m_deleteSz); + // copy pixels to be erased + auto const &s = getSubSheet(m_img, m_idx); + auto const dst = deletedPixels.begin(); + auto const src = s.pixels.begin() + m_deletePos; + ox::copy_n(src, m_deleteSz, dst); + return deletedPixels; + }()} {} ox::Error DeleteTilesCommand::redo() noexcept { auto &s = getSubSheet(m_img, m_idx); diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.hpp index 846c228..a0e0b34 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/deletetilescommand.hpp @@ -11,10 +11,10 @@ namespace nostalgia::gfx { class DeleteTilesCommand: public TileSheetCommand { private: TileSheet &m_img; - TileSheet::SubSheetIdx m_idx; - std::size_t m_deletePos = 0; - std::size_t m_deleteSz = 0; - ox::Vector m_deletedPixels = {}; + TileSheet::SubSheetIdx const m_idx; + std::size_t const m_deletePos = 0; + std::size_t const m_deleteSz = 0; + ox::Vector const m_deletedPixels; public: DeleteTilesCommand( 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 0b8f59a..ea5ced0 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/drawcommand.cpp @@ -62,11 +62,11 @@ constexpr void iterateLine(ox::Point const &a, ox::Point const &b, auto const &f DrawCommand::DrawCommand( TileSheet &img, TileSheet::SubSheetIdx subSheetIdx, - std::size_t idx, + std::size_t const idx, int const palIdx) noexcept: - m_img(img), - m_subSheetIdx(std::move(subSheetIdx)), - m_palIdx(palIdx) { + m_img{img}, + m_subSheetIdx{std::move(subSheetIdx)}, + m_palIdx{palIdx} { auto &subsheet = getSubSheet(m_img, m_subSheetIdx); m_changes.emplace_back(static_cast(idx), getPixel(subsheet, idx)); } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.cpp index 7bc7fd0..15109eb 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.cpp @@ -12,19 +12,19 @@ InsertTilesCommand::InsertTilesCommand( std::size_t const tileIdx, std::size_t const tileCnt) noexcept: m_img{img}, - m_idx{std::move(idx)} { - m_insertPos = tileIdx * PixelsPerTile; - m_insertCnt = tileCnt * PixelsPerTile; - m_deletedPixels.resize(m_insertCnt); - // copy pixels to be erased - { - auto &s = getSubSheet(m_img, m_idx); - auto &p = s.pixels; - auto const dst = m_deletedPixels.begin(); - auto const src = p.begin() + p.size() - m_insertCnt; - ox::copy_n(src, m_insertCnt, dst); - } -} + m_idx{std::move(idx)}, + m_insertPos{tileIdx * PixelsPerTile}, + m_insertCnt{tileCnt * PixelsPerTile}, + m_insertedPixels{[this] { + ox::Vector insertedPixels(m_insertCnt); + // copy pixels to be erased + auto &s = getSubSheet(m_img, m_idx); + auto &p = s.pixels; + auto const dst = insertedPixels.begin(); + auto const src = p.begin() + p.size() - m_insertCnt; + ox::copy_n(src, m_insertCnt, dst); + return insertedPixels; + }()} {} OX_ALLOW_UNSAFE_BUFFERS_BEGIN @@ -52,7 +52,7 @@ ox::Error InsertTilesCommand::undo() noexcept { auto const src = &p[srcIdx]; ox::memmove(dst1, src, sz); } - ox::memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size()); + ox::memcpy(dst2, m_insertedPixels.data(), m_insertedPixels.size()); return {}; } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.hpp index 49f77ab..220311b 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/inserttilescommand.hpp @@ -11,10 +11,10 @@ namespace nostalgia::gfx { class InsertTilesCommand: public TileSheetCommand { private: TileSheet &m_img; - TileSheet::SubSheetIdx m_idx; - std::size_t m_insertPos = 0; - std::size_t m_insertCnt = 0; - ox::Vector m_deletedPixels = {}; + TileSheet::SubSheetIdx const m_idx; + std::size_t const m_insertPos{}; + std::size_t const m_insertCnt{}; + ox::Vector const m_insertedPixels; public: InsertTilesCommand( diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.cpp index 9d084e3..e4d7c8a 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.cpp @@ -15,7 +15,6 @@ UpdateSubSheetCommand::UpdateSubSheetCommand( m_img{img}, m_idx{std::move(idx)}, m_sheet{getSubSheet(m_img, m_idx)} { - m_sheet = getSubSheet(m_img, m_idx); m_sheet.name = std::move(name); OX_THROW_ERROR(resizeSubsheet(m_sheet, {cols, rows})); } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.hpp index b2c5511..4969488 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/commands/updatesubsheetcommand.hpp @@ -11,7 +11,7 @@ namespace nostalgia::gfx { class UpdateSubSheetCommand: public TileSheetCommand { private: TileSheet &m_img; - TileSheet::SubSheetIdx m_idx; + TileSheet::SubSheetIdx const m_idx; TileSheet::SubSheet m_sheet; public: 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 fff75c9..ba53777 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -192,6 +192,8 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key const key, bool const do } void TileSheetEditorImGui::draw(studio::Context&) noexcept { + setCopyEnabled(m_model.hasSelection()); + setCutEnabled(m_model.hasSelection()); if (ig::mainWinHasFocus() && m_tool == TileSheetTool::Select) { if (ImGui::IsKeyDown(ImGuiKey_ModCtrl) && !m_palPathFocused) { if (ImGui::IsKeyPressed(ImGuiKey_A)) { diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp index 89bdf29..5c68a82 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.cpp @@ -84,9 +84,9 @@ void TileSheetEditorModel::cut() { auto const pt1 = m_selection->a; auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight}; turbine::setClipboardObject(m_tctx, std::move(cb)); - if (auto const cmd = ox::makeCatch( + if (auto cmd = ox::make_unique_catch( CommandId::Cut, m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb); cmd.ok()) { - std::ignore = pushCommand(cmd.value); + std::ignore = pushCommand(std::move(cmd.value)); } } @@ -125,9 +125,9 @@ void TileSheetEditorModel::paste() { auto const&s = activeSubSheet(); auto const pt1 = m_selection->a; auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight}; - if (auto const cmd = ox::makeCatch( + if (auto cmd = ox::make_unique_catch( CommandId::Paste, m_img, m_activeSubsSheetIdx, pt1, pt2, *cb); cmd.value) { - std::ignore = pushCommand(cmd.value); + std::ignore = pushCommand(std::move(cmd.value)); } } @@ -142,7 +142,7 @@ ox::String const &TileSheetEditorModel::palPath() const & noexcept { ox::Error TileSheetEditorModel::setPalette(ox::StringViewCR path) noexcept { OX_REQUIRE(uuid, keelCtx(m_tctx).pathToUuid.at(path)); - std::ignore = pushCommand(ox::make( + std::ignore = pushCommand(ox::make_unique( activeSubSheetIdx(), m_img, uuid->toString())); return {}; } @@ -169,7 +169,7 @@ void TileSheetEditorModel::drawCommand(ox::Point const &pt, std::size_t const pa if (m_ongoingDrawCommand) { m_updated = m_updated || m_ongoingDrawCommand->append(idx); } else if (activeSubSheet.pixels[idx] != palIdx) { - std::ignore = pushCommand(ox::make( + std::ignore = pushCommand(ox::make_unique( m_img, m_activeSubsSheetIdx, idx, static_cast(palIdx))); } } @@ -188,7 +188,7 @@ void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t cons m_ongoingDrawCommand->lineUpdate(m_lineStartPt, pt); m_updated = true; } else { - std::ignore = pushCommand(ox::make( + std::ignore = pushCommand(ox::make_unique( m_img, m_activeSubsSheetIdx, idx, static_cast(palIdx))); m_lineStartPt = pt; } @@ -203,27 +203,27 @@ void TileSheetEditorModel::endDrawCommand() noexcept { } void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const &parentIdx) noexcept { - std::ignore = pushCommand(ox::make(m_img, parentIdx)); + std::ignore = pushCommand(ox::make_unique(m_img, parentIdx)); } void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const &idx) noexcept { - std::ignore = pushCommand(ox::make(m_img, idx)); + std::ignore = pushCommand(ox::make_unique(m_img, idx)); } void TileSheetEditorModel::insertTiles( TileSheet::SubSheetIdx const &idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept { - std::ignore = pushCommand(ox::make(m_img, idx, tileIdx, tileCnt)); + std::ignore = pushCommand(ox::make_unique(m_img, idx, tileIdx, tileCnt)); } void TileSheetEditorModel::deleteTiles( TileSheet::SubSheetIdx const &idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept { - std::ignore = pushCommand(ox::make(m_img, idx, tileIdx, tileCnt)); + std::ignore = pushCommand(ox::make_unique(m_img, idx, tileIdx, tileCnt)); } ox::Error TileSheetEditorModel::updateSubsheet( TileSheet::SubSheetIdx const &idx, ox::StringViewCR name, int const cols, int const rows) noexcept { - OX_REQUIRE(cmd, ox::makeCatch(m_img, idx, name, cols, rows)); - std::ignore = pushCommand(cmd); + OX_REQUIRE_M(cmd, ox::make_unique_catch(m_img, idx, name, cols, rows)); + std::ignore = pushCommand(std::move(cmd)); return {}; } @@ -255,29 +255,29 @@ void TileSheetEditorModel::fill(ox::Point const &pt, uint8_t const palIdx) noexc if (m_ongoingDrawCommand) { m_updated = m_updated || m_ongoingDrawCommand->append(idxList); } else if (getPixel(activeSubSheet, pt) != palIdx) { - std::ignore = pushCommand(ox::make(m_img, m_activeSubsSheetIdx, idxList, palIdx)); + std::ignore = pushCommand(ox::make_unique(m_img, m_activeSubsSheetIdx, idxList, palIdx)); } } ox::Error TileSheetEditorModel::rotateLeft() noexcept { - auto &ss = activeSubSheet(); + auto const &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( + return pushCommand(ox::make_unique( m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Left)); } ox::Error TileSheetEditorModel::rotateRight() noexcept { - auto &ss = activeSubSheet(); + auto const &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( + return pushCommand(ox::make_unique( m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Right)); } @@ -315,6 +315,10 @@ void TileSheetEditorModel::clearSelection() noexcept { m_selection.reset(); } +bool TileSheetEditorModel::hasSelection() const noexcept { + return m_selection.has_value(); +} + bool TileSheetEditorModel::updated() const noexcept { return m_updated; } @@ -365,7 +369,7 @@ ox::Error TileSheetEditorModel::flipX() noexcept { a = m_selection->a; b = m_selection->b; } - return pushCommand(ox::make(m_img, m_activeSubsSheetIdx, a, b)); + return pushCommand(ox::make_unique(m_img, m_activeSubsSheetIdx, a, b)); } ox::Error TileSheetEditorModel::flipY() noexcept { @@ -376,7 +380,7 @@ ox::Error TileSheetEditorModel::flipY() noexcept { a = m_selection->a; b = m_selection->b; } - return pushCommand(ox::make(m_img, m_activeSubsSheetIdx, a, b)); + return pushCommand(ox::make_unique(m_img, m_activeSubsSheetIdx, a, b)); } bool TileSheetEditorModel::rotateEligible() const noexcept { @@ -390,7 +394,7 @@ bool TileSheetEditorModel::rotateEligible() const noexcept { } ox::Error TileSheetEditorModel::moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept { - return pushCommand(ox::make(m_img, std::move(src), std::move(dst))); + return pushCommand(ox::make_unique(m_img, std::move(src), std::move(dst))); } void TileSheetEditorModel::getFillPixels( @@ -437,9 +441,9 @@ void TileSheetEditorModel::setPalPath() noexcept { } } -ox::Error TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept { - std::ignore = m_undoStack.push(ox::UPtr{cmd}); - m_ongoingDrawCommand = dynamic_cast(cmd); +ox::Error TileSheetEditorModel::pushCommand(ox::UPtr &&cmd) noexcept { + m_ongoingDrawCommand = dynamic_cast(cmd.get()); + std::ignore = m_undoStack.push(std::move(cmd)); m_updated = true; return {}; } diff --git a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp index fae6401..3a2663b 100644 --- a/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp +++ b/src/nostalgia/modules/gfx/src/studio/tilesheeteditor/tilesheeteditormodel.hpp @@ -118,6 +118,9 @@ class TileSheetEditorModel final: public ox::SignalHandler { void clearSelection() noexcept; + [[nodiscard]] + bool hasSelection() const noexcept; + [[nodiscard]] bool updated() const noexcept; @@ -152,7 +155,7 @@ class TileSheetEditorModel final: public ox::SignalHandler { void setPalPath() noexcept; - ox::Error pushCommand(studio::UndoCommand *cmd) noexcept; + ox::Error pushCommand(ox::UPtr &&cmd) noexcept; ox::Error handleFileRename(ox::StringViewCR, ox::StringViewCR newPath, ox::UUID const &id) noexcept; diff --git a/src/olympic/studio/applib/src/app.cpp b/src/olympic/studio/applib/src/app.cpp index 8507638..6eb9d13 100644 --- a/src/olympic/studio/applib/src/app.cpp +++ b/src/olympic/studio/applib/src/app.cpp @@ -28,10 +28,15 @@ class StudioUIDrawer: public turbine::gl::Drawer { }; static void keyEventHandler(turbine::Context &ctx, turbine::Key key, bool down) noexcept { - auto sctx = turbine::applicationData(ctx); + auto const sctx = turbine::applicationData(ctx); sctx->ui.handleKeyEvent(key, down); } +static void mouseButtonEventHandler(turbine::Context &ctx, int const btn, bool const down) noexcept { + auto const sctx = turbine::applicationData(ctx); + sctx->ui.handleMouseButtonEvent(btn, down); +} + [[nodiscard]] ox::Vector> WindowIcons() noexcept; @@ -43,6 +48,7 @@ static ox::Error runApp( oxLogError(turbine::setWindowIcon(*ctx, WindowIcons())); turbine::setWindowTitle(*ctx, keelCtx(*ctx).appName); turbine::setKeyEventHandler(*ctx, keyEventHandler); + turbine::setMouseButtonEventHandler(*ctx, mouseButtonEventHandler); turbine::requireRefreshWithin(*ctx, 0); StudioUI ui(*ctx, projectDataDir); StudioUIDrawer drawer(ui); diff --git a/src/olympic/studio/applib/src/popups/about.cpp b/src/olympic/studio/applib/src/popups/about.cpp index 4b7dc1f..a1eb89a 100644 --- a/src/olympic/studio/applib/src/popups/about.cpp +++ b/src/olympic/studio/applib/src/popups/about.cpp @@ -15,7 +15,11 @@ namespace studio { AboutPopup::AboutPopup(turbine::Context &ctx) noexcept: Popup("About"), +#ifdef DEBUG + m_text{sfmt("{} [DEBUG] - {}", keelCtx(ctx).appName, olympic::appVersion)} { +#else m_text{sfmt("{} - {}", keelCtx(ctx).appName, olympic::appVersion)} { +#endif } void AboutPopup::draw(Context &sctx) noexcept { diff --git a/src/olympic/studio/applib/src/popups/newmenu.cpp b/src/olympic/studio/applib/src/popups/newmenu.cpp index bc95411..e0b42a2 100644 --- a/src/olympic/studio/applib/src/popups/newmenu.cpp +++ b/src/olympic/studio/applib/src/popups/newmenu.cpp @@ -80,9 +80,9 @@ void NewMenu::addItemMaker(ox::UPtr &&im) noexcept { }); } -void NewMenu::installItemTemplate(ox::UPtr &tmplt) noexcept { +void NewMenu::installItemTemplate(ox::UPtr &&tmplt) noexcept { for (auto const&im : m_types) { - if (im->installTemplate(tmplt)) { + if (im->installTemplate(std::move(tmplt))) { break; } } diff --git a/src/olympic/studio/applib/src/popups/newmenu.hpp b/src/olympic/studio/applib/src/popups/newmenu.hpp index 35c08d9..1299151 100644 --- a/src/olympic/studio/applib/src/popups/newmenu.hpp +++ b/src/olympic/studio/applib/src/popups/newmenu.hpp @@ -72,7 +72,7 @@ class NewMenu final: public Popup { void addItemMaker(ox::UPtr &&im) noexcept; - void installItemTemplate(ox::UPtr &tmplt) noexcept; + void installItemTemplate(ox::UPtr &&tmplt) noexcept; private: void drawNewItemType(Context const&sctx) noexcept; diff --git a/src/olympic/studio/applib/src/studioui.cpp b/src/olympic/studio/applib/src/studioui.cpp index c807c77..51acc24 100644 --- a/src/olympic/studio/applib/src/studioui.cpp +++ b/src/olympic/studio/applib/src/studioui.cpp @@ -34,18 +34,6 @@ static bool shutdownHandler(turbine::Context &ctx) { return sctx->ui.handleShutdown(); } -void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept { - ox::String path = std::move(filePath); - if (beginsWith(path, "uuid://")) { - auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path); - if (err) { - return; - } - path = p; - } - ctx.ui.navigateTo(std::move(path), std::move(navArgs)); -} - namespace ig { extern bool s_mainWinHasFocus; } @@ -126,9 +114,8 @@ static ox::Error convertStudioConfigV1ToStudioConfigV2( using StudioConfig = StudioConfigV2; -StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept: - m_sctx{*this, ctx}, - m_tctx{ctx}, +StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept: + m_sctx{*this, tctx}, m_projectDataDir{std::move(projectDataDir)} { { ImFontConfig fontCfg; @@ -139,7 +126,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce // but AddFontFromMemoryTTF requires a mutable buffer. // However, setting fontCfg.FontDataOwnedByAtlas ensures // that it will still be treated as const. - // ImGui documentation recognize that this is a bad design, + // ImGui documentation recognizes that this is a bad design, // and hopefully it will change at some point. io.Fonts->AddFontFromMemoryTTF(const_cast(font.data()), static_cast(font.size()), 13, &fontCfg); } @@ -148,6 +135,9 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce oxLogError(headerizeConfigFile(kctx)); turbine::setApplicationData(m_tctx, &m_sctx); turbine::setShutdownHandler(m_tctx, shutdownHandler); + m_sctx.navCallback = [this](ox::StringParam filePath, ox::StringParam navArgs) { + handleNavigationChange(std::move(filePath), std::move(navArgs)); + }; m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile); m_projectExplorer.addDir.connect(this, &StudioUI::addDir); m_projectExplorer.addItem.connect(this, &StudioUI::addFile); @@ -187,7 +177,17 @@ void StudioUI::handleKeyEvent(turbine::Key const key, bool const down) noexcept } } -void StudioUI::navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept { +void StudioUI::handleMouseButtonEvent(int const btn, bool const down) noexcept { + if (down) { + if (btn == 3) { // back button + navigateBack(m_sctx); + } else if (btn == 4) { // forward button + navigateForward(m_sctx); + } + } +} + +void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept { m_navAction.emplace(std::move(path), std::move(navArgs)); } @@ -271,7 +271,7 @@ void StudioUI::drawMenu() noexcept { ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { - auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; + auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; if (ImGui::MenuItem( "Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) { oxLogError(undoStack->undo()); @@ -311,6 +311,19 @@ void StudioUI::drawMenu() noexcept { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Navigate")) { + constexpr auto backShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+[" : "Alt+Left Arrow"; + constexpr auto fwdShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+]" : "Alt+Right Arrow"; + if (ImGui::MenuItem("Back", backShortcut, false, m_sctx.navIdx > 1)) { + navigateBack(m_sctx); + } + if (ImGui::MenuItem( + "Forward", fwdShortcut, false, + m_sctx.navIdx < m_sctx.navStack.size())) { + navigateForward(m_sctx); + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) { m_aboutPopup.open(); @@ -354,10 +367,16 @@ void StudioUI::drawTabs() noexcept { } if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] { m_activeEditor->onActivated(); + if (!m_sctx.navIdx || + (m_sctx.navIdx <= m_sctx.navStack.size() && + m_sctx.navStack[m_sctx.navIdx - 1].filePath != m_activeEditor->itemPath())) { + m_sctx.navStack.resize(m_sctx.navIdx); + ++m_sctx.navIdx; + m_sctx.navStack.emplace_back(ox::String{m_activeEditor->itemPath()}, ox::String{""}); + } } if (m_closeActiveTab) [[unlikely]] { ImGui::SetTabItemClosed(e->itemDisplayName().c_str()); - } else if (open) [[likely]] { e->draw(m_sctx); } @@ -397,9 +416,10 @@ void StudioUI::drawTabs() noexcept { m_closeActiveTab = false; } if (m_navAction) { - oxLogError(openFile(m_navAction->path)); - m_activeEditor->navigateTo(m_navAction->args); - m_navAction.reset(); + if (!openFile(m_navAction->path)) { + m_activeEditor->navigateTo(m_navAction->args); + m_navAction.reset(); + } } } @@ -418,7 +438,7 @@ void StudioUI::loadModule(Module const &mod) noexcept { } auto tmplts = mod.itemTemplates(m_sctx); for (auto &t : tmplts) { - m_newMenu.installItemTemplate(t); + m_newMenu.installItemTemplate(std::move(t)); } } @@ -508,6 +528,22 @@ void StudioUI::handleKeyInput() noexcept { } } } + if constexpr (ox::defines::OS == ox::OS::Darwin) { + if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) { + navigateBack(m_sctx); + } else if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) { + navigateForward(m_sctx); + } + } + } + if constexpr (ox::defines::OS != ox::OS::Darwin) { + if (ImGui::IsKeyDown(ImGuiKey_ModAlt)) { + if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) { + navigateBack(m_sctx); + } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) { + navigateForward(m_sctx); + } + } } } @@ -553,24 +589,36 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne ox::Error StudioUI::handleDeleteDir(ox::StringViewCR path) noexcept { auto const p = sfmt("{}/", path); - for (auto &e : m_editors) { - if (beginsWith(e->itemPath(), p)) { - oxLogError(closeFile(path)); - m_closeActiveTab = true; - break; - } - } + std::ignore = m_editors.erase( + std::remove_if( + m_editors.begin(), m_editors.end(), + [&](ox::UPtr const &e) { + if (beginsWith(e->itemPath(), p)) { + oxLogError(closeFile(path)); + if (e.get() != m_activeEditor) { + return true; + } + m_closeActiveTab = true; + } + return false; + })); return m_projectExplorer.refreshProjectTreeModel(); } ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept { - for (auto &e : m_editors) { - if (path == e->itemPath()) { - oxLogError(closeFile(path)); - m_closeActiveTab = true; - break; - } - } + std::ignore = m_editors.erase( + std::remove_if( + m_editors.begin(), m_editors.end(), + [&](ox::UPtr const &e) { + if (path == e->itemPath()) { + oxLogError(closeFile(path)); + if (e.get() != m_activeEditor) { + return true; + } + m_closeActiveTab = true; + } + return false; + })); return m_projectExplorer.refreshProjectTreeModel(); } @@ -654,7 +702,10 @@ ox::Error StudioUI::openFile(ox::StringViewCR path) noexcept { ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActiveTab) noexcept { if (!m_project) { - return ox::Error(1, "No project open to open a file from"); + return ox::Error(1, "no project open to open a file from"); + } + if (!m_project->romFs().exists(path)) { + return ox::Error{1, "file does note exist"}; } if (m_openFiles.contains(path)) { for (auto &e : m_editors) { @@ -668,10 +719,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi } OX_REQUIRE(ext, fileExt(path)); // create Editor - BaseEditor *editor = nullptr; + ox::UPtr editor; auto const err = m_editorMakers.contains(ext) ? - m_editorMakers[ext](path).moveTo(editor) : - ox::makeCatch(m_sctx, path).moveTo(editor); + m_editorMakers[ext](path).to>().moveTo(editor) : + ox::make_unique_catch(m_sctx, path).moveTo(editor); if (err) { if constexpr(!ox::defines::Debug) { oxErrf("Could not open Editor: {}\n", toStr(err)); @@ -681,11 +732,11 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi return err; } editor->closed.connect(this, &StudioUI::closeFile); - m_editors.emplace_back(editor); + auto const &e = m_editors.emplace_back(std::move(editor)); m_openFiles.emplace_back(path); if (makeActiveTab) { m_activeEditor = m_editors.back().value->get(); - m_activeEditorUpdatePending = editor; + m_activeEditorUpdatePending = e.get(); } // save to config studio::editConfig(keelCtx(m_tctx), [&path](StudioConfig &config) { diff --git a/src/olympic/studio/applib/src/studioui.hpp b/src/olympic/studio/applib/src/studioui.hpp index d1bf8e8..dc75878 100644 --- a/src/olympic/studio/applib/src/studioui.hpp +++ b/src/olympic/studio/applib/src/studioui.hpp @@ -25,12 +25,12 @@ namespace studio { -class StudioUI: public ox::SignalHandler { +class StudioUI final: public ox::SignalHandler { friend class StudioUIDrawer; private: Context m_sctx; - turbine::Context &m_tctx; + turbine::Context &m_tctx{m_sctx.tctx}; ox::String m_projectDataDir; ox::UPtr m_project; TaskRunner m_taskRunner; @@ -83,11 +83,13 @@ class StudioUI: public ox::SignalHandler { ox::Optional m_navAction; public: - explicit StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept; + explicit StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept; void handleKeyEvent(turbine::Key, bool down) noexcept; - void navigateTo(ox::StringParam path, ox::StringParam navArgs) noexcept; + void handleMouseButtonEvent(int btn, bool down) noexcept; + + void handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept; [[nodiscard]] constexpr Project *project() noexcept { diff --git a/src/olympic/studio/modlib/include/studio/context.hpp b/src/olympic/studio/modlib/include/studio/context.hpp index 17f067a..3efccce 100644 --- a/src/olympic/studio/modlib/include/studio/context.hpp +++ b/src/olympic/studio/modlib/include/studio/context.hpp @@ -15,11 +15,24 @@ namespace studio { class StudioUI; struct Context { - StudioUI &ui; - Project *project = nullptr; - turbine::Context &tctx; - Context(StudioUI &pUi, turbine::Context &pTctx) noexcept: - ui(pUi), tctx(pTctx) {} + public: + friend void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept; + friend void navigateBack(Context &ctx) noexcept; + friend void navigateForward(Context &ctx) noexcept; + friend StudioUI; + StudioUI &ui; + Project *project = nullptr; + turbine::Context &tctx; + protected: + struct NavPath { + ox::String filePath; + ox::String navArgs; + }; + size_t navIdx{}; + ox::Vector navStack; + std::function navCallback; + Context(StudioUI &pUi, turbine::Context &pTctx) noexcept: + ui{pUi}, tctx{pTctx} {} }; [[nodiscard]] @@ -27,6 +40,15 @@ inline keel::Context &keelCtx(Context &ctx) noexcept { return keelCtx(ctx.tctx); } +[[nodiscard]] +inline keel::Context const &keelCtx(Context const &ctx) noexcept { + return keelCtx(ctx.tctx); +} + void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs = "") noexcept; +void navigateBack(Context &ctx) noexcept; + +void navigateForward(Context &ctx) noexcept; + } diff --git a/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp b/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp index 94708e5..acbebce 100644 --- a/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp +++ b/src/olympic/studio/modlib/include/studio/filepickerpopup.hpp @@ -13,7 +13,11 @@ class FilePickerPopup { private: ox::String m_name; - FileExplorer m_explorer; + struct Explorer: public FileExplorer { + mutable bool opened{}; + explicit Explorer(keel::Context &kctx); + void fileOpened(ox::StringViewCR path) const noexcept override; + } m_explorer; ox::Vector const m_fileExts; bool m_open{}; @@ -33,6 +37,10 @@ class FilePickerPopup { ox::Optional draw(Context &ctx) noexcept; + private: + [[nodiscard]] + ox::Optional handlePick() noexcept; + }; } diff --git a/src/olympic/studio/modlib/include/studio/imguiutil.hpp b/src/olympic/studio/modlib/include/studio/imguiutil.hpp index e8d2bb2..dd0b620 100644 --- a/src/olympic/studio/modlib/include/studio/imguiutil.hpp +++ b/src/olympic/studio/modlib/include/studio/imguiutil.hpp @@ -46,14 +46,14 @@ ox::Result getDragDropPayload() noexcept { static_cast(payload->DataSize)}); } -ox::Error setDragDropPayload(ox::CStringViewCR name, auto const&obj) noexcept { +ox::Error setDragDropPayload(ox::CStringViewCR name, auto const &obj) noexcept { OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); ImGui::SetDragDropPayload(name.c_str(), buff.data(), buff.size()); return {}; } template -ox::Error setDragDropPayload(T const&obj) noexcept { +ox::Error setDragDropPayload(T const &obj) noexcept { OX_REQUIRE(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); ImGui::SetDragDropPayload(ox::ModelTypeName_v, buff.data(), buff.size()); return {}; @@ -77,7 +77,7 @@ class DragDropSource { } }; -auto dragDropSource(auto const&cb, ImGuiDragDropFlags const flags = 0) noexcept { +auto dragDropSource(auto const &cb, ImGuiDragDropFlags const flags = 0) noexcept { if constexpr(ox::is_same_v) { if (ig::DragDropSource const tgt{flags}; tgt) [[unlikely]] { return cb(); @@ -108,7 +108,7 @@ class DragDropTarget { } }; -auto dragDropTarget(auto const&cb) noexcept { +auto dragDropTarget(auto const &cb) noexcept { if constexpr(ox::is_same_v) { if (ig::DragDropTarget const tgt; tgt) [[unlikely]] { return cb(); @@ -124,7 +124,7 @@ auto dragDropTarget(auto const&cb) noexcept { class ChildStackItem { public: - explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz = {}) noexcept; + explicit ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz = {}) noexcept; ~ChildStackItem() noexcept; }; @@ -146,7 +146,7 @@ class IndentStackItem { void centerNextWindow(turbine::Context &ctx) noexcept; -bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz = BtnSz) noexcept; +bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz = BtnSz) noexcept; template struct TextInput { @@ -234,7 +234,7 @@ PopupResponse PopupControlsOk( ox::CStringViewCR ok); [[nodiscard]] -bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); +bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz = {285, 0}); /** * @@ -266,7 +266,7 @@ bool ComboBox(ox::CStringView lbl, ox::Span list, size_t &sele */ bool ComboBox( ox::CStringViewCR lbl, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selectedIdx) noexcept; @@ -278,10 +278,10 @@ bool FileComboBox( bool ListBox( ox::CStringViewCR name, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selIdx, - ImVec2 const&sz = {0, 0}) noexcept; + ImVec2 const &sz = {0, 0}) noexcept; /** * @@ -290,7 +290,7 @@ bool ListBox( * @param selIdx * @return true if new value selected, false otherwise */ -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept; +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept; class FilePicker { private: @@ -306,7 +306,7 @@ class FilePicker { studio::Context &sctx, ox::StringParam title, ox::StringParam fileExt, - ImVec2 const&size = {}) noexcept; + ImVec2 const &size = {}) noexcept; void draw() noexcept; @@ -348,6 +348,8 @@ class QuestionPopup: public Popup { QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; + void setQuestion(ox::StringParam msg) noexcept; + void draw(Context &ctx) noexcept override; }; diff --git a/src/olympic/studio/modlib/include/studio/itemmaker.hpp b/src/olympic/studio/modlib/include/studio/itemmaker.hpp index 6d52731..ecd7be2 100644 --- a/src/olympic/studio/modlib/include/studio/itemmaker.hpp +++ b/src/olympic/studio/modlib/include/studio/itemmaker.hpp @@ -109,7 +109,7 @@ class ItemMaker { return m_typeDisplayName; } - bool installTemplate(ox::UPtr &tmpl) { + bool installTemplate(ox::UPtr &&tmpl) { if (typeName() == tmpl->typeName() && typeVersion() <= tmpl->typeVersion()) { m_templates.emplace_back(std::move(tmpl)); @@ -120,10 +120,6 @@ class ItemMaker { return false; } - bool installTemplate(ox::UPtr &&tmpl) { - return installTemplate(tmpl); - } - constexpr ox::Vector> const&itemTemplates() const noexcept { return m_templates; } diff --git a/src/olympic/studio/modlib/src/CMakeLists.txt b/src/olympic/studio/modlib/src/CMakeLists.txt index 28bc274..6adb19b 100644 --- a/src/olympic/studio/modlib/src/CMakeLists.txt +++ b/src/olympic/studio/modlib/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_library( Studio configio.cpp + context.cpp editor.cpp filepickerpopup.cpp filetreemodel.cpp diff --git a/src/olympic/studio/modlib/src/context.cpp b/src/olympic/studio/modlib/src/context.cpp new file mode 100644 index 0000000..674400f --- /dev/null +++ b/src/olympic/studio/modlib/src/context.cpp @@ -0,0 +1,69 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +namespace studio { + +void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept { + ox::String path = std::move(filePath); + if (beginsWith(path, "uuid://")) { + auto const [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path); + if (err) { + return; + } + path = p; + } + ctx.navStack.resize(ctx.navIdx + 1); + ctx.navStack.emplace_back(ox::String{path}, ox::String{navArgs.view()}); + try { + ctx.navCallback(std::move(path), std::move(navArgs)); + } catch (std::exception const &e) { + oxErrf("Nav error: {}", e.what()); + oxAssert(false, "Nav error"); + } +} + +void navigateBack(Context &ctx) noexcept { + if (!ctx.navIdx) { + return; + } + --ctx.navIdx; + while (ctx.navIdx < ctx.navStack.size() && ctx.navIdx) { + auto const i = ctx.navIdx - 1; + auto const &n = ctx.navStack[i]; + if (!ctx.project->exists(n.filePath)) { + std::ignore = ctx.navStack.erase(i); + --ctx.navIdx; + continue; + } + try { + ctx.navCallback(n.filePath, n.navArgs); + } catch (std::exception const &e) { + oxAssert(ctx.navCallback != nullptr, "navCallback is null"); + oxErrf("navigateForward failed: {}", e.what()); + } + break; + } +} + +void navigateForward(Context &ctx) noexcept { + while (ctx.navIdx < ctx.navStack.size()) { + auto const &n = ctx.navStack[ctx.navIdx]; + try { + ctx.navCallback(n.filePath, n.navArgs); + } catch (std::exception const &e) { + oxAssert(ctx.navCallback != nullptr, "navCallback is null"); + oxErrf("navigateForward failed: {}", e.what()); + } + if (!ctx.project->exists(n.filePath)) { + std::ignore = ctx.navStack.erase(ctx.navIdx); + continue; + } + ++ctx.navIdx; + break; + } +} + +} \ No newline at end of file diff --git a/src/olympic/studio/modlib/src/editor.cpp b/src/olympic/studio/modlib/src/editor.cpp index 442a586..d05e05b 100644 --- a/src/olympic/studio/modlib/src/editor.cpp +++ b/src/olympic/studio/modlib/src/editor.cpp @@ -72,8 +72,10 @@ bool BaseEditor::exportable() const noexcept { } void BaseEditor::setCutEnabled(bool v) { - m_cutEnabled = v; - cutEnabledChanged.emit(v); + if (m_cutEnabled != v) { + m_cutEnabled = v; + cutEnabledChanged.emit(v); + } } bool BaseEditor::cutEnabled() const noexcept { @@ -81,8 +83,10 @@ bool BaseEditor::cutEnabled() const noexcept { } void BaseEditor::setCopyEnabled(bool v) { - m_copyEnabled = v; - copyEnabledChanged.emit(v); + if (m_copyEnabled != v) { + m_copyEnabled = v; + copyEnabledChanged.emit(v); + } } bool BaseEditor::copyEnabled() const noexcept { @@ -90,8 +94,10 @@ bool BaseEditor::copyEnabled() const noexcept { } void BaseEditor::setPasteEnabled(bool v) { - m_pasteEnabled = v; - pasteEnabledChanged.emit(v); + if (m_pasteEnabled != v) { + m_pasteEnabled = v; + pasteEnabledChanged.emit(v); + } } bool BaseEditor::pasteEnabled() const noexcept { diff --git a/src/olympic/studio/modlib/src/filepickerpopup.cpp b/src/olympic/studio/modlib/src/filepickerpopup.cpp index 2b499fe..e4390ff 100644 --- a/src/olympic/studio/modlib/src/filepickerpopup.cpp +++ b/src/olympic/studio/modlib/src/filepickerpopup.cpp @@ -8,6 +8,15 @@ namespace studio { +FilePickerPopup::Explorer::Explorer(keel::Context &kctx): + FileExplorer{kctx} { +} + +void FilePickerPopup::Explorer::fileOpened(ox::StringViewCR) const noexcept { + opened = true; +} + + FilePickerPopup::FilePickerPopup( ox::StringParam name, keel::Context &kctx, @@ -41,6 +50,7 @@ void FilePickerPopup::refresh() noexcept { void FilePickerPopup::open() noexcept { refresh(); m_open = true; + m_explorer.opened = false; } void FilePickerPopup::close() noexcept { @@ -60,16 +70,22 @@ ox::Optional FilePickerPopup::draw(Context &ctx) noexcept { if (ig::BeginPopup(ctx.tctx, m_name, m_open, {380, 340})) { auto const vp = ImGui::GetContentRegionAvail(); m_explorer.draw(ctx, {vp.x, vp.y - 30}); - if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) { - auto p = m_explorer.selectedPath(); - if (p) { - out.emplace(*p); - } - close(); + if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK || m_explorer.opened) { + out = handlePick(); } ImGui::EndPopup(); } return out; } +ox::Optional FilePickerPopup::handlePick() noexcept { + ox::Optional out; + auto p = m_explorer.selectedPath(); + if (p) { + out.emplace(*p); + } + close(); + return out; +} + } diff --git a/src/olympic/studio/modlib/src/imguiutil.cpp b/src/olympic/studio/modlib/src/imguiutil.cpp index e4886d9..2fc7585 100644 --- a/src/olympic/studio/modlib/src/imguiutil.cpp +++ b/src/olympic/studio/modlib/src/imguiutil.cpp @@ -10,7 +10,7 @@ namespace studio::ig { -ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const&sz) noexcept { +ChildStackItem::ChildStackItem(ox::CStringViewCR id, ImVec2 const &sz) noexcept { ImGui::BeginChild(id.c_str(), sz); } @@ -50,7 +50,7 @@ void centerNextWindow(turbine::Context &ctx) noexcept { ImGui::SetNextWindowPos(ImVec2(screenW / mod, screenH / mod), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); } -bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept { +bool PushButton(ox::CStringViewCR lbl, ImVec2 const &btnSz) noexcept { return ImGui::Button(lbl.c_str(), btnSz); } @@ -101,7 +101,7 @@ PopupResponse PopupControlsOk( return out; } -bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) { +bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const &sz) { constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; centerNextWindow(ctx); ImGui::OpenPopup(popupName.c_str()); @@ -149,7 +149,7 @@ bool ComboBox( bool ComboBox( ox::CStringViewCR lbl, - std::function const&f, + std::function const &f, size_t strCnt, size_t &selectedIdx) noexcept { bool out{}; @@ -172,16 +172,16 @@ bool FileComboBox( Context &sctx, ox::StringViewCR fileExt, size_t &selectedIdx) noexcept { - auto const&list = sctx.project->fileList(fileExt); + auto const &list = sctx.project->fileList(fileExt); return ComboBox(lbl, list, selectedIdx); } bool ListBox( ox::CStringViewCR name, - std::function const&f, + std::function const &f, size_t const strCnt, size_t &selIdx, - ImVec2 const&sz) noexcept { + ImVec2 const &sz) noexcept { auto out = false; if (ImGui::BeginListBox(name.c_str(), sz)) { for (size_t i = 0; i < strCnt; ++i) { @@ -199,13 +199,13 @@ bool ListBox( return out; } -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept { +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept { return ListBox(name, [list](size_t i) -> ox::CStringView { return list[i]; }, list.size(), selIdx); } -bool ListBox(ox::CStringViewCR name, ox::SpanView const&list, size_t &selIdx) noexcept { +bool ListBox(ox::CStringViewCR name, ox::SpanView const &list, size_t &selIdx) noexcept { return ListBox(name, [list](size_t i) -> ox::CStringView { return list[i]; }, list.size(), selIdx); @@ -216,7 +216,7 @@ FilePicker::FilePicker( Context &sctx, ox::StringParam title, ox::StringParam fileExt, - ImVec2 const&size) noexcept: + ImVec2 const &size) noexcept: m_sctx(sctx), m_title(std::move(title)), m_fileExt(std::move(fileExt)), @@ -230,7 +230,7 @@ void FilePicker::draw() noexcept { auto constexpr popupSz = ImVec2{450.f, 0}; IDStackItem const idStackItem(m_title); if (BeginPopup(m_sctx.tctx, m_title, m_show, popupSz)) { - auto const&list = m_sctx.project->fileList(m_fileExt); + auto const &list = m_sctx.project->fileList(m_fileExt); size_t selIdx{}; ComboBox(m_title, list, selIdx); if (PopupControlsOkCancel(popupSz.x, m_show) == ig::PopupResponse::OK) { @@ -267,6 +267,10 @@ QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) no m_question{std::move(question)} { } +void QuestionPopup::setQuestion(ox::StringParam msg) noexcept { + m_question = std::move(msg); +} + void QuestionPopup::draw(Context &ctx) noexcept { switch (m_stage) { case Stage::Closed: diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index f5801b3..214902e 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -101,8 +101,8 @@ ox::Result Project::stat(ox::StringViewCR path) const noexcept { 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); + OX_RETURN_ERROR(writeBuff(dest, ox::BufferView{buff} + keel::K1HdrSz)); return {}; } diff --git a/src/olympic/turbine/include/turbine/turbine.hpp b/src/olympic/turbine/include/turbine/turbine.hpp index 338cd4b..65cce5d 100644 --- a/src/olympic/turbine/include/turbine/turbine.hpp +++ b/src/olympic/turbine/include/turbine/turbine.hpp @@ -79,11 +79,14 @@ enum Key { }; using KeyEventHandler = void(*)(Context&, Key, bool); +using MouseButtonEventHandler = void(*)(Context&, int btn, bool); void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept; +void setMouseButtonEventHandler(Context &ctx, MouseButtonEventHandler h) noexcept; + [[nodiscard]] -KeyEventHandler keyEventHandler(Context &ctx) noexcept; +KeyEventHandler keyEventHandler(Context const &ctx) noexcept; [[nodiscard]] bool buttonDown(Context const&ctx, Key) noexcept; diff --git a/src/olympic/turbine/src/gba/turbine.cpp b/src/olympic/turbine/src/gba/turbine.cpp index c015e03..df24c0e 100644 --- a/src/olympic/turbine/src/gba/turbine.cpp +++ b/src/olympic/turbine/src/gba/turbine.cpp @@ -133,7 +133,9 @@ void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept { ctx.keyEventHandler = h; } -KeyEventHandler keyEventHandler(Context &ctx) noexcept { +void setMouseButtonEventHandler(Context&, MouseButtonEventHandler) noexcept {} + +KeyEventHandler keyEventHandler(Context const &ctx) noexcept { return ctx.keyEventHandler; } diff --git a/src/olympic/turbine/src/glfw/context.hpp b/src/olympic/turbine/src/glfw/context.hpp index c526b6b..a9e1d31 100644 --- a/src/olympic/turbine/src/glfw/context.hpp +++ b/src/olympic/turbine/src/glfw/context.hpp @@ -16,6 +16,7 @@ class Context { UpdateHandler updateHandler = [](Context&) -> int {return -1;}; keel::Context keelCtx; KeyEventHandler keyEventHandler = nullptr; + MouseButtonEventHandler mouseButtonEventHandler = nullptr; ox::AnyPtr applicationData; // GLFW impl data //////////////////////////////////////////////////////// diff --git a/src/olympic/turbine/src/glfw/turbine.cpp b/src/olympic/turbine/src/glfw/turbine.cpp index ff02f4d..716e703 100644 --- a/src/olympic/turbine/src/glfw/turbine.cpp +++ b/src/olympic/turbine/src/glfw/turbine.cpp @@ -299,9 +299,14 @@ static void handleKeyPress(Context &ctx, int const key, bool const down) noexcep static void handleGlfwCursorPosEvent(GLFWwindow*, double, double) noexcept { } -static void handleGlfwMouseButtonEvent(GLFWwindow *window, int, int, int) noexcept { +static void handleGlfwMouseButtonEvent( + GLFWwindow *window, + int const btn, + int const action, + int) noexcept { auto &ctx = *static_cast(glfwGetWindowUserPointer(window)); setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod); + ctx.mouseButtonEventHandler(ctx, btn, action == 1); } static void handleGlfwKeyEvent(GLFWwindow *window, int const key, int, int const action, int) noexcept { @@ -339,7 +344,8 @@ ox::Result> init( glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); constexpr auto Scale = 5; - ctx->window = glfwCreateWindow(240 * Scale, 160 * Scale, ctx->keelCtx.appName.c_str(), nullptr, nullptr); + ctx->window = glfwCreateWindow( + 240 * Scale, 160 * Scale, ctx->keelCtx.appName.c_str(), nullptr, nullptr); //ctx.window = glfwCreateWindow(876, 743, ctx.keelCtx.appName.c_str(), nullptr, nullptr); if (ctx->window == nullptr) { return ox::Error(1, "Could not open GLFW window"); @@ -402,14 +408,14 @@ ox::Error run(Context &ctx) noexcept { glfwPollEvents(); auto const ticks = ticksMs(ctx); if (ctx.wakeupTime <= ticks) { - auto const st = ctx.updateHandler(ctx); - if (st >= 0) { - ctx.wakeupTime = ticks + static_cast(st); - sleepTime = static_cast(st); - } else { - ctx.wakeupTime = ~uint64_t{0}; - sleepTime = ctx.wakeupTime - ticks; - } + auto const st = ctx.updateHandler(ctx); + if (st >= 0) { + ctx.wakeupTime = ticks + static_cast(st); + sleepTime = static_cast(st); + } else { + ctx.wakeupTime = ~uint64_t{0}; + sleepTime = ctx.wakeupTime - ticks; + } } else { sleepTime = ctx.wakeupTime - ticks; } @@ -447,15 +453,19 @@ void setShutdownHandler(Context &ctx, ShutdownHandler const handler) noexcept { ctx.shutdownHandler = handler; } -void setUpdateHandler(Context &ctx, UpdateHandler h) noexcept { +void setUpdateHandler(Context &ctx, UpdateHandler const h) noexcept { ctx.updateHandler = h; } -void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept { +void setKeyEventHandler(Context &ctx, KeyEventHandler const h) noexcept { ctx.keyEventHandler = h; } -KeyEventHandler keyEventHandler(Context &ctx) noexcept { +void setMouseButtonEventHandler(Context &ctx, MouseButtonEventHandler const h) noexcept { + ctx.mouseButtonEventHandler = h; +} + +KeyEventHandler keyEventHandler(Context const &ctx) noexcept { return ctx.keyEventHandler; } diff --git a/util/scripts/loc.py b/util/scripts/loc.py new file mode 100755 index 0000000..c887801 --- /dev/null +++ b/util/scripts/loc.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python3 + +from pathlib import Path +import argparse + +def parse_args(): + parser = argparse.ArgumentParser(description="Count and sort lines of code in selected files.") + parser.add_argument( + "--search-dirs", nargs="+", required=True, + help="List of directories to search (recursively)." + ) + parser.add_argument( + "--include-exts", nargs="+", required=True, + help="List of file extensions to include (e.g. .cpp .hpp .py)" + ) + parser.add_argument( + "--exclude-paths", nargs="*", default=[], + help="Full file paths to exclude from counting." + ) + return parser.parse_args() + + +def main(): + args = parse_args() + include_exts = tuple(args.include_exts) + exclude_paths = {Path(p).resolve() for p in args.exclude_paths} + files = [] + for base in args.search_dirs: + path = Path(base) + if path.is_dir(): + for file in path.rglob("*"): + try: + resolved_file = file.resolve() + if ( + file.is_file() + and file.name.endswith(include_exts) + and resolved_file not in exclude_paths + ): + files.append(str(resolved_file)) + except FileNotFoundError: + continue + line_counts = [] + total_lines = 0 + for f in files: + try: + with open(f, "r", encoding="utf-8", errors="ignore") as file: + lines = sum(1 for _ in file) + line_counts.append((lines, f)) + total_lines += lines + except Exception as e: + print(f"Failed to read {f}: {e}") + line_counts.sort() + for count, filename in line_counts: + print(f"{count:>7} {filename}") + print(f"{total_lines:>7} total") + + +if __name__ == "__main__": + main()