From 5def47cdccdb44dfac851d01e08638f06d578e5e Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 07:52:58 +0200 Subject: [PATCH] Plan canvas toolbar bindings --- docs/modernization/build-inventory.md | 5 +- docs/modernization/debt.md | 10 +- docs/modernization/roadmap.md | 21 +++- src/app_core/canvas_tool_ui.h | 59 ++++++++++ src/app_layout.cpp | 140 ++++++++---------------- tests/CMakeLists.txt | 13 +++ tests/app_core/canvas_tool_ui_tests.cpp | 43 ++++++++ tools/pano_cli/main.cpp | 47 ++++++++ 8 files changed, 241 insertions(+), 97 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 80f6a0f..7360f83 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -269,7 +269,10 @@ Known local toolchain state: - `src/legacy_canvas_tool_services.*` is the current app-shell bridge for canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core` - contracts while canvas mode tip visibility and pressure remapping now ask + contracts; `App::init_toolbar_draw()` now consumes the tested + `pp_app_core` draw-toolbar binding plan exposed through + `pano_cli plan-canvas-tool-toolbar` before retained button lookup and legacy + execution dispatch. Canvas mode tip visibility and pressure remapping now ask `PlatformServices`; `NodeCanvas::update_cursor()` now consumes `pp_app_core` canvas cursor visibility planning exposed through `pano_cli plan-canvas-cursor` before retained platform cursor dispatch. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index a1c5f04..6c270b0 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -75,6 +75,14 @@ agent or engineer to remove them without reconstructing context from chat. small-brush, not-painting, modifier, and malformed-brush states for automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor execution remain open under DEBT-0027. +- 2026-06-05: DEBT-0027 was narrowed again. The full draw-toolbar binding set + now goes through tested `pp_app_core` planning, live + `App::init_toolbar_draw()` consumes the same plan for button handler wiring + and default draw-mode initialization, and `pano_cli plan-canvas-tool-toolbar` + exposes the binding ids, actions, button-class expectation, and default + selection for automation. Retained `NodeButton`/`NodeButtonCustom` lookup, + legacy `Canvas` mode mutation, picking/touch-lock state, and transform action + execution remain open under DEBT-0027. - 2026-06-05: DEBT-0033 was narrowed again. Canvas reset-camera defaults, viewport density, and cursor mode now go through tested `pp_app_core` plans and shared `src/legacy_canvas_view_services.*` execution. Live Tools @@ -190,7 +198,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot, mini-state, and size/flow slider preview planning now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, `pano_cli plan-quick-slider-preview`, and the `QuickUiServices` boundary; live slot/popup/restore/reset execution is centralized in `src/legacy_quick_ui_services.*`, and live slider callbacks now consume `pp_app_core` preview cursor/tip planning directly, but the bridge and panel adapter still mutate legacy quick UI widgets, `Brush` previews, color picker popup state, preset popup state, and direct legacy `CanvasModePen`/`CanvasModeLine` fields | Preserve quick-panel behavior while quick brush/color and slider preview commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `pano_cli plan-quick-slider-preview --slider-x 10 --slider-y 20 --slider-height 40 --zoom 2 --pen-mode --no-line-mode`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, color execution, and slider preview mode updates are owned by injected app/brush/UI/canvas services with no legacy quick-panel adapter, popup adapter, direct brush-preview mutation, or direct `CanvasMode*` field writes | | DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter | -| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, active-state planning/execution dispatch, canvas cursor visibility planning, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-cursor`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, `NodeCanvas::update_cursor()` consumes the cursor planner before retained platform cursor dispatch, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, `CanvasModePen` drawing/resizing/picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, and cursor/UI singletons | Preserve current toolbar, stylus eraser, cursor visibility, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5`; `pano_cli plan-canvas-cursor --visibility small-brush --bad-size`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, cursor visibility planning/execution, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter | +| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, full draw-toolbar binding planning, canvas input mode switching, active-state planning/execution dispatch, canvas cursor visibility planning, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-toolbar`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-cursor`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, `NodeCanvas::update_cursor()` consumes the cursor planner before retained platform cursor dispatch, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, `CanvasModePen` drawing/resizing/picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, retained `NodeButton`/`NodeButtonCustom` lookup, and cursor/UI singletons | Preserve current toolbar, stylus eraser, cursor visibility, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-toolbar`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5`; `pano_cli plan-canvas-cursor --visibility small-brush --bad-size`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar binding, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, cursor visibility planning/execution, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter | | DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter | | DEBT-0029 | Open | Modernization | Image import route planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-image-import`, and the `DocumentImageImportServices` boundary, and live File-menu import execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still loads images with legacy `Image`, calls legacy `Canvas::import_equirectangular`, or configures legacy import transform mode directly | Preserve current File > Import behavior while image import moves toward document/app/asset command services | `pp_app_core_document_import_tests`; `pano_cli plan-image-import --width 4096 --height 2048`; `pano_cli plan-image-import --width 1024 --height 1024`; `ctest --preset desktop-fast --build-config Debug` | Image loading, equirectangular import, transform-placement import, and failure reporting are owned by injected document/app/asset services with File-menu callbacks acting only as adapters and no legacy image-import adapter | | DEBT-0030 | Open | Modernization | File export menu action planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-export-menu`, and the `DocumentExportMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge 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 injected document/app/renderer/video services with File-menu callbacks acting only as UI adapters and no legacy export adapter | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 8be7f52..850e552 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -611,6 +611,12 @@ camera, grid, copy, cut, fill, mask, flood-fill, pick, and touch-lock toolbar commands. Canvas tool execution now dispatches through `CanvasToolServices` in `src/legacy_canvas_tool_services.*` before legacy toolbar selection, `Canvas` mode, pen picking, touch-lock, and transform state adapters continue. +`pano_cli plan-canvas-tool-toolbar` now exposes the full draw-toolbar binding +set, including button ids, select/toggle actions, button-class expectation, and +the default draw-mode initialization. Live `App::init_toolbar_draw` consumes the +same app-core toolbar plan to install handlers and apply the initial draw tool +while retained `NodeButton`/`NodeButtonCustom` lookup and legacy canvas-tool +execution remain under `DEBT-0027`. `pano_cli plan-canvas-tool-state` exposes the matching toolbar active-state refresh used by `App::update` before legacy `Canvas` mode state remains the source of truth. `NodeCanvas` stylus eraser mode switching consumes the same @@ -1911,8 +1917,9 @@ Results: grid/heightmap/lightmap planning as JSON automation. - `pp_app_core_canvas_tool_ui_tests` passed, covering toolbar mode selection, copy/cut transform action planning, pick no-op outside draw mode, and - touch-lock toggling, plus toolbar active-state derivation for draw, copy, and - bucket modes, service dispatch ordering, pick no-op execution, and malformed + touch-lock toggling, full draw-toolbar binding projection, binding-to-action + conversion, plus toolbar active-state derivation for draw, copy, and bucket + modes, service dispatch ordering, pick no-op execution, and malformed execution payload rejection. - `pp_app_core_canvas_hotkey_tests` passed, covering E draw/erase toggles, Ctrl+Z/Ctrl+Shift+Z history planning, Ctrl+S/Ctrl+Shift+S document save @@ -1931,10 +1938,20 @@ Results: `pano_cli_plan_canvas_tool_touch_lock_smoke`, and `pano_cli_plan_canvas_tool_rejects_unknown` passed and expose live draw toolbar planning as JSON automation. +- `pano_cli_plan_canvas_tool_toolbar_smoke` and + `pano_cli_plan_canvas_tool_toolbar_rejects_unknown` passed and expose the + full live draw-toolbar binding set as JSON automation. - `pano_cli_plan_canvas_tool_state_draw_smoke`, `pano_cli_plan_canvas_tool_state_copy_smoke`, and `pano_cli_plan_canvas_tool_state_rejects_unknown` passed and expose draw toolbar active-state refresh as JSON automation. +- `PanoPainter`, `pano_cli`, and `pp_app_core_canvas_tool_ui_tests` built on + Windows after `App::init_toolbar_draw()` moved to the app-core toolbar + binding plan; the build required the documented clean after a stale + debug-info `LNK1103`. +- Android arm64 headless `pp_app_core`, `pano_cli`, and + `pp_app_core_canvas_tool_ui_tests` built after the same toolbar binding + planner change. - `pp_app_core_document_canvas_tests` passed, covering clear-current-layer undo/dirty intent, no-canvas no-op behavior, and invalid clear color rejection, service dispatch color forwarding, no-op execution preservation, diff --git a/src/app_core/canvas_tool_ui.h b/src/app_core/canvas_tool_ui.h index 702d8e8..fd151c8 100644 --- a/src/app_core/canvas_tool_ui.h +++ b/src/app_core/canvas_tool_ui.h @@ -2,7 +2,9 @@ #include "foundation/result.h" +#include #include +#include namespace pp::app { @@ -32,6 +34,12 @@ enum class CanvasToolTransformAction { cut, }; +enum class CanvasToolToolbarAction { + select_mode, + toggle_picking, + toggle_touch_lock, +}; + enum class CanvasCursorVisibilityMode { never, small_brush, @@ -68,6 +76,19 @@ struct CanvasToolButtonState { bool flood_fill_active = false; }; +struct CanvasToolToolbarBinding { + std::string_view button_id; + CanvasToolToolbarAction action = CanvasToolToolbarAction::select_mode; + CanvasToolMode mode = CanvasToolMode::draw; + bool custom_button = true; + bool applies_default_on_init = false; +}; + +struct CanvasToolToolbarPlan { + std::array bindings {}; + CanvasToolMode default_mode = CanvasToolMode::draw; +}; + struct CanvasCursorVisibilityInput { CanvasToolMode mode = CanvasToolMode::draw; CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never; @@ -139,6 +160,44 @@ public: return plan; } +[[nodiscard]] inline constexpr CanvasToolToolbarPlan plan_canvas_tool_toolbar() noexcept +{ + return { + std::array { + CanvasToolToolbarBinding { "btn-pen", CanvasToolToolbarAction::select_mode, CanvasToolMode::draw, true, true }, + CanvasToolToolbarBinding { "btn-pick", CanvasToolToolbarAction::toggle_picking, CanvasToolMode::draw, true, false }, + CanvasToolToolbarBinding { "btn-touchlock", CanvasToolToolbarAction::toggle_touch_lock, CanvasToolMode::draw, true, false }, + CanvasToolToolbarBinding { "btn-erase", CanvasToolToolbarAction::select_mode, CanvasToolMode::erase, true, false }, + CanvasToolToolbarBinding { "btn-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::line, true, false }, + CanvasToolToolbarBinding { "btn-cam", CanvasToolToolbarAction::select_mode, CanvasToolMode::camera, false, false }, + CanvasToolToolbarBinding { "btn-grid", CanvasToolToolbarAction::select_mode, CanvasToolMode::grid, false, false }, + CanvasToolToolbarBinding { "btn-copy", CanvasToolToolbarAction::select_mode, CanvasToolMode::copy, false, false }, + CanvasToolToolbarBinding { "btn-cut", CanvasToolToolbarAction::select_mode, CanvasToolMode::cut, false, false }, + CanvasToolToolbarBinding { "btn-fill", CanvasToolToolbarAction::select_mode, CanvasToolMode::fill, false, false }, + CanvasToolToolbarBinding { "btn-mask-free", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_free, true, false }, + CanvasToolToolbarBinding { "btn-mask-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_line, true, false }, + CanvasToolToolbarBinding { "btn-bucket", CanvasToolToolbarAction::select_mode, CanvasToolMode::flood_fill, true, false }, + }, + CanvasToolMode::draw, + }; +} + +[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_toolbar_binding_action( + const CanvasToolToolbarBinding& binding, + bool current_mode_is_draw) noexcept +{ + switch (binding.action) { + case CanvasToolToolbarAction::select_mode: + return plan_canvas_tool_select(binding.mode); + case CanvasToolToolbarAction::toggle_picking: + return plan_canvas_tool_pick_toggle(current_mode_is_draw); + case CanvasToolToolbarAction::toggle_touch_lock: + return plan_canvas_tool_touch_lock_toggle(); + } + + return plan_canvas_tool_select(CanvasToolMode::draw); +} + [[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state( CanvasToolMode mode, bool picking, diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 6feaaba..25e06ad 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -590,108 +590,62 @@ void App::init_sidebar() }; } } -template -void apply_canvas_tool_select(App& app, T* button, pp::app::CanvasToolMode mode) +[[nodiscard]] bool current_canvas_mode_is_draw(App& app) noexcept { - const auto plan = pp::app::plan_canvas_tool_select(mode); - const auto status = pp::panopainter::execute_legacy_canvas_tool_plan(app, plan, button); + return app.canvas && app.canvas->m_canvas && app.canvas->m_canvas->m_current_mode == kCanvasMode::Draw; +} + +template +void execute_canvas_tool_toolbar_binding( + App& app, + const pp::app::CanvasToolToolbarBinding& binding, + T* button) +{ + const auto plan = pp::app::plan_canvas_tool_toolbar_binding_action( + binding, + current_canvas_mode_is_draw(app)); + const auto status = binding.action == pp::app::CanvasToolToolbarAction::select_mode + ? pp::panopainter::execute_legacy_canvas_tool_plan(app, plan, button) + : pp::panopainter::execute_legacy_canvas_tool_plan(app, plan); if (!status.ok()) - LOG("Canvas tool select action failed: %s", status.message); + LOG("Canvas toolbar action failed: %s", status.message); +} + +template +void bind_canvas_tool_toolbar_button( + App& app, + const pp::app::CanvasToolToolbarBinding& binding, + T* button) +{ + button->on_click = [&app, binding, button](Node*) { + execute_canvas_tool_toolbar_binding(app, binding, button); + }; } void App::init_toolbar_draw() { - if (auto* button = layout[main_id]->find("btn-pen")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::draw); - }; - //button->set_active(true); - const auto plan = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw); - const auto status = pp::panopainter::execute_legacy_canvas_tool_plan(*this, plan); + const auto toolbar = pp::app::plan_canvas_tool_toolbar(); + bool apply_default_tool = false; + for (const auto& binding : toolbar.bindings) { + if (binding.custom_button) { + if (auto* button = layout[main_id]->find(binding.button_id.data())) { + bind_canvas_tool_toolbar_button(*this, binding, button); + apply_default_tool = apply_default_tool || binding.applies_default_on_init; + } + } else { + if (auto* button = layout[main_id]->find(binding.button_id.data())) { + bind_canvas_tool_toolbar_button(*this, binding, button); + apply_default_tool = apply_default_tool || binding.applies_default_on_init; + } + } + } + + if (apply_default_tool) { + const auto default_plan = pp::app::plan_canvas_tool_select(toolbar.default_mode); + const auto status = pp::panopainter::execute_legacy_canvas_tool_plan(*this, default_plan); if (!status.ok()) LOG("Canvas default tool action failed: %s", status.message); } - if (auto* button = layout[main_id]->find("btn-pick")) - { - button->on_click = [this](Node*) { - const auto plan = pp::app::plan_canvas_tool_pick_toggle( - canvas->m_canvas->m_current_mode == kCanvasMode::Draw); - const auto status = pp::panopainter::execute_legacy_canvas_tool_plan(*this, plan); - if (!status.ok()) - LOG("Canvas pick action failed: %s", status.message); - }; - } - if (auto* button = layout[main_id]->find("btn-touchlock")) - { - button->on_click = [this](Node*) { - const auto plan = pp::app::plan_canvas_tool_touch_lock_toggle(); - const auto status = pp::panopainter::execute_legacy_canvas_tool_plan(*this, plan); - if (!status.ok()) - LOG("Canvas touch-lock action failed: %s", status.message); - }; - } - if (auto* button = layout[main_id]->find("btn-erase")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::erase); - }; - } - if (auto* button = layout[main_id]->find("btn-line")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::line); - }; - } - if (auto* button = layout[main_id]->find("btn-cam")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::camera); - }; - } - if (auto* button = layout[main_id]->find("btn-grid")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::grid); - }; - } - if (auto* button = layout[main_id]->find("btn-copy")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::copy); - }; - } - if (auto* button = layout[main_id]->find("btn-cut")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::cut); - }; - } - if (auto* button = layout[main_id]->find("btn-fill")) - { - // polygon fill - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::fill); - }; - } - if (auto* button = layout[main_id]->find("btn-mask-free")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_free); - }; - } - if (auto* button = layout[main_id]->find("btn-mask-line")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::mask_line); - }; - } - if (auto* button = layout[main_id]->find("btn-bucket")) - { - button->on_click = [this, button](Node*) { - apply_canvas_tool_select(*this, button, pp::app::CanvasToolMode::flood_fill); - }; - } } void App::init_menu_file() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dab0d95..89e4b60 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1894,6 +1894,19 @@ if(TARGET pano_cli) LABELS "app;ui;integration;desktop-fast;fuzz" WILL_FAIL TRUE) + add_test(NAME pano_cli_plan_canvas_tool_toolbar_smoke + COMMAND pano_cli plan-canvas-tool-toolbar) + set_tests_properties(pano_cli_plan_canvas_tool_toolbar_smoke PROPERTIES + LABELS "app;ui;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool-toolbar\".*\"defaultMode\":\"draw\".*\"bindingCount\":13.*\"id\":\"btn-pen\".*\"defaultOnInit\":true.*\"id\":\"btn-pick\".*\"action\":\"toggle-picking\".*\"id\":\"btn-bucket\".*\"mode\":\"bucket\"") + + add_test(NAME pano_cli_plan_canvas_tool_toolbar_rejects_unknown + COMMAND pano_cli plan-canvas-tool-toolbar --unexpected) + set_tests_properties(pano_cli_plan_canvas_tool_toolbar_rejects_unknown PROPERTIES + LABELS "app;ui;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-tool-toolbar\".*\"message\":\"unknown option\"") + add_test(NAME pano_cli_plan_canvas_tool_state_draw_smoke COMMAND pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock) set_tests_properties(pano_cli_plan_canvas_tool_state_draw_smoke PROPERTIES diff --git a/tests/app_core/canvas_tool_ui_tests.cpp b/tests/app_core/canvas_tool_ui_tests.cpp index efb8ab7..559ed95 100644 --- a/tests/app_core/canvas_tool_ui_tests.cpp +++ b/tests/app_core/canvas_tool_ui_tests.cpp @@ -98,6 +98,47 @@ void pick_and_touch_lock_toggle_state(pp::tests::Harness& harness) PP_EXPECT(harness, !touch_lock.no_op); } +void toolbar_plan_exposes_full_legacy_button_set(pp::tests::Harness& harness) +{ + const auto toolbar = pp::app::plan_canvas_tool_toolbar(); + PP_EXPECT(harness, toolbar.bindings.size() == 13); + PP_EXPECT(harness, toolbar.default_mode == pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, toolbar.bindings[0].button_id == "btn-pen"); + PP_EXPECT(harness, toolbar.bindings[0].mode == pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, toolbar.bindings[0].applies_default_on_init); + PP_EXPECT(harness, toolbar.bindings[1].button_id == "btn-pick"); + PP_EXPECT(harness, toolbar.bindings[1].action == pp::app::CanvasToolToolbarAction::toggle_picking); + PP_EXPECT(harness, toolbar.bindings[2].button_id == "btn-touchlock"); + PP_EXPECT(harness, toolbar.bindings[2].action == pp::app::CanvasToolToolbarAction::toggle_touch_lock); + PP_EXPECT(harness, toolbar.bindings[5].button_id == "btn-cam"); + PP_EXPECT(harness, !toolbar.bindings[5].custom_button); + PP_EXPECT(harness, toolbar.bindings[12].button_id == "btn-bucket"); + PP_EXPECT(harness, toolbar.bindings[12].mode == pp::app::CanvasToolMode::flood_fill); +} + +void toolbar_binding_actions_match_canvas_tool_plans(pp::tests::Harness& harness) +{ + const auto toolbar = pp::app::plan_canvas_tool_toolbar(); + const auto draw = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[0], true); + const auto pick_from_draw = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[1], true); + const auto pick_from_camera = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[1], false); + const auto touch_lock = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[2], false); + const auto copy = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[7], false); + const auto cut = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[8], false); + + PP_EXPECT(harness, draw.operation == pp::app::CanvasToolOperation::select_mode); + PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw); + PP_EXPECT(harness, pick_from_draw.operation == pp::app::CanvasToolOperation::toggle_picking); + PP_EXPECT(harness, pick_from_draw.toggles_picking); + PP_EXPECT(harness, !pick_from_draw.no_op); + PP_EXPECT(harness, pick_from_camera.no_op); + PP_EXPECT(harness, !pick_from_camera.toggles_picking); + PP_EXPECT(harness, touch_lock.operation == pp::app::CanvasToolOperation::toggle_touch_lock); + PP_EXPECT(harness, touch_lock.toggles_touch_lock); + PP_EXPECT(harness, copy.transform_action == pp::app::CanvasToolTransformAction::copy); + PP_EXPECT(harness, cut.transform_action == pp::app::CanvasToolTransformAction::cut); +} + void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness) { const auto draw = pp::app::plan_canvas_tool_button_state( @@ -295,6 +336,8 @@ int main() harness.run("selection plans canvas modes", selection_plans_canvas_modes); harness.run("transform tools plan copy and cut actions", transform_tools_plan_copy_and_cut_actions); harness.run("pick and touch lock toggle state", pick_and_touch_lock_toggle_state); + harness.run("toolbar plan exposes full legacy button set", toolbar_plan_exposes_full_legacy_button_set); + harness.run("toolbar binding actions match canvas tool plans", toolbar_binding_actions_match_canvas_tool_plans); harness.run("button state tracks active mode and toggles", button_state_tracks_active_mode_and_toggles); harness.run("cursor visibility projects canvas state", cursor_visibility_projects_canvas_state); harness.run("cursor visibility forces visible for modifier or tool", cursor_visibility_forces_visible_for_modifier_or_tool); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 02de411..f9f034e 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1526,6 +1526,20 @@ const char* canvas_tool_operation_name(pp::app::CanvasToolOperation operation) n return "select-mode"; } +const char* canvas_tool_toolbar_action_name(pp::app::CanvasToolToolbarAction action) noexcept +{ + switch (action) { + case pp::app::CanvasToolToolbarAction::select_mode: + return "select-mode"; + case pp::app::CanvasToolToolbarAction::toggle_picking: + return "toggle-picking"; + case pp::app::CanvasToolToolbarAction::toggle_touch_lock: + return "toggle-touch-lock"; + } + + return "select-mode"; +} + const char* canvas_tool_mode_name(pp::app::CanvasToolMode mode) noexcept { switch (mode) { @@ -2210,6 +2224,7 @@ void print_help() << " plan-stroke-composite [--width N] [--height N] [--layer-blend N] [--stroke-blend N] [--dual-blend] [--pattern-blend] [--framebuffer-fetch] [--texture-copy] [--blit] [--explicit-transitions] [--render-only] [--depth]\n" << " plan-canvas-hotkey --event key-down|key-up|touch-tap --key e|z|s|tab|alt|android-back|bracket-left|bracket-right [--ctrl] [--shift] [--mouse-focus] [--undo-count N] [--redo-count N] [--touch-fingers N]\n" << " plan-canvas-tool --kind draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket|pick|touch-lock [--current-mode-draw]\n" + << " plan-canvas-tool-toolbar\n" << " plan-canvas-tool-state [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--picking] [--touch-lock]\n" << " plan-canvas-camera-reset\n" << " plan-canvas-view-density [--density N] [--bad-float]\n" @@ -7666,6 +7681,34 @@ int plan_canvas_tool(int argc, char** argv) return 0; } +int plan_canvas_tool_toolbar(int argc, char** argv) +{ + if (argc > 2) { + print_error("plan-canvas-tool-toolbar", "unknown option"); + return 2; + } + + const auto toolbar = pp::app::plan_canvas_tool_toolbar(); + std::cout << "{\"ok\":true,\"command\":\"plan-canvas-tool-toolbar\"" + << ",\"defaultMode\":\"" << canvas_tool_mode_name(toolbar.default_mode) + << "\",\"bindingCount\":" << toolbar.bindings.size() + << ",\"bindings\":["; + for (std::size_t i = 0; i < toolbar.bindings.size(); ++i) { + const auto& binding = toolbar.bindings[i]; + if (i > 0) { + std::cout << ","; + } + std::cout << "{\"id\":\"" << json_escape(binding.button_id) + << "\",\"action\":\"" << canvas_tool_toolbar_action_name(binding.action) + << "\",\"mode\":\"" << canvas_tool_mode_name(binding.mode) + << "\",\"customButton\":" << json_bool(binding.custom_button) + << ",\"defaultOnInit\":" << json_bool(binding.applies_default_on_init) + << "}"; + } + std::cout << "]}\n"; + return 0; +} + pp::foundation::Result parse_canvas_hotkey_event(std::string_view event) { if (event == "key-down") { @@ -11172,6 +11215,10 @@ int main(int argc, char** argv) return plan_canvas_tool(argc, argv); } + if (command == "plan-canvas-tool-toolbar") { + return plan_canvas_tool_toolbar(argc, argv); + } + if (command == "plan-canvas-tool-state") { return plan_canvas_tool_state(argc, argv); }