From 62561624ed4f8d09de2b532095373dc79a7e3c54 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 12:37:32 +0200 Subject: [PATCH] Extract main toolbar action planning --- CMakeLists.txt | 1 + docs/modernization/debt.md | 3 +- docs/modernization/roadmap.md | 13 ++ src/app_core/main_toolbar.h | 146 ++++++++++++++++++++ src/app_layout.cpp | 54 ++++++-- tests/CMakeLists.txt | 34 +++++ tests/app_core/main_toolbar_tests.cpp | 114 ++++++++++++++++ tools/pano_cli/main.cpp | 186 ++++++++++++++++++++++++++ 8 files changed, 536 insertions(+), 15 deletions(-) create mode 100644 src/app_core/main_toolbar.h create mode 100644 tests/app_core/main_toolbar_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b230f1..ae90848 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -243,6 +243,7 @@ add_library(pp_app_core STATIC src/app_core/file_menu.h src/app_core/grid_ui.h src/app_core/history_ui.h + src/app_core/main_toolbar.h src/app_core/quick_ui.h src/app_core/tools_menu.h) target_include_directories(pp_app_core diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 3f65fa3..d0bf61b 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, status/display UI rendering, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, status/display UI rendering, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | @@ -52,6 +52,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0032 | Open | Modernization | Layer menu command planning now consumes pure `pp_app_core` through `App::init_menu_layer` and `pano_cli plan-layer-menu`, but live execution still calls legacy `Canvas::clear`, `App::dialog_layer_rename`, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer clear, rename, merge-down execution, animation gating, and selected-layer state are owned by document/app services with Layer-menu callbacks acting only as UI adapters | | DEBT-0033 | Open | Modernization | Tools menu and floating-panel planning now consumes pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, and `pano_cli plan-tools-panel`, but live execution still constructs legacy `NodePanelFloating` panels, mutates legacy panel nodes, clears `CanvasModeGrid`, resets `NodeCanvas` camera state, opens legacy shortcuts UI, and calls the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter | | DEBT-0034 | Open | Modernization | About menu command planning now consumes pure `pp_app_core` through `App::init_menu_about` and `pano_cli plan-about-menu`, but live execution still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by app/UI/platform services with `App::init_menu_about` acting only as a UI adapter | +| DEBT-0035 | Open | Modernization | Main toolbar/status command planning now consumes pure `pp_app_core` through `App::init_toolbar_main` and `pano_cli plan-main-toolbar`, but live execution still opens legacy open/save/settings/message-box dialogs, mutates legacy `ActionManager` history, and clears the legacy `Canvas` directly | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 7742e7f..167d55a 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -534,6 +534,11 @@ updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. `pano_cli plan-history-operation` exposes app-core planning for undo, redo, and clear-history availability used by toolbar buttons and canvas shortcuts before legacy `ActionManager` stack execution continues. +`pano_cli plan-main-toolbar` exposes app-core planning for the live main +toolbar/status-bar shell, including open/save dialogs, undo/redo availability, +clear-history availability, clear-canvas no-canvas blocking, message-box +creation, and settings dialog routing before legacy dialogs, `ActionManager`, +and `Canvas` execution continue. `pano_cli plan-quick-operation` exposes app-core planning for quick brush/color slot selection versus popup opening, plus quick mini-state restore/reset validation used by the live quick panel before legacy `Brush`, color picker, @@ -1286,6 +1291,14 @@ Results: `pano_cli_plan_about_menu_crash_disabled_smoke`, and `pano_cli_plan_about_menu_rejects_unknown` passed and expose live About menu planning as JSON automation. +- `pp_app_core_main_toolbar_tests` passed, covering live toolbar/status direct + dialog routing, undo/redo availability, clear-history availability, no-canvas + clear blocking, and negative history metric rejection. +- `pano_cli_plan_main_toolbar_undo_smoke`, + `pano_cli_plan_main_toolbar_redo_empty_smoke`, + `pano_cli_plan_main_toolbar_clear_canvas_no_canvas_smoke`, and + `pano_cli_plan_main_toolbar_rejects_negative_count` passed and expose live + toolbar/status planning as JSON automation. - `pp_app_core_document_sharing_tests` passed, covering saved-path gating before platform share execution. - `pano_cli_plan_share_file_unsaved_smoke` and diff --git a/src/app_core/main_toolbar.h b/src/app_core/main_toolbar.h new file mode 100644 index 0000000..6455280 --- /dev/null +++ b/src/app_core/main_toolbar.h @@ -0,0 +1,146 @@ +#pragma once + +#include "app_core/document_canvas.h" +#include "app_core/history_ui.h" +#include "foundation/result.h" + +#include + +namespace pp::app { + +enum class MainToolbarCommand { + open_document, + save_document, + undo, + redo, + clear_history, + clear_canvas, + show_message_box, + show_settings, +}; + +enum class MainToolbarAction { + show_open_dialog, + show_save_dialog, + invoke_undo, + invoke_redo, + clear_history, + clear_canvas, + show_message_box, + show_settings_dialog, + no_op_unavailable, +}; + +struct MainToolbarPlan { + MainToolbarCommand command = MainToolbarCommand::open_document; + MainToolbarAction action = MainToolbarAction::show_open_dialog; + std::string label; + bool requires_canvas = false; + bool updates_memory_label = false; + bool updates_title = false; + bool records_undo = false; + bool marks_unsaved = false; + bool no_op = false; +}; + +[[nodiscard]] inline pp::foundation::Result plan_main_toolbar_command( + MainToolbarCommand command, + int undo_count = 0, + int redo_count = 0, + int memory_bytes = 0, + bool has_canvas = true) +{ + MainToolbarPlan plan; + plan.command = command; + + switch (command) { + case MainToolbarCommand::open_document: + plan.action = MainToolbarAction::show_open_dialog; + plan.label = "Open"; + return pp::foundation::Result::success(plan); + + case MainToolbarCommand::save_document: + plan.action = MainToolbarAction::show_save_dialog; + plan.label = "Save"; + return pp::foundation::Result::success(plan); + + case MainToolbarCommand::undo: + { + const auto history = plan_history_undo(undo_count); + if (!history) { + return pp::foundation::Result::failure(history.status()); + } + plan.action = history.value().invokes_undo + ? MainToolbarAction::invoke_undo + : MainToolbarAction::no_op_unavailable; + plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)"; + plan.updates_memory_label = history.value().updates_memory_label; + plan.updates_title = history.value().updates_title; + plan.no_op = history.value().no_op; + return pp::foundation::Result::success(plan); + } + + case MainToolbarCommand::redo: + { + const auto history = plan_history_redo(redo_count); + if (!history) { + return pp::foundation::Result::failure(history.status()); + } + plan.action = history.value().invokes_redo + ? MainToolbarAction::invoke_redo + : MainToolbarAction::no_op_unavailable; + plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)"; + plan.updates_memory_label = history.value().updates_memory_label; + plan.updates_title = history.value().updates_title; + plan.no_op = history.value().no_op; + return pp::foundation::Result::success(plan); + } + + case MainToolbarCommand::clear_history: + { + const auto history = plan_history_clear(undo_count, redo_count, memory_bytes); + if (!history) { + return pp::foundation::Result::failure(history.status()); + } + plan.action = history.value().clears_history + ? MainToolbarAction::clear_history + : MainToolbarAction::no_op_unavailable; + plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)"; + plan.updates_memory_label = history.value().updates_memory_label; + plan.no_op = history.value().no_op; + return pp::foundation::Result::success(plan); + } + + case MainToolbarCommand::clear_canvas: + { + const auto clear = plan_document_canvas_clear(has_canvas); + if (!clear) { + return pp::foundation::Result::failure(clear.status()); + } + plan.action = clear.value().clears_canvas + ? MainToolbarAction::clear_canvas + : MainToolbarAction::no_op_unavailable; + plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)"; + plan.requires_canvas = true; + plan.records_undo = clear.value().records_undo; + plan.marks_unsaved = clear.value().marks_unsaved; + plan.no_op = clear.value().no_op; + return pp::foundation::Result::success(plan); + } + + case MainToolbarCommand::show_message_box: + plan.action = MainToolbarAction::show_message_box; + plan.label = "Show Message Box"; + return pp::foundation::Result::success(plan); + + case MainToolbarCommand::show_settings: + plan.action = MainToolbarAction::show_settings_dialog; + plan.label = "Settings"; + return pp::foundation::Result::success(plan); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown main toolbar command")); +} + +} // namespace pp::app diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 637cf71..1fbed08 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -17,6 +17,7 @@ #include "app_core/file_menu.h" #include "app_core/app_status.h" #include "app_core/history_ui.h" +#include "app_core/main_toolbar.h" #include "app_core/tools_menu.h" #include "settings.h" #include "serializer.h" @@ -281,39 +282,51 @@ void App::init_toolbar_main() if (auto* button = layout[main_id]->find("btn-open")) { button->on_click = [this, button](Node*) { - dialog_open(); + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::open_document); + if (plan && plan.value().action == pp::app::MainToolbarAction::show_open_dialog) + dialog_open(); }; } if (auto* button = layout[main_id]->find("btn-save")) { button->on_click = [this, button](Node*) { - dialog_save(); + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::save_document); + if (plan && plan.value().action == pp::app::MainToolbarAction::show_save_dialog) + dialog_save(); }; } if (auto* button = layout[main_id]->find("btn-undo")) { button->on_click = [this, button](Node*) { - const auto plan = pp::app::plan_history_undo(static_cast(ActionManager::I.m_actions.size())); - if (plan && plan.value().invokes_undo) + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::undo, + static_cast(ActionManager::I.m_actions.size())); + if (plan && plan.value().action == pp::app::MainToolbarAction::invoke_undo) ActionManager::undo(); }; } if (auto* button = layout[main_id]->find("btn-redo")) { button->on_click = [this, button](Node*) { - const auto plan = pp::app::plan_history_redo(static_cast(ActionManager::I.m_redos.size())); - if (plan && plan.value().invokes_redo) + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::redo, + 0, + static_cast(ActionManager::I.m_redos.size())); + if (plan && plan.value().action == pp::app::MainToolbarAction::invoke_redo) ActionManager::redo(); }; } if (auto* button = layout[main_id]->find("btn-clean-memory")) { button->on_click = [this](Node*) { - const auto plan = pp::app::plan_history_clear( + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::clear_history, static_cast(ActionManager::I.m_actions.size()), static_cast(ActionManager::I.m_redos.size()), static_cast(ActionManager::I.m_memory)); - if (plan && plan.value().clears_history) + if (plan && plan.value().action == pp::app::MainToolbarAction::clear_history) ActionManager::clear(); }; } @@ -321,18 +334,27 @@ void App::init_toolbar_main() { button->on_click = [this](Node*) { //exit(0); - const auto plan = pp::app::plan_document_canvas_clear(static_cast(canvas)); - if (plan && plan.value().clears_canvas) + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::clear_canvas, + 0, + 0, + 0, + static_cast(canvas)); + if (plan && plan.value().action == pp::app::MainToolbarAction::clear_canvas) canvas->m_canvas->clear({ - plan.value().r, - plan.value().g, - plan.value().b, - plan.value().a }); + 0.0F, + 0.0F, + 0.0F, + 0.0F }); }; } if (auto* button = layout[main_id]->find("btn-popup")) { button->on_click = [this](Node*) { + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::show_message_box); + if (!plan || plan.value().action != pp::app::MainToolbarAction::show_message_box) + return; msgbox = new NodeMessageBox(); msgbox->set_manager(&layout); msgbox->init(); @@ -342,6 +364,10 @@ void App::init_toolbar_main() if (auto* button = layout[main_id]->find("btn-settings")) { button->on_click = [this](Node*) { + const auto plan = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::show_settings); + if (!plan || plan.value().action != pp::app::MainToolbarAction::show_settings_dialog) + return; settings = new NodeSettings(); settings->set_manager(&layout); settings->init(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d1c95c0..698e181 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -318,6 +318,16 @@ add_test(NAME pp_app_core_history_ui_tests COMMAND pp_app_core_history_ui_tests) set_tests_properties(pp_app_core_history_ui_tests PROPERTIES LABELS "app;document;ui;desktop-fast;fuzz") +add_executable(pp_app_core_main_toolbar_tests + app_core/main_toolbar_tests.cpp) +target_link_libraries(pp_app_core_main_toolbar_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_main_toolbar_tests COMMAND pp_app_core_main_toolbar_tests) +set_tests_properties(pp_app_core_main_toolbar_tests PROPERTIES + LABELS "app;document;ui;desktop-fast;fuzz") + add_executable(pp_app_core_quick_ui_tests app_core/quick_ui_tests.cpp) target_link_libraries(pp_app_core_quick_ui_tests PRIVATE @@ -1182,6 +1192,30 @@ if(TARGET pano_cli) LABELS "app;document;ui;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_main_toolbar_undo_smoke + COMMAND pano_cli plan-main-toolbar --command undo --undo-count 2) + set_tests_properties(pano_cli_plan_main_toolbar_undo_smoke PROPERTIES + LABELS "app;document;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-main-toolbar\".*\"command\":\"undo\".*\"action\":\"invoke-undo\".*\"updatesMemoryLabel\":true.*\"updatesTitle\":true.*\"noOp\":false") + + add_test(NAME pano_cli_plan_main_toolbar_redo_empty_smoke + COMMAND pano_cli plan-main-toolbar --command redo) + set_tests_properties(pano_cli_plan_main_toolbar_redo_empty_smoke PROPERTIES + LABELS "app;document;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-main-toolbar\".*\"command\":\"redo\".*\"action\":\"no-op-unavailable\".*\"label\":\"Redo \\(No history\\)\".*\"noOp\":true") + + add_test(NAME pano_cli_plan_main_toolbar_clear_canvas_no_canvas_smoke + COMMAND pano_cli plan-main-toolbar --command clear-canvas --no-canvas) + set_tests_properties(pano_cli_plan_main_toolbar_clear_canvas_no_canvas_smoke PROPERTIES + LABELS "app;document;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-main-toolbar\".*\"hasCanvas\":false.*\"action\":\"no-op-unavailable\".*\"label\":\"Clear Canvas \\(No canvas\\)\".*\"requiresCanvas\":true.*\"recordsUndo\":false.*\"marksUnsaved\":false.*\"noOp\":true") + + add_test(NAME pano_cli_plan_main_toolbar_rejects_negative_count + COMMAND pano_cli plan-main-toolbar --command undo --undo-count -1) + set_tests_properties(pano_cli_plan_main_toolbar_rejects_negative_count PROPERTIES + LABELS "app;document;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_quick_operation_select_brush_smoke COMMAND pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2) set_tests_properties(pano_cli_plan_quick_operation_select_brush_smoke PROPERTIES diff --git a/tests/app_core/main_toolbar_tests.cpp b/tests/app_core/main_toolbar_tests.cpp new file mode 100644 index 0000000..dbdd8a2 --- /dev/null +++ b/tests/app_core/main_toolbar_tests.cpp @@ -0,0 +1,114 @@ +#include "app_core/main_toolbar.h" +#include "test_harness.h" + +namespace { + +void direct_dialog_commands_are_available(pp::tests::Harness& harness) +{ + const auto open = pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::open_document); + PP_EXPECT(harness, open); + if (open) { + PP_EXPECT(harness, open.value().action == pp::app::MainToolbarAction::show_open_dialog); + PP_EXPECT(harness, open.value().label == "Open"); + PP_EXPECT(harness, !open.value().no_op); + } + + const auto save = pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::save_document); + PP_EXPECT(harness, save); + if (save) { + PP_EXPECT(harness, save.value().action == pp::app::MainToolbarAction::show_save_dialog); + PP_EXPECT(harness, save.value().label == "Save"); + } + + const auto settings = pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::show_settings); + PP_EXPECT(harness, settings); + if (settings) { + PP_EXPECT(harness, settings.value().action == pp::app::MainToolbarAction::show_settings_dialog); + PP_EXPECT(harness, settings.value().label == "Settings"); + } +} + +void history_commands_reuse_history_breakpoints(pp::tests::Harness& harness) +{ + const auto undo = pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::undo, 2); + PP_EXPECT(harness, undo); + if (undo) { + PP_EXPECT(harness, undo.value().action == pp::app::MainToolbarAction::invoke_undo); + PP_EXPECT(harness, undo.value().updates_memory_label); + PP_EXPECT(harness, undo.value().updates_title); + PP_EXPECT(harness, !undo.value().no_op); + } + + const auto redo_empty = pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::redo, 0, 0); + PP_EXPECT(harness, redo_empty); + if (redo_empty) { + PP_EXPECT(harness, redo_empty.value().action == pp::app::MainToolbarAction::no_op_unavailable); + PP_EXPECT(harness, redo_empty.value().label == "Redo (No history)"); + PP_EXPECT(harness, redo_empty.value().no_op); + } + + const auto clear = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::clear_history, + 0, + 0, + 2048); + PP_EXPECT(harness, clear); + if (clear) { + PP_EXPECT(harness, clear.value().action == pp::app::MainToolbarAction::clear_history); + PP_EXPECT(harness, clear.value().updates_memory_label); + PP_EXPECT(harness, !clear.value().updates_title); + } +} + +void canvas_clear_requires_live_canvas(pp::tests::Harness& harness) +{ + const auto clear = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::clear_canvas, + 0, + 0, + 0, + true); + PP_EXPECT(harness, clear); + if (clear) { + PP_EXPECT(harness, clear.value().action == pp::app::MainToolbarAction::clear_canvas); + PP_EXPECT(harness, clear.value().requires_canvas); + PP_EXPECT(harness, clear.value().records_undo); + PP_EXPECT(harness, clear.value().marks_unsaved); + PP_EXPECT(harness, !clear.value().no_op); + } + + const auto missing_canvas = pp::app::plan_main_toolbar_command( + pp::app::MainToolbarCommand::clear_canvas, + 0, + 0, + 0, + false); + PP_EXPECT(harness, missing_canvas); + if (missing_canvas) { + PP_EXPECT(harness, missing_canvas.value().action == pp::app::MainToolbarAction::no_op_unavailable); + PP_EXPECT(harness, missing_canvas.value().label == "Clear Canvas (No canvas)"); + PP_EXPECT(harness, missing_canvas.value().requires_canvas); + PP_EXPECT(harness, !missing_canvas.value().records_undo); + PP_EXPECT(harness, !missing_canvas.value().marks_unsaved); + PP_EXPECT(harness, missing_canvas.value().no_op); + } +} + +void rejects_negative_history_metrics(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::undo, -1)); + PP_EXPECT(harness, !pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::redo, 0, -1)); + PP_EXPECT(harness, !pp::app::plan_main_toolbar_command(pp::app::MainToolbarCommand::clear_history, 0, 0, -1)); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("direct dialog commands are available", direct_dialog_commands_are_available); + harness.run("history commands reuse history breakpoints", history_commands_reuse_history_breakpoints); + harness.run("canvas clear requires live canvas", canvas_clear_requires_live_canvas); + harness.run("rejects negative history metrics", rejects_negative_history_metrics); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index fcfffd9..5165994 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -18,6 +18,7 @@ #include "app_core/file_menu.h" #include "app_core/grid_ui.h" #include "app_core/history_ui.h" +#include "app_core/main_toolbar.h" #include "app_core/quick_ui.h" #include "app_core/tools_menu.h" #include "assets/image_format.h" @@ -322,6 +323,14 @@ struct PlanHistoryOperationArgs { int memory_bytes = 0; }; +struct PlanMainToolbarArgs { + std::string command = "undo"; + int undo_count = 0; + int redo_count = 0; + int memory_bytes = 0; + bool has_canvas = true; +}; + struct PlanCanvasToolArgs { std::string kind = "draw"; bool current_mode_draw = false; @@ -1107,6 +1116,56 @@ const char* history_ui_operation_name(pp::app::HistoryUiOperation operation) noe return "undo"; } +const char* main_toolbar_command_name(pp::app::MainToolbarCommand command) noexcept +{ + switch (command) { + case pp::app::MainToolbarCommand::open_document: + return "open"; + case pp::app::MainToolbarCommand::save_document: + return "save"; + case pp::app::MainToolbarCommand::undo: + return "undo"; + case pp::app::MainToolbarCommand::redo: + return "redo"; + case pp::app::MainToolbarCommand::clear_history: + return "clear-history"; + case pp::app::MainToolbarCommand::clear_canvas: + return "clear-canvas"; + case pp::app::MainToolbarCommand::show_message_box: + return "message-box"; + case pp::app::MainToolbarCommand::show_settings: + return "settings"; + } + + return "open"; +} + +const char* main_toolbar_action_name(pp::app::MainToolbarAction action) noexcept +{ + switch (action) { + case pp::app::MainToolbarAction::show_open_dialog: + return "show-open-dialog"; + case pp::app::MainToolbarAction::show_save_dialog: + return "show-save-dialog"; + case pp::app::MainToolbarAction::invoke_undo: + return "invoke-undo"; + case pp::app::MainToolbarAction::invoke_redo: + return "invoke-redo"; + case pp::app::MainToolbarAction::clear_history: + return "clear-history"; + case pp::app::MainToolbarAction::clear_canvas: + return "clear-canvas"; + case pp::app::MainToolbarAction::show_message_box: + return "show-message-box"; + case pp::app::MainToolbarAction::show_settings_dialog: + return "show-settings-dialog"; + case pp::app::MainToolbarAction::no_op_unavailable: + return "no-op-unavailable"; + } + + return "no-op-unavailable"; +} + const char* quick_ui_slot_kind_name(pp::app::QuickUiSlotKind kind) noexcept { switch (kind) { @@ -1494,6 +1553,7 @@ void print_help() << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" + << " plan-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\n" << " plan-quick-operation --kind brush|color|restore|reset [--current-index N] [--slot-index N] [--brush-index N] [--color-index N] [--slot-count N] [--fire-event]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" @@ -4458,6 +4518,128 @@ int plan_history_operation(int argc, char** argv) return 0; } +pp::foundation::Result parse_main_toolbar_command(std::string_view command) +{ + if (command == "open") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::open_document); + } + if (command == "save") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::save_document); + } + if (command == "undo") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::undo); + } + if (command == "redo") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::redo); + } + if (command == "clear-history") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::clear_history); + } + if (command == "clear-canvas") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::clear_canvas); + } + if (command == "message-box") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::show_message_box); + } + if (command == "settings") { + return pp::foundation::Result::success( + pp::app::MainToolbarCommand::show_settings); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown main toolbar command")); +} + +pp::foundation::Status parse_plan_main_toolbar_args( + int argc, + char** argv, + PlanMainToolbarArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--command") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.command = argv[++i]; + } else if (key == "--undo-count" || key == "--redo-count" || key == "--memory-bytes") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--undo-count") { + args.undo_count = value.value(); + } else if (key == "--redo-count") { + args.redo_count = value.value(); + } else { + args.memory_bytes = value.value(); + } + } else if (key == "--no-canvas") { + args.has_canvas = false; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_main_toolbar(int argc, char** argv) +{ + PlanMainToolbarArgs args; + const auto status = parse_plan_main_toolbar_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-main-toolbar", status.message); + return 2; + } + + const auto command = parse_main_toolbar_command(args.command); + if (!command) { + print_error("plan-main-toolbar", command.status().message); + return 2; + } + + const auto plan = pp::app::plan_main_toolbar_command( + command.value(), + args.undo_count, + args.redo_count, + args.memory_bytes, + args.has_canvas); + if (!plan) { + print_error("plan-main-toolbar", plan.status().message); + return 2; + } + + const auto& value = plan.value(); + std::cout << "{\"ok\":true,\"command\":\"plan-main-toolbar\"" + << ",\"state\":{\"command\":\"" << json_escape(args.command) + << "\",\"undoCount\":" << args.undo_count + << ",\"redoCount\":" << args.redo_count + << ",\"memoryBytes\":" << args.memory_bytes + << ",\"hasCanvas\":" << json_bool(args.has_canvas) + << "},\"plan\":{\"command\":\"" << main_toolbar_command_name(value.command) + << "\",\"action\":\"" << main_toolbar_action_name(value.action) + << "\",\"label\":\"" << json_escape(value.label) + << "\",\"requiresCanvas\":" << json_bool(value.requires_canvas) + << ",\"updatesMemoryLabel\":" << json_bool(value.updates_memory_label) + << ",\"updatesTitle\":" << json_bool(value.updates_title) + << ",\"recordsUndo\":" << json_bool(value.records_undo) + << ",\"marksUnsaved\":" << json_bool(value.marks_unsaved) + << ",\"noOp\":" << json_bool(value.no_op) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_quick_operation_args( int argc, char** argv, @@ -7042,6 +7224,10 @@ int main(int argc, char** argv) return plan_history_operation(argc, argv); } + if (command == "plan-main-toolbar") { + return plan_main_toolbar(argc, argv); + } + if (command == "plan-quick-operation") { return plan_quick_operation(argc, argv); }