diff --git a/CMakeLists.txt b/CMakeLists.txt index 157ac95..4a2824d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,7 +242,8 @@ 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/quick_ui.h) + src/app_core/quick_ui.h + src/app_core/tools_menu.h) target_include_directories(pp_app_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index ae1ce85..de798f2 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, `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-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 contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI 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_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-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, `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-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 | @@ -50,6 +50,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0030 | Open | Modernization | File export menu action planning now consumes pure `pp_app_core` through the File menu and `pano_cli plan-export-menu`, but live execution still opens legacy export dialogs and then reaches legacy canvas/render/video export code | Preserve current export menu behavior while export command execution moves toward document/app/renderer/video services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind png`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-menu --kind layers --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Export menu routing, license gating, target creation, image/layer/cube/depth/animation/timelapse execution, and error reporting are owned by document/app services with File-menu callbacks acting only as adapters | | 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 | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0cfe2c1..4bc7bf6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -538,6 +538,13 @@ legacy `ActionManager` stack execution continues. slot selection versus popup opening, plus quick mini-state restore/reset validation used by the live quick panel before legacy `Brush`, color picker, stroke preview, and preset popup execution continue. +`pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core +planning for top-level Tools commands and floating-panel requests, including +already-visible no-ops, panel chrome metadata, shortcuts, camera reset, +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. `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 @@ -1256,6 +1263,16 @@ Results: `pano_cli_plan_quick_operation_rejects_bad_slot`, and `pano_cli_plan_quick_operation_rejects_bad_restore` passed and expose live quick-panel planning as JSON automation. +- `pp_app_core_tools_menu_tests` passed, covering Tools submenu routing, + root-closing commands, platform-only SonarPen gating, floating panel chrome + metadata, already-visible panel no-ops, and animation panel non-droppable + state. +- `pano_cli_plan_tools_menu_shortcuts_smoke`, + `pano_cli_plan_tools_menu_sonarpen_unavailable_smoke`, + `pano_cli_plan_tools_panel_layers_smoke`, + `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_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/tools_menu.h b/src/app_core/tools_menu.h new file mode 100644 index 0000000..0bc8dd9 --- /dev/null +++ b/src/app_core/tools_menu.h @@ -0,0 +1,141 @@ +#pragma once + +#include + +namespace pp::app { + +enum class ToolsMenuCommand { + panels, + options, + clear_grids, + reset_camera, + shortcuts, + sonarpen, +}; + +enum class ToolsMenuAction { + show_panels_submenu, + show_options_submenu, + clear_grid_overlays, + reset_camera, + show_shortcuts_dialog, + start_sonarpen, + no_op_unavailable, +}; + +enum class ToolsPanel { + presets, + color, + color_advanced, + layers, + brush, + grids, + animation, +}; + +enum class ToolsPanelAction { + open_floating_panel, + no_op_already_visible, +}; + +struct ToolsMenuPlan { + ToolsMenuCommand command = ToolsMenuCommand::panels; + ToolsMenuAction action = ToolsMenuAction::show_panels_submenu; + std::string_view label; + bool closes_root_popup = false; +}; + +struct ToolsPanelPlan { + ToolsPanel panel = ToolsPanel::presets; + ToolsPanelAction action = ToolsPanelAction::open_floating_panel; + std::string_view title; + int width = 0; + int height = 0; + int min_width = 0; + int min_height = 0; + bool droppable = true; + bool hides_embedded_title = false; +}; + +[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command( + ToolsMenuCommand command, + bool sonarpen_available = false) noexcept +{ + switch (command) { + case ToolsMenuCommand::panels: + return { command, ToolsMenuAction::show_panels_submenu, "Panels", false }; + case ToolsMenuCommand::options: + return { command, ToolsMenuAction::show_options_submenu, "Options", false }; + case ToolsMenuCommand::clear_grids: + return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true }; + case ToolsMenuCommand::reset_camera: + return { command, ToolsMenuAction::reset_camera, "Reset Camera", true }; + case ToolsMenuCommand::shortcuts: + return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true }; + case ToolsMenuCommand::sonarpen: + return { + command, + sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable, + "SonarPen", + sonarpen_available, + }; + } + + return { command, ToolsMenuAction::no_op_unavailable, "", false }; +} + +[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel( + ToolsPanel panel, + bool already_visible) noexcept +{ + ToolsPanelPlan plan; + plan.panel = panel; + plan.action = already_visible + ? ToolsPanelAction::no_op_already_visible + : ToolsPanelAction::open_floating_panel; + + switch (panel) { + case ToolsPanel::presets: + plan.title = "Brushes"; + plan.height = 300; + plan.min_height = 300; + plan.min_width = 100; + break; + case ToolsPanel::color: + plan.title = "Color Picker"; + plan.height = 300; + plan.hides_embedded_title = true; + break; + case ToolsPanel::color_advanced: + plan.title = "Color Picker"; + plan.width = 300; + plan.height = 300; + break; + case ToolsPanel::layers: + plan.title = "Layers"; + plan.height = 300; + plan.min_height = 100; + plan.hides_embedded_title = true; + break; + case ToolsPanel::brush: + plan.title = "Brush Settings"; + plan.height = 300; + plan.hides_embedded_title = true; + break; + case ToolsPanel::grids: + plan.title = "Grid"; + plan.height = 300; + plan.hides_embedded_title = true; + break; + case ToolsPanel::animation: + plan.title = "Animation"; + plan.width = 500; + plan.height = 300; + plan.droppable = false; + break; + } + + return plan; +} + +} diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 0c21baf..c3d6af7 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -16,6 +16,7 @@ #include "app_core/file_menu.h" #include "app_core/app_status.h" #include "app_core/history_ui.h" +#include "app_core/tools_menu.h" #include "settings.h" #include "serializer.h" #include "font.h" @@ -223,6 +224,29 @@ pp::app::DocumentLayerMenuPlan make_layer_menu_plan( return {}; } +[[nodiscard]] bool should_open_tools_panel(const pp::app::ToolsPanelPlan& plan) noexcept +{ + return plan.action == pp::app::ToolsPanelAction::open_floating_panel; +} + +void apply_tools_panel_chrome(NodePanelFloating& panel, const pp::app::ToolsPanelPlan& plan) +{ + if (plan.width > 0 && plan.height > 0) { + panel.SetSize(static_cast(plan.width), static_cast(plan.height)); + } else { + if (plan.width > 0) + panel.SetWidth(static_cast(plan.width)); + if (plan.height > 0) + panel.SetHeight(static_cast(plan.height)); + } + if (plan.min_width > 0) + panel.SetMinWidth(static_cast(plan.min_width)); + if (plan.min_height > 0) + panel.SetMinHeight(static_cast(plan.min_height)); + panel.m_title->set_text(plan.title.data()); + panel.m_droppable = plan.droppable; +} + } // namespace void App::title_update() @@ -1052,6 +1076,10 @@ void App::init_menu_tools() if (auto tick = popup_exp->find("tools-panels")) tick->on_click = [this, popup_exp](Node* b) { + const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels); + if (menu_plan.action != pp::app::ToolsMenuAction::show_panels_submenu) + return; + glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); auto popup_time = layout[const_hash("panels-menu")]->m_children[0]->clone(); popup_time->update(); @@ -1076,14 +1104,14 @@ void App::init_menu_tools() }; popup_time->find("panel-presets")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(floating_presets.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::presets, + visible(floating_presets.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Presets; - fpanel->SetHeight(300); - fpanel->SetMinHeight(300); - fpanel->SetMinWidth(100); - fpanel->m_title->set_text("Brushes"); + apply_tools_panel_chrome(*fpanel, plan); if (!floating_presets) { floating_presets = fpanel->m_container->add_child_ref(); @@ -1103,19 +1131,21 @@ void App::init_menu_tools() }; popup_time->find("panel-color")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(floating_color.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::color, + visible(floating_color.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Color; - fpanel->SetHeight(300); - fpanel->SetMinHeight(300); - fpanel->m_title->set_text("Color Picker"); + apply_tools_panel_chrome(*fpanel, plan); if (!floating_color) { floating_color = fpanel->m_container->add_child_ref(); floating_color->SetHeightP(100); //floating_color->SetMinHeight(300); - floating_color->find("title")->SetVisibility(false); + if (plan.hides_embedded_title) + floating_color->find("title")->SetVisibility(false); floating_color->on_color_changed = [this](Node* target, glm::vec4 color) { apply_brush_color_plan(*this, color, false, false); }; @@ -1128,13 +1158,14 @@ void App::init_menu_tools() popup_time->destroy(); }; popup_time->find("panel-color-adv")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(floating_picker.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::color_advanced, + visible(floating_picker.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::ColorAdv; - fpanel->SetHeight(300); - fpanel->SetWidth(300); - fpanel->m_title->set_text("Color Picker"); + apply_tools_panel_chrome(*fpanel, plan); if (!floating_picker) { floating_picker = fpanel->m_container->add_child_ref(); @@ -1154,72 +1185,80 @@ void App::init_menu_tools() popup_time->destroy(); }; popup_time->find("panel-layers")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(layers.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::layers, + visible(layers.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Layers; - fpanel->SetMinHeight(100); - fpanel->SetHeight(300); + apply_tools_panel_chrome(*fpanel, plan); fpanel->m_container->add_child(layers); - fpanel->m_title->set_text("Layers"); layers->SetPositioning(YGPositionTypeRelative); layers->SetPosition(0, 0); layers->SetWidthP(100); layers->SetHeightP(100); layers->SetFlexShrink(0); - layers->find("title")->SetVisibility(false); + if (plan.hides_embedded_title) + layers->find("title")->SetVisibility(false); popup_exp->destroy(); popup_time->destroy(); }; popup_time->find("panel-brush")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(stroke.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::brush, + visible(stroke.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Brush; fpanel->m_container->add_child(stroke); - fpanel->SetHeight(300); - fpanel->m_title->set_text("Brush Settings"); + apply_tools_panel_chrome(*fpanel, plan); stroke->SetPositioning(YGPositionTypeRelative); stroke->SetPosition(0, 0); stroke->SetWidthP(100); stroke->SetHeightP(100); - stroke->find("title")->SetVisibility(false); + if (plan.hides_embedded_title) + stroke->find("title")->SetVisibility(false); popup_exp->destroy(); popup_time->destroy(); }; popup_time->find("panel-grids")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(grid.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::grids, + visible(grid.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Grids; fpanel->m_container->add_child(grid); - fpanel->SetHeight(300); - fpanel->m_title->set_text("Grid"); + apply_tools_panel_chrome(*fpanel, plan); grid->SetPositioning(YGPositionTypeRelative); grid->SetPosition(0, 0); grid->SetWidthP(100); grid->SetHeightP(100); - grid->find("title")->SetVisibility(false); + if (plan.hides_embedded_title) + grid->find("title")->SetVisibility(false); popup_exp->destroy(); popup_time->destroy(); }; popup_time->find("panel-animation")->on_click = [this, popup_time, popup_exp, visible](Node*) { - if (visible(grid.get())) + const auto plan = pp::app::plan_tools_panel( + pp::app::ToolsPanel::animation, + visible(animation.get())); + if (!should_open_tools_panel(plan)) return; auto fpanel = floatings_container->add_child(); fpanel->m_class = NodePanelFloating::kClass::Animation; fpanel->m_container->add_child(animation); - fpanel->SetSize(500, 300); - fpanel->m_title->set_text("Animation"); - fpanel->m_droppable = false; - grid->SetPositioning(YGPositionTypeRelative); - grid->SetPosition(0, 0); - grid->SetWidthP(100); - grid->SetHeightP(100); - grid->find("title")->SetVisibility(false); + apply_tools_panel_chrome(*fpanel, plan); + animation->SetPositioning(YGPositionTypeRelative); + animation->SetPosition(0, 0); + animation->SetWidthP(100); + animation->SetHeightP(100); popup_exp->destroy(); popup_time->destroy(); @@ -1228,6 +1267,10 @@ void App::init_menu_tools() if (auto options = popup_exp->find("tools-options")) options->on_click = [this, options, main](Node* b) { + const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options); + if (menu_plan.action != pp::app::ToolsMenuAction::show_options_submenu) + return; + glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0); auto popup_time = layout[const_hash("options-menu")]->m_children[0]->clone(); popup_time->update(); @@ -1378,22 +1421,39 @@ void App::init_menu_tools() }; popup_exp->find("clear-grids")->on_click = [this, popup_exp](Node*) { - CanvasModeGrid* mode = (CanvasModeGrid*)Canvas::modes[(int)kCanvasMode::Grid][0]; - mode->clear(); - popup_exp->mouse_release(); - popup_exp->destroy(); + const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids); + if (plan.action == pp::app::ToolsMenuAction::clear_grid_overlays) + { + CanvasModeGrid* mode = (CanvasModeGrid*)Canvas::modes[(int)kCanvasMode::Grid][0]; + mode->clear(); + } + if (plan.closes_root_popup) + { + popup_exp->mouse_release(); + popup_exp->destroy(); + } }; popup_exp->find("camera-reset")->on_click = [this, popup_exp](Node*) { - canvas->reset_camera(); - popup_exp->mouse_release(); - popup_exp->destroy(); + const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera); + if (plan.action == pp::app::ToolsMenuAction::reset_camera) + canvas->reset_camera(); + if (plan.closes_root_popup) + { + popup_exp->mouse_release(); + popup_exp->destroy(); + } }; popup_exp->find("shortcuts")->on_click = [this, popup_exp](Node*) { - dialog_shortcuts(); - popup_exp->mouse_release(); - popup_exp->destroy(); + const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts); + if (plan.action == pp::app::ToolsMenuAction::show_shortcuts_dialog) + dialog_shortcuts(); + if (plan.closes_root_popup) + { + popup_exp->mouse_release(); + popup_exp->destroy(); + } }; /* @@ -1406,9 +1466,14 @@ void App::init_menu_tools() #if __IOS__ popup_exp->find("sonarpen")->on_click = [this, popup_exp](Node*) { - [ios_app sonarpen_start]; - popup_exp->mouse_release(); - popup_exp->destroy(); + const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true); + if (plan.action == pp::app::ToolsMenuAction::start_sonarpen) + [ios_app sonarpen_start]; + if (plan.closes_root_popup) + { + popup_exp->mouse_release(); + popup_exp->destroy(); + } }; #endif }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ab8c58e..9e13b50 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -318,6 +318,16 @@ add_test(NAME pp_app_core_quick_ui_tests COMMAND pp_app_core_quick_ui_tests) set_tests_properties(pp_app_core_quick_ui_tests PROPERTIES LABELS "app;ui;paint;desktop-fast;fuzz") +add_executable(pp_app_core_tools_menu_tests + app_core/tools_menu_tests.cpp) +target_link_libraries(pp_app_core_tools_menu_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_tools_menu_tests COMMAND pp_app_core_tools_menu_tests) +set_tests_properties(pp_app_core_tools_menu_tests PROPERTIES + LABELS "app;ui;desktop-fast;fuzz") + add_executable(pp_app_core_document_route_tests app_core/document_route_tests.cpp) target_link_libraries(pp_app_core_document_route_tests PRIVATE @@ -811,6 +821,36 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-preferences\".*\"scaleSelection\":\\{\"hasSelection\":false,\"index\":0\\}.*\"direction\":\"left-to-right\".*\"timelapse\":\\{\"enabled\":false,\"recordingAction\":\"stop-recording\"\\}.*\"vrControllers\":\\{\"enabled\":false\\}") + add_test(NAME pano_cli_plan_tools_menu_shortcuts_smoke + COMMAND pano_cli plan-tools-menu --command shortcuts) + set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-menu\".*\"command\":\"shortcuts\".*\"action\":\"show-shortcuts-dialog\".*\"closesRootPopup\":true") + + add_test(NAME pano_cli_plan_tools_menu_sonarpen_unavailable_smoke + COMMAND pano_cli plan-tools-menu --command sonarpen) + set_tests_properties(pano_cli_plan_tools_menu_sonarpen_unavailable_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-menu\".*\"sonarpenAvailable\":false.*\"action\":\"no-op-unavailable\".*\"closesRootPopup\":false") + + add_test(NAME pano_cli_plan_tools_panel_layers_smoke + COMMAND pano_cli plan-tools-panel --panel layers) + set_tests_properties(pano_cli_plan_tools_panel_layers_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-panel\".*\"panel\":\"layers\".*\"action\":\"open-floating-panel\".*\"title\":\"Layers\".*\"height\":300.*\"minHeight\":100.*\"hidesEmbeddedTitle\":true") + + add_test(NAME pano_cli_plan_tools_panel_visible_noop_smoke + COMMAND pano_cli plan-tools-panel --panel animation --already-visible) + set_tests_properties(pano_cli_plan_tools_panel_visible_noop_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-panel\".*\"alreadyVisible\":true.*\"action\":\"no-op-already-visible\".*\"title\":\"Animation\".*\"droppable\":false") + + add_test(NAME pano_cli_plan_tools_panel_rejects_unknown + COMMAND pano_cli plan-tools-panel --panel missing) + set_tests_properties(pano_cli_plan_tools_panel_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/tools_menu_tests.cpp b/tests/app_core/tools_menu_tests.cpp new file mode 100644 index 0000000..651e38d --- /dev/null +++ b/tests/app_core/tools_menu_tests.cpp @@ -0,0 +1,80 @@ +#include "app_core/tools_menu.h" +#include "test_harness.h" + +namespace { + +void tools_menu_maps_submenus_and_commands(pp::tests::Harness& harness) +{ + const auto panels = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels); + const auto options = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options); + const auto clear = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids); + const auto camera = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera); + const auto shortcuts = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts); + + PP_EXPECT(harness, panels.action == pp::app::ToolsMenuAction::show_panels_submenu); + PP_EXPECT(harness, !panels.closes_root_popup); + PP_EXPECT(harness, options.action == pp::app::ToolsMenuAction::show_options_submenu); + PP_EXPECT(harness, clear.action == pp::app::ToolsMenuAction::clear_grid_overlays); + PP_EXPECT(harness, clear.closes_root_popup); + PP_EXPECT(harness, camera.action == pp::app::ToolsMenuAction::reset_camera); + PP_EXPECT(harness, camera.closes_root_popup); + PP_EXPECT(harness, shortcuts.action == pp::app::ToolsMenuAction::show_shortcuts_dialog); + PP_EXPECT(harness, shortcuts.closes_root_popup); +} + +void tools_menu_gates_platform_only_actions(pp::tests::Harness& harness) +{ + const auto unavailable = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen); + const auto available = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true); + + PP_EXPECT(harness, unavailable.action == pp::app::ToolsMenuAction::no_op_unavailable); + PP_EXPECT(harness, !unavailable.closes_root_popup); + PP_EXPECT(harness, available.action == pp::app::ToolsMenuAction::start_sonarpen); + PP_EXPECT(harness, available.closes_root_popup); +} + +void tools_panel_plans_floating_panel_metadata(pp::tests::Harness& harness) +{ + const auto presets = pp::app::plan_tools_panel(pp::app::ToolsPanel::presets, false); + const auto color = pp::app::plan_tools_panel(pp::app::ToolsPanel::color, false); + const auto layers = pp::app::plan_tools_panel(pp::app::ToolsPanel::layers, false); + const auto animation = pp::app::plan_tools_panel(pp::app::ToolsPanel::animation, false); + + PP_EXPECT(harness, presets.action == pp::app::ToolsPanelAction::open_floating_panel); + PP_EXPECT(harness, presets.title == "Brushes"); + PP_EXPECT(harness, presets.height == 300); + PP_EXPECT(harness, presets.min_height == 300); + PP_EXPECT(harness, presets.min_width == 100); + PP_EXPECT(harness, color.title == "Color Picker"); + PP_EXPECT(harness, color.hides_embedded_title); + PP_EXPECT(harness, layers.title == "Layers"); + PP_EXPECT(harness, layers.min_height == 100); + PP_EXPECT(harness, layers.hides_embedded_title); + PP_EXPECT(harness, animation.title == "Animation"); + PP_EXPECT(harness, animation.width == 500); + PP_EXPECT(harness, animation.height == 300); + PP_EXPECT(harness, !animation.droppable); +} + +void tools_panel_no_ops_when_panel_is_already_visible(pp::tests::Harness& harness) +{ + const auto visible = pp::app::plan_tools_panel(pp::app::ToolsPanel::brush, true); + const auto hidden = pp::app::plan_tools_panel(pp::app::ToolsPanel::brush, false); + + PP_EXPECT(harness, visible.action == pp::app::ToolsPanelAction::no_op_already_visible); + PP_EXPECT(harness, hidden.action == pp::app::ToolsPanelAction::open_floating_panel); + PP_EXPECT(harness, visible.title == hidden.title); + PP_EXPECT(harness, visible.hides_embedded_title == hidden.hides_embedded_title); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("tools menu maps submenus and commands", tools_menu_maps_submenus_and_commands); + harness.run("tools menu gates platform only actions", tools_menu_gates_platform_only_actions); + harness.run("tools panel plans floating panel metadata", tools_panel_plans_floating_panel_metadata); + harness.run("tools panel no ops when panel is already visible", tools_panel_no_ops_when_panel_is_already_visible); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 274927d..c0d7530 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -18,6 +18,7 @@ #include "app_core/grid_ui.h" #include "app_core/history_ui.h" #include "app_core/quick_ui.h" +#include "app_core/tools_menu.h" #include "assets/image_format.h" #include "assets/image_metadata.h" #include "assets/image_pixels.h" @@ -341,6 +342,16 @@ struct PlanQuickOperationArgs { bool fire_event = false; }; +struct PlanToolsMenuArgs { + std::string command = "shortcuts"; + bool sonarpen_available = false; +}; + +struct PlanToolsPanelArgs { + std::string panel = "layers"; + bool already_visible = false; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -650,6 +661,135 @@ const char* file_menu_action_name(pp::app::FileMenuAction action) noexcept return "show-new-document-dialog"; } +const char* tools_menu_command_name(pp::app::ToolsMenuCommand command) noexcept +{ + switch (command) { + case pp::app::ToolsMenuCommand::panels: + return "panels"; + case pp::app::ToolsMenuCommand::options: + return "options"; + case pp::app::ToolsMenuCommand::clear_grids: + return "clear-grids"; + case pp::app::ToolsMenuCommand::reset_camera: + return "reset-camera"; + case pp::app::ToolsMenuCommand::shortcuts: + return "shortcuts"; + case pp::app::ToolsMenuCommand::sonarpen: + return "sonarpen"; + } + + return "shortcuts"; +} + +const char* tools_menu_action_name(pp::app::ToolsMenuAction action) noexcept +{ + switch (action) { + case pp::app::ToolsMenuAction::show_panels_submenu: + return "show-panels-submenu"; + case pp::app::ToolsMenuAction::show_options_submenu: + return "show-options-submenu"; + case pp::app::ToolsMenuAction::clear_grid_overlays: + return "clear-grid-overlays"; + case pp::app::ToolsMenuAction::reset_camera: + return "reset-camera"; + case pp::app::ToolsMenuAction::show_shortcuts_dialog: + return "show-shortcuts-dialog"; + case pp::app::ToolsMenuAction::start_sonarpen: + return "start-sonarpen"; + case pp::app::ToolsMenuAction::no_op_unavailable: + return "no-op-unavailable"; + } + + return "no-op-unavailable"; +} + +const char* tools_panel_name(pp::app::ToolsPanel panel) noexcept +{ + switch (panel) { + case pp::app::ToolsPanel::presets: + return "presets"; + case pp::app::ToolsPanel::color: + return "color"; + case pp::app::ToolsPanel::color_advanced: + return "color-advanced"; + case pp::app::ToolsPanel::layers: + return "layers"; + case pp::app::ToolsPanel::brush: + return "brush"; + case pp::app::ToolsPanel::grids: + return "grids"; + case pp::app::ToolsPanel::animation: + return "animation"; + } + + return "layers"; +} + +const char* tools_panel_action_name(pp::app::ToolsPanelAction action) noexcept +{ + switch (action) { + case pp::app::ToolsPanelAction::open_floating_panel: + return "open-floating-panel"; + case pp::app::ToolsPanelAction::no_op_already_visible: + return "no-op-already-visible"; + } + + return "no-op-already-visible"; +} + +pp::foundation::Result parse_tools_menu_command(std::string_view command) +{ + if (command == "panels") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::panels); + } + if (command == "options") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::options); + } + if (command == "clear-grids") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::clear_grids); + } + if (command == "reset-camera") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::reset_camera); + } + if (command == "shortcuts") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::shortcuts); + } + if (command == "sonarpen") { + return pp::foundation::Result::success(pp::app::ToolsMenuCommand::sonarpen); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown tools menu command")); +} + +pp::foundation::Result parse_tools_panel(std::string_view panel) +{ + if (panel == "presets") { + return pp::foundation::Result::success(pp::app::ToolsPanel::presets); + } + if (panel == "color") { + return pp::foundation::Result::success(pp::app::ToolsPanel::color); + } + if (panel == "color-advanced" || panel == "advanced-color") { + return pp::foundation::Result::success(pp::app::ToolsPanel::color_advanced); + } + if (panel == "layers") { + return pp::foundation::Result::success(pp::app::ToolsPanel::layers); + } + if (panel == "brush") { + return pp::foundation::Result::success(pp::app::ToolsPanel::brush); + } + if (panel == "grids" || panel == "grid") { + return pp::foundation::Result::success(pp::app::ToolsPanel::grids); + } + if (panel == "animation") { + return pp::foundation::Result::success(pp::app::ToolsPanel::animation); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown tools panel")); +} + const char* document_layer_rename_action_name(pp::app::DocumentLayerRenameAction action) noexcept { switch (action) { @@ -1264,6 +1404,8 @@ void print_help() << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n" << " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n" << " 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-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" @@ -2816,6 +2958,111 @@ int plan_app_preferences(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_tools_menu_args( + int argc, + char** argv, + PlanToolsMenuArgs& 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 == "--sonarpen-available") { + args.sonarpen_available = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_tools_menu(int argc, char** argv) +{ + PlanToolsMenuArgs args; + const auto status = parse_plan_tools_menu_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-tools-menu", status.message); + return 2; + } + + const auto command = parse_tools_menu_command(args.command); + if (!command) { + print_error("plan-tools-menu", command.status().message); + return 2; + } + + const auto plan = pp::app::plan_tools_menu_command(command.value(), args.sonarpen_available); + + std::cout << "{\"ok\":true,\"command\":\"plan-tools-menu\"" + << ",\"state\":{\"command\":\"" << json_escape(args.command) + << "\",\"sonarpenAvailable\":" << json_bool(args.sonarpen_available) + << "},\"plan\":{\"command\":\"" << tools_menu_command_name(plan.command) + << "\",\"action\":\"" << tools_menu_action_name(plan.action) + << "\",\"label\":\"" << json_escape(std::string(plan.label)) + << "\",\"closesRootPopup\":" << json_bool(plan.closes_root_popup) + << "}}\n"; + return 0; +} + +pp::foundation::Status parse_plan_tools_panel_args( + int argc, + char** argv, + PlanToolsPanelArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--panel") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.panel = argv[++i]; + } else if (key == "--already-visible") { + args.already_visible = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_tools_panel(int argc, char** argv) +{ + PlanToolsPanelArgs args; + const auto status = parse_plan_tools_panel_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-tools-panel", status.message); + return 2; + } + + const auto panel = parse_tools_panel(args.panel); + if (!panel) { + print_error("plan-tools-panel", panel.status().message); + return 2; + } + + const auto plan = pp::app::plan_tools_panel(panel.value(), args.already_visible); + + std::cout << "{\"ok\":true,\"command\":\"plan-tools-panel\"" + << ",\"state\":{\"panel\":\"" << json_escape(args.panel) + << "\",\"alreadyVisible\":" << json_bool(args.already_visible) + << "},\"plan\":{\"panel\":\"" << tools_panel_name(plan.panel) + << "\",\"action\":\"" << tools_panel_action_name(plan.action) + << "\",\"title\":\"" << json_escape(std::string(plan.title)) + << "\",\"width\":" << plan.width + << ",\"height\":" << plan.height + << ",\"minWidth\":" << plan.min_width + << ",\"minHeight\":" << plan.min_height + << ",\"droppable\":" << json_bool(plan.droppable) + << ",\"hidesEmbeddedTitle\":" << json_bool(plan.hides_embedded_title) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_app_status_args( int argc, char** argv, @@ -6579,6 +6826,14 @@ int main(int argc, char** argv) return plan_app_status(argc, argv); } + if (command == "plan-tools-menu") { + return plan_tools_menu(argc, argv); + } + + if (command == "plan-tools-panel") { + return plan_tools_panel(argc, argv); + } + if (command == "plan-document-resize") { return plan_document_resize(argc, argv); }