From b5bd6d42f731d534c540b17c3060c65792875080 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 12:27:47 +0200 Subject: [PATCH] Extract about menu action planning --- CMakeLists.txt | 1 + docs/modernization/debt.md | 3 +- docs/modernization/roadmap.md | 13 +++ src/app_core/about_menu.h | 86 +++++++++++++++ src/app_layout.cpp | 142 ++++++++++++++++-------- tests/CMakeLists.txt | 34 ++++++ tests/app_core/about_menu_tests.cpp | 81 ++++++++++++++ tools/pano_cli/main.cpp | 160 ++++++++++++++++++++++++++++ 8 files changed, 476 insertions(+), 44 deletions(-) create mode 100644 src/app_core/about_menu.h create mode 100644 tests/app_core/about_menu_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a2824d..4b230f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -223,6 +223,7 @@ target_link_libraries(pp_platform_api pp_project_warnings) add_library(pp_app_core STATIC + src/app_core/about_menu.h src/app_core/app_preferences.h src/app_core/app_status.h src/app_core/brush_ui.h diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index de798f2..3f65fa3 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, `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-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 contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/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_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-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, `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-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 | @@ -51,6 +51,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0031 | Open | Modernization | Top-level File menu command planning now consumes pure `pp_app_core` through `App::init_menu_file` and `pano_cli plan-file-menu`, but live execution still invokes legacy dialogs, platform pickers, cloud code, share code, and canvas import/export paths directly | Preserve File menu behavior while app workflows move toward app/document/platform command services | `pp_app_core_file_menu_tests`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-file-menu --command import`; `pano_cli plan-file-menu --command cloud-upload`; `ctest --preset desktop-fast --build-config Debug` | File menu routing, picker dispatch, save/share/cloud/resize/export execution, and image/project import execution are owned by app/document/platform services with `App::init_menu_file` acting only as a UI adapter | | 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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 4bc7bf6..7742e7f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -545,6 +545,11 @@ grid-clear, and platform-only SonarPen gating before legacy UI/panel/canvas execution continues. The live animation panel route now also checks animation panel visibility and applies animation panel layout state instead of using the grid panel by mistake. +`pano_cli plan-about-menu` exposes app-core planning for About menu help, +about, what's-new, crash-test, and performance-test commands, including +versioned what's-new labels, diagnostic gating, and no-canvas performance-test +blocking before legacy dialogs, platform crash hooks, and Canvas performance +strokes continue. `pp_platform_api` now owns a headless `PlatformServices` interface for startup storage path preparation, clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread lifecycle hooks, render-context @@ -1273,6 +1278,14 @@ Results: `pano_cli_plan_tools_panel_visible_noop_smoke`, and `pano_cli_plan_tools_panel_rejects_unknown` passed and expose live Tools menu/panel planning as JSON automation. +- `pp_app_core_about_menu_tests` passed, covering About/help/what's-new dialog + routing, versioned what's-new labels, crash diagnostic gating, performance + workload metadata, and no-canvas performance-test blocking. +- `pano_cli_plan_about_menu_news_smoke`, + `pano_cli_plan_about_menu_performance_no_canvas_smoke`, + `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_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/about_menu.h b/src/app_core/about_menu.h new file mode 100644 index 0000000..afe76c5 --- /dev/null +++ b/src/app_core/about_menu.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +namespace pp::app { + +enum class AboutMenuCommand { + help_guide, + about_app, + whats_new, + induce_crash, + performance_test, +}; + +enum class AboutMenuAction { + show_user_manual, + show_about_dialog, + show_whats_new_dialog, + trigger_crash_test, + run_performance_test, + no_op_unavailable, +}; + +struct AboutMenuPlan { + AboutMenuCommand command = AboutMenuCommand::help_guide; + AboutMenuAction action = AboutMenuAction::show_user_manual; + std::string label; + bool closes_root_popup = true; + bool requires_canvas = false; + int performance_iterations = 0; + int performance_updates_per_iteration = 0; +}; + +[[nodiscard]] inline AboutMenuPlan plan_about_menu_command( + AboutMenuCommand command, + int version_major, + int version_minor, + int version_fix, + bool diagnostics_available = true, + bool has_canvas = true) +{ + AboutMenuPlan plan; + plan.command = command; + + switch (command) { + case AboutMenuCommand::help_guide: + plan.action = AboutMenuAction::show_user_manual; + plan.label = "Help Guide"; + break; + case AboutMenuCommand::about_app: + plan.action = AboutMenuAction::show_about_dialog; + plan.label = "About PanoPainter"; + break; + case AboutMenuCommand::whats_new: + plan.action = AboutMenuAction::show_whats_new_dialog; + plan.label = "What's new in " + + std::to_string(version_major) + + "." + + std::to_string(version_minor) + + "." + + std::to_string(version_fix) + + "?"; + break; + case AboutMenuCommand::induce_crash: + plan.label = "Induce crash"; + plan.action = diagnostics_available + ? AboutMenuAction::trigger_crash_test + : AboutMenuAction::no_op_unavailable; + plan.closes_root_popup = diagnostics_available; + break; + case AboutMenuCommand::performance_test: + plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)"; + plan.requires_canvas = true; + plan.performance_iterations = 100; + plan.performance_updates_per_iteration = 10; + plan.action = has_canvas + ? AboutMenuAction::run_performance_test + : AboutMenuAction::no_op_unavailable; + plan.closes_root_popup = has_canvas; + break; + } + + return plan; +} + +} diff --git a/src/app_layout.cpp b/src/app_layout.cpp index c3d6af7..637cf71 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -6,6 +6,7 @@ #include "node_progress_bar.h" #include "node_dialog_picker.h" #include "node_panel_floating.h" +#include "app_core/about_menu.h" #include "app_core/app_preferences.h" #include "app_core/brush_ui.h" #include "app_core/canvas_tool_ui.h" @@ -1495,76 +1496,131 @@ void App::init_menu_about() layout[main_id]->add_child(popup); popup->find("about-app")->on_click = [this, popup](Node*) { - dialog_about(); - popup->mouse_release(); - popup->destroy(); + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::about_app, + g_version_major, + g_version_minor, + g_version_fix); + if (plan.action == pp::app::AboutMenuAction::show_about_dialog) + dialog_about(); + if (plan.closes_root_popup) + { + popup->mouse_release(); + popup->destroy(); + } }; popup->find("about-doc")->on_click = [this, popup](Node*) { // auto path = Asset::absolute("data/doc/test.pdf"); // display_file(path); - dialog_usermanual(); - popup->mouse_release(); - popup->destroy(); + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::help_guide, + g_version_major, + g_version_minor, + g_version_fix); + if (plan.action == pp::app::AboutMenuAction::show_user_manual) + dialog_usermanual(); + if (plan.closes_root_popup) + { + popup->mouse_release(); + popup->destroy(); + } }; if (auto item = popup->find("about-news")) { if (auto text = item->find("menu-label")) { - static char label[128]; - sprintf(label, "What's new in %d.%d.%d?", g_version_major, g_version_minor, g_version_fix); - text->set_text(label); + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::whats_new, + g_version_major, + g_version_minor, + g_version_fix); + text->set_text(plan.label.c_str()); } item->on_click = [this, popup](Node*) { - dialog_whatsnew(true); - popup->mouse_release(); - popup->destroy(); + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::whats_new, + g_version_major, + g_version_minor, + g_version_fix); + if (plan.action == pp::app::AboutMenuAction::show_whats_new_dialog) + dialog_whatsnew(true); + if (plan.closes_root_popup) + { + popup->mouse_release(); + popup->destroy(); + } }; } if (auto b = popup->find("about-crash")) { b->on_click = [this, popup](Node*) { - LOG("crashing"); - crash_test(); - popup->mouse_release(); - popup->destroy(); + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::induce_crash, + g_version_major, + g_version_minor, + g_version_fix); + if (plan.action == pp::app::AboutMenuAction::trigger_crash_test) + { + LOG("crashing"); + crash_test(); + } + if (plan.closes_root_popup) + { + popup->mouse_release(); + popup->destroy(); + } }; } if (auto b = popup->find("about-perf")) { b->on_click = [this, popup](Node*) { - LOG("perf"); - static char str[256]; - render_task([&] + const auto plan = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::performance_test, + g_version_major, + g_version_minor, + g_version_fix, + true, + Canvas::I != nullptr); + if (plan.action == pp::app::AboutMenuAction::run_performance_test) { - auto start = std::chrono::high_resolution_clock::now(); - Canvas::I->stroke_start({ 0, 0, 0 }, 0.9f); - for (int i = 0; i < 100; i++) + LOG("perf"); + std::string message; + const int performance_iterations = plan.performance_iterations; + render_task([&] { - Canvas::I->stroke_update({ 100, 100, 0 }, 0.9f); - Canvas::I->stroke_update({ 200, 200, 0 }, 0.9f); - Canvas::I->stroke_update({ 200, 100, 0 }, 0.9f); - Canvas::I->stroke_update({ 100, 200, 0 }, 0.9f); - Canvas::I->stroke_update({ 300, 300, 0 }, 0.9f); - Canvas::I->stroke_update({ 200, 500, 0 }, 0.9f); - Canvas::I->stroke_update({ 500, 500, 0 }, 0.9f); - Canvas::I->stroke_update({ 400, 400, 0 }, 0.9f); - Canvas::I->stroke_update({ 0, 200, 0 }, 0.9f); - Canvas::I->stroke_update({ 200, 0, 0 }, 0.9f); - Canvas::I->stroke_draw(); - } - Canvas::I->stroke_end(); - auto diff = std::chrono::high_resolution_clock::now() - start; - auto ms = std::chrono::duration_cast(diff).count(); - LOG("%lld ms", ms); - sprintf(str, "Time %lld ms", ms); - }); - message_box("Performance test", str); - popup->mouse_release(); - popup->destroy(); + auto start = std::chrono::high_resolution_clock::now(); + Canvas::I->stroke_start({ 0, 0, 0 }, 0.9f); + for (int i = 0; i < performance_iterations; i++) + { + Canvas::I->stroke_update({ 100, 100, 0 }, 0.9f); + Canvas::I->stroke_update({ 200, 200, 0 }, 0.9f); + Canvas::I->stroke_update({ 200, 100, 0 }, 0.9f); + Canvas::I->stroke_update({ 100, 200, 0 }, 0.9f); + Canvas::I->stroke_update({ 300, 300, 0 }, 0.9f); + Canvas::I->stroke_update({ 200, 500, 0 }, 0.9f); + Canvas::I->stroke_update({ 500, 500, 0 }, 0.9f); + Canvas::I->stroke_update({ 400, 400, 0 }, 0.9f); + Canvas::I->stroke_update({ 0, 200, 0 }, 0.9f); + Canvas::I->stroke_update({ 200, 0, 0 }, 0.9f); + Canvas::I->stroke_draw(); + } + Canvas::I->stroke_end(); + auto diff = std::chrono::high_resolution_clock::now() - start; + auto ms = std::chrono::duration_cast(diff).count(); + LOG("%lld ms", ms); + message = "Time " + std::to_string(ms) + " ms"; + }); + message_box("Performance test", message); + } + if (plan.closes_root_popup) + { + popup->mouse_release(); + popup->destroy(); + } }; } }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9e13b50..d1c95c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -268,6 +268,16 @@ add_test(NAME pp_ui_core_layout_xml_tests COMMAND pp_ui_core_layout_xml_tests) set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES LABELS "ui;desktop-fast;fuzz") +add_executable(pp_app_core_about_menu_tests + app_core/about_menu_tests.cpp) +target_link_libraries(pp_app_core_about_menu_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_about_menu_tests COMMAND pp_app_core_about_menu_tests) +set_tests_properties(pp_app_core_about_menu_tests PROPERTIES + LABELS "app;ui;desktop-fast;fuzz") + add_executable(pp_app_core_brush_ui_tests app_core/brush_ui_tests.cpp) target_link_libraries(pp_app_core_brush_ui_tests PRIVATE @@ -851,6 +861,30 @@ if(TARGET pano_cli) LABELS "app;ui;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_about_menu_news_smoke + COMMAND pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7) + set_tests_properties(pano_cli_plan_about_menu_news_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-about-menu\".*\"command\":\"news\".*\"versionMajor\":2.*\"action\":\"show-whats-new-dialog\".*\"label\":\"What's new in 2.5.7\\?\".*\"closesRootPopup\":true") + + add_test(NAME pano_cli_plan_about_menu_performance_no_canvas_smoke + COMMAND pano_cli plan-about-menu --command performance --no-canvas) + set_tests_properties(pano_cli_plan_about_menu_performance_no_canvas_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-about-menu\".*\"hasCanvas\":false.*\"action\":\"no-op-unavailable\".*\"label\":\"Performance test \\(No canvas\\)\".*\"closesRootPopup\":false.*\"requiresCanvas\":true") + + add_test(NAME pano_cli_plan_about_menu_crash_disabled_smoke + COMMAND pano_cli plan-about-menu --command crash --no-diagnostics) + set_tests_properties(pano_cli_plan_about_menu_crash_disabled_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-about-menu\".*\"diagnosticsAvailable\":false.*\"action\":\"no-op-unavailable\".*\"closesRootPopup\":false") + + add_test(NAME pano_cli_plan_about_menu_rejects_unknown + COMMAND pano_cli plan-about-menu --command missing) + set_tests_properties(pano_cli_plan_about_menu_rejects_unknown PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_app_status_smoke COMMAND pano_cli plan-app-status --doc-name demo diff --git a/tests/app_core/about_menu_tests.cpp b/tests/app_core/about_menu_tests.cpp new file mode 100644 index 0000000..583ab11 --- /dev/null +++ b/tests/app_core/about_menu_tests.cpp @@ -0,0 +1,81 @@ +#include "app_core/about_menu.h" +#include "test_harness.h" + +namespace { + +void about_menu_maps_user_facing_dialog_actions(pp::tests::Harness& harness) +{ + const auto help = pp::app::plan_about_menu_command(pp::app::AboutMenuCommand::help_guide, 1, 2, 3); + const auto about = pp::app::plan_about_menu_command(pp::app::AboutMenuCommand::about_app, 1, 2, 3); + const auto news = pp::app::plan_about_menu_command(pp::app::AboutMenuCommand::whats_new, 1, 2, 3); + + PP_EXPECT(harness, help.action == pp::app::AboutMenuAction::show_user_manual); + PP_EXPECT(harness, help.label == "Help Guide"); + PP_EXPECT(harness, help.closes_root_popup); + PP_EXPECT(harness, about.action == pp::app::AboutMenuAction::show_about_dialog); + PP_EXPECT(harness, about.label == "About PanoPainter"); + PP_EXPECT(harness, news.action == pp::app::AboutMenuAction::show_whats_new_dialog); + PP_EXPECT(harness, news.label == "What's new in 1.2.3?"); +} + +void about_menu_gates_diagnostic_crash_action(pp::tests::Harness& harness) +{ + const auto enabled = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::induce_crash, + 1, + 2, + 3, + true); + const auto disabled = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::induce_crash, + 1, + 2, + 3, + false); + + PP_EXPECT(harness, enabled.action == pp::app::AboutMenuAction::trigger_crash_test); + PP_EXPECT(harness, enabled.closes_root_popup); + PP_EXPECT(harness, disabled.action == pp::app::AboutMenuAction::no_op_unavailable); + PP_EXPECT(harness, !disabled.closes_root_popup); +} + +void about_menu_performance_test_declares_workload_and_canvas_requirement(pp::tests::Harness& harness) +{ + const auto available = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::performance_test, + 1, + 2, + 3, + true, + true); + const auto no_canvas = pp::app::plan_about_menu_command( + pp::app::AboutMenuCommand::performance_test, + 1, + 2, + 3, + true, + false); + + PP_EXPECT(harness, available.action == pp::app::AboutMenuAction::run_performance_test); + PP_EXPECT(harness, available.requires_canvas); + PP_EXPECT(harness, available.performance_iterations == 100); + PP_EXPECT(harness, available.performance_updates_per_iteration == 10); + PP_EXPECT(harness, available.closes_root_popup); + PP_EXPECT(harness, no_canvas.action == pp::app::AboutMenuAction::no_op_unavailable); + PP_EXPECT(harness, no_canvas.label == "Performance test (No canvas)"); + PP_EXPECT(harness, no_canvas.requires_canvas); + PP_EXPECT(harness, !no_canvas.closes_root_popup); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("about menu maps user facing dialog actions", about_menu_maps_user_facing_dialog_actions); + harness.run("about menu gates diagnostic crash action", about_menu_gates_diagnostic_crash_action); + harness.run( + "about menu performance test declares workload and canvas requirement", + about_menu_performance_test_declares_workload_and_canvas_requirement); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index c0d7530..fcfffd9 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,3 +1,4 @@ +#include "app_core/about_menu.h" #include "app_core/app_preferences.h" #include "app_core/app_status.h" #include "app_core/brush_ui.h" @@ -352,6 +353,15 @@ struct PlanToolsPanelArgs { bool already_visible = false; }; +struct PlanAboutMenuArgs { + std::string command = "news"; + int version_major = 1; + int version_minor = 0; + int version_fix = 0; + bool diagnostics_available = true; + bool has_canvas = true; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -737,6 +747,44 @@ const char* tools_panel_action_name(pp::app::ToolsPanelAction action) noexcept return "no-op-already-visible"; } +const char* about_menu_command_name(pp::app::AboutMenuCommand command) noexcept +{ + switch (command) { + case pp::app::AboutMenuCommand::help_guide: + return "help"; + case pp::app::AboutMenuCommand::about_app: + return "about"; + case pp::app::AboutMenuCommand::whats_new: + return "news"; + case pp::app::AboutMenuCommand::induce_crash: + return "crash"; + case pp::app::AboutMenuCommand::performance_test: + return "performance"; + } + + return "help"; +} + +const char* about_menu_action_name(pp::app::AboutMenuAction action) noexcept +{ + switch (action) { + case pp::app::AboutMenuAction::show_user_manual: + return "show-user-manual"; + case pp::app::AboutMenuAction::show_about_dialog: + return "show-about-dialog"; + case pp::app::AboutMenuAction::show_whats_new_dialog: + return "show-whats-new-dialog"; + case pp::app::AboutMenuAction::trigger_crash_test: + return "trigger-crash-test"; + case pp::app::AboutMenuAction::run_performance_test: + return "run-performance-test"; + case pp::app::AboutMenuAction::no_op_unavailable: + return "no-op-unavailable"; + } + + return "no-op-unavailable"; +} + pp::foundation::Result parse_tools_menu_command(std::string_view command) { if (command == "panels") { @@ -790,6 +838,33 @@ pp::foundation::Result parse_tools_panel(std::string_view p pp::foundation::Status::invalid_argument("unknown tools panel")); } +pp::foundation::Result parse_about_menu_command(std::string_view command) +{ + if (command == "help" || command == "help-guide" || command == "manual") { + return pp::foundation::Result::success( + pp::app::AboutMenuCommand::help_guide); + } + if (command == "about" || command == "about-app") { + return pp::foundation::Result::success( + pp::app::AboutMenuCommand::about_app); + } + if (command == "news" || command == "whats-new") { + return pp::foundation::Result::success( + pp::app::AboutMenuCommand::whats_new); + } + if (command == "crash" || command == "induce-crash") { + return pp::foundation::Result::success( + pp::app::AboutMenuCommand::induce_crash); + } + if (command == "performance" || command == "perf") { + return pp::foundation::Result::success( + pp::app::AboutMenuCommand::performance_test); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown about menu command")); +} + const char* document_layer_rename_action_name(pp::app::DocumentLayerRenameAction action) noexcept { switch (action) { @@ -1406,6 +1481,7 @@ void print_help() << " plan-app-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N]\n" << " plan-tools-menu --command panels|options|clear-grids|reset-camera|shortcuts|sonarpen [--sonarpen-available]\n" << " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n" + << " plan-about-menu --command help|about|news|crash|performance [--version-major N] [--version-minor N] [--version-fix N] [--no-diagnostics] [--no-canvas]\n" << " plan-canvas-clear [--no-canvas] [--r N] [--g N] [--b N] [--a N]\n" << " plan-image-import --width N --height N\n" << " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n" @@ -3063,6 +3139,86 @@ int plan_tools_panel(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_about_menu_args( + int argc, + char** argv, + PlanAboutMenuArgs& 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 == "--version-major" || key == "--version-minor" || key == "--version-fix") { + 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 == "--version-major") { + args.version_major = value.value(); + } else if (key == "--version-minor") { + args.version_minor = value.value(); + } else { + args.version_fix = value.value(); + } + } else if (key == "--no-diagnostics") { + args.diagnostics_available = false; + } 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_about_menu(int argc, char** argv) +{ + PlanAboutMenuArgs args; + const auto status = parse_plan_about_menu_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-about-menu", status.message); + return 2; + } + + const auto command = parse_about_menu_command(args.command); + if (!command) { + print_error("plan-about-menu", command.status().message); + return 2; + } + + const auto plan = pp::app::plan_about_menu_command( + command.value(), + args.version_major, + args.version_minor, + args.version_fix, + args.diagnostics_available, + args.has_canvas); + + std::cout << "{\"ok\":true,\"command\":\"plan-about-menu\"" + << ",\"state\":{\"command\":\"" << json_escape(args.command) + << "\",\"versionMajor\":" << args.version_major + << ",\"versionMinor\":" << args.version_minor + << ",\"versionFix\":" << args.version_fix + << ",\"diagnosticsAvailable\":" << json_bool(args.diagnostics_available) + << ",\"hasCanvas\":" << json_bool(args.has_canvas) + << "},\"plan\":{\"command\":\"" << about_menu_command_name(plan.command) + << "\",\"action\":\"" << about_menu_action_name(plan.action) + << "\",\"label\":\"" << json_escape(plan.label) + << "\",\"closesRootPopup\":" << json_bool(plan.closes_root_popup) + << ",\"requiresCanvas\":" << json_bool(plan.requires_canvas) + << ",\"performanceIterations\":" << plan.performance_iterations + << ",\"performanceUpdatesPerIteration\":" << plan.performance_updates_per_iteration + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_app_status_args( int argc, char** argv, @@ -6834,6 +6990,10 @@ int main(int argc, char** argv) return plan_tools_panel(argc, argv); } + if (command == "plan-about-menu") { + return plan_about_menu(argc, argv); + } + if (command == "plan-document-resize") { return plan_document_resize(argc, argv); }