Add tools menu service boundary

This commit is contained in:
2026-06-03 12:52:46 +02:00
parent b67f3d63cf
commit ea96f38875
5 changed files with 176 additions and 19 deletions

View File

@@ -50,7 +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-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-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-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-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and call 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 injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter 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 injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter | | DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, but the live adapter 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 injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, but the live adapter still opens legacy open/save/settings/message-box dialogs, mutates legacy `ActionManager` history, and clears the legacy `Canvas` directly | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter | | DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, but the live adapter still opens legacy open/save/settings/message-box dialogs, mutates legacy `ActionManager` history, and clears the legacy `Canvas` directly | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |

View File

@@ -548,10 +548,11 @@ stroke preview, and preset popup execution continue.
`pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core `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 planning for top-level Tools commands and floating-panel requests, including
already-visible no-ops, panel chrome metadata, shortcuts, camera reset, already-visible no-ops, panel chrome metadata, shortcuts, camera reset,
grid-clear, and platform-only SonarPen gating before legacy UI/panel/canvas grid-clear, and platform-only SonarPen gating. Direct Tools commands now
execution continues. The live animation panel route now also checks animation dispatch through `ToolsMenuServices` before the legacy UI/panel/canvas/platform
panel visibility and applies animation panel layout state instead of using the adapters continue execution. The live animation panel route now also checks
grid panel by mistake. 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, `pano_cli plan-about-menu` exposes app-core planning for About menu help,
about, what's-new, crash-test, and performance-test commands, including about, what's-new, crash-test, and performance-test commands, including
versioned what's-new labels, diagnostic gating, and no-canvas performance-test versioned what's-new labels, diagnostic gating, and no-canvas performance-test
@@ -1277,9 +1278,9 @@ Results:
`pano_cli_plan_quick_operation_rejects_bad_restore` passed and expose live `pano_cli_plan_quick_operation_rejects_bad_restore` passed and expose live
quick-panel planning as JSON automation. quick-panel planning as JSON automation.
- `pp_app_core_tools_menu_tests` passed, covering Tools submenu routing, - `pp_app_core_tools_menu_tests` passed, covering Tools submenu routing,
root-closing commands, platform-only SonarPen gating, floating panel chrome root-closing commands, platform-only SonarPen gating, executor dispatch,
metadata, already-visible panel no-ops, and animation panel non-droppable unavailable no-op actions, floating panel chrome metadata, already-visible
state. panel no-ops, and animation panel non-droppable state.
- `pano_cli_plan_tools_menu_shortcuts_smoke`, - `pano_cli_plan_tools_menu_shortcuts_smoke`,
`pano_cli_plan_tools_menu_sonarpen_unavailable_smoke`, `pano_cli_plan_tools_menu_sonarpen_unavailable_smoke`,
`pano_cli_plan_tools_panel_layers_smoke`, `pano_cli_plan_tools_panel_layers_smoke`,

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "foundation/result.h"
#include <string_view> #include <string_view>
namespace pp::app { namespace pp::app {
@@ -57,6 +59,18 @@ struct ToolsPanelPlan {
bool hides_embedded_title = false; bool hides_embedded_title = false;
}; };
class ToolsMenuServices {
public:
virtual ~ToolsMenuServices() = default;
virtual void show_panels_submenu() = 0;
virtual void show_options_submenu() = 0;
virtual void clear_grid_overlays() = 0;
virtual void reset_camera() = 0;
virtual void show_shortcuts_dialog() = 0;
virtual void start_sonarpen() = 0;
};
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command( [[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
ToolsMenuCommand command, ToolsMenuCommand command,
bool sonarpen_available = false) noexcept bool sonarpen_available = false) noexcept
@@ -138,4 +152,34 @@ struct ToolsPanelPlan {
return plan; return plan;
} }
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
const ToolsMenuPlan& plan,
ToolsMenuServices& services)
{
switch (plan.action) {
case ToolsMenuAction::show_panels_submenu:
services.show_panels_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::show_options_submenu:
services.show_options_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::clear_grid_overlays:
services.clear_grid_overlays();
return pp::foundation::Status::success();
case ToolsMenuAction::reset_camera:
services.reset_camera();
return pp::foundation::Status::success();
case ToolsMenuAction::show_shortcuts_dialog:
services.show_shortcuts_dialog();
return pp::foundation::Status::success();
case ToolsMenuAction::start_sonarpen:
services.start_sonarpen();
return pp::foundation::Status::success();
case ToolsMenuAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown tools menu action");
}
} }

View File

@@ -376,6 +376,49 @@ private:
App& app_; App& app_;
}; };
class LegacyToolsMenuServices final : public pp::app::ToolsMenuServices {
public:
explicit LegacyToolsMenuServices(App& app) noexcept
: app_(app)
{
}
void show_panels_submenu() override
{
}
void show_options_submenu() override
{
}
void clear_grid_overlays() override
{
auto* mode = static_cast<CanvasModeGrid*>(Canvas::modes[(int)kCanvasMode::Grid][0]);
mode->clear();
}
void reset_camera() override
{
if (app_.canvas)
app_.canvas->reset_camera();
}
void show_shortcuts_dialog() override
{
app_.dialog_shortcuts();
}
void start_sonarpen() override
{
#if __IOS__
[app_.ios_app sonarpen_start];
#endif
}
private:
App& app_;
};
void execute_main_toolbar_plan(App& app, const pp::app::MainToolbarPlan& plan) void execute_main_toolbar_plan(App& app, const pp::app::MainToolbarPlan& plan)
{ {
LegacyMainToolbarServices services(app); LegacyMainToolbarServices services(app);
@@ -392,6 +435,14 @@ void execute_about_menu_plan(App& app, const pp::app::AboutMenuPlan& plan)
LOG("About menu action failed: %s", status.message); LOG("About menu action failed: %s", status.message);
} }
void execute_tools_menu_plan(App& app, const pp::app::ToolsMenuPlan& plan)
{
LegacyToolsMenuServices services(app);
const auto status = pp::app::execute_tools_menu_plan(plan, services);
if (!status.ok())
LOG("Tools menu action failed: %s", status.message);
}
} // namespace } // namespace
void App::title_update() void App::title_update()
@@ -1580,11 +1631,7 @@ void App::init_menu_tools()
popup_exp->find<NodeButtonCustom>("clear-grids")->on_click = [this, popup_exp](Node*) { popup_exp->find<NodeButtonCustom>("clear-grids")->on_click = [this, popup_exp](Node*) {
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids); const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids);
if (plan.action == pp::app::ToolsMenuAction::clear_grid_overlays) execute_tools_menu_plan(*this, plan);
{
CanvasModeGrid* mode = (CanvasModeGrid*)Canvas::modes[(int)kCanvasMode::Grid][0];
mode->clear();
}
if (plan.closes_root_popup) if (plan.closes_root_popup)
{ {
popup_exp->mouse_release(); popup_exp->mouse_release();
@@ -1594,8 +1641,7 @@ void App::init_menu_tools()
popup_exp->find<NodeButtonCustom>("camera-reset")->on_click = [this, popup_exp](Node*) { popup_exp->find<NodeButtonCustom>("camera-reset")->on_click = [this, popup_exp](Node*) {
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera); const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera);
if (plan.action == pp::app::ToolsMenuAction::reset_camera) execute_tools_menu_plan(*this, plan);
canvas->reset_camera();
if (plan.closes_root_popup) if (plan.closes_root_popup)
{ {
popup_exp->mouse_release(); popup_exp->mouse_release();
@@ -1605,8 +1651,7 @@ void App::init_menu_tools()
popup_exp->find<NodeButtonCustom>("shortcuts")->on_click = [this, popup_exp](Node*) { popup_exp->find<NodeButtonCustom>("shortcuts")->on_click = [this, popup_exp](Node*) {
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts); const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
if (plan.action == pp::app::ToolsMenuAction::show_shortcuts_dialog) execute_tools_menu_plan(*this, plan);
dialog_shortcuts();
if (plan.closes_root_popup) if (plan.closes_root_popup)
{ {
popup_exp->mouse_release(); popup_exp->mouse_release();
@@ -1625,8 +1670,7 @@ void App::init_menu_tools()
#if __IOS__ #if __IOS__
popup_exp->find<NodeButtonCustom>("sonarpen")->on_click = [this, popup_exp](Node*) { popup_exp->find<NodeButtonCustom>("sonarpen")->on_click = [this, popup_exp](Node*) {
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true); const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true);
if (plan.action == pp::app::ToolsMenuAction::start_sonarpen) execute_tools_menu_plan(*this, plan);
[ios_app sonarpen_start];
if (plan.closes_root_popup) if (plan.closes_root_popup)
{ {
popup_exp->mouse_release(); popup_exp->mouse_release();

View File

@@ -3,6 +3,33 @@
namespace { namespace {
class FakeToolsMenuServices final : public pp::app::ToolsMenuServices {
public:
void show_panels_submenu() override { panels_submenus += 1; }
void show_options_submenu() override { options_submenus += 1; }
void clear_grid_overlays() override { clear_grid_calls += 1; }
void reset_camera() override { reset_camera_calls += 1; }
void show_shortcuts_dialog() override { shortcut_dialogs += 1; }
void start_sonarpen() override { sonarpen_starts += 1; }
[[nodiscard]] int total_calls() const noexcept
{
return panels_submenus
+ options_submenus
+ clear_grid_calls
+ reset_camera_calls
+ shortcut_dialogs
+ sonarpen_starts;
}
int panels_submenus = 0;
int options_submenus = 0;
int clear_grid_calls = 0;
int reset_camera_calls = 0;
int shortcut_dialogs = 0;
int sonarpen_starts = 0;
};
void tools_menu_maps_submenus_and_commands(pp::tests::Harness& harness) 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 panels = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels);
@@ -67,6 +94,45 @@ void tools_panel_no_ops_when_panel_is_already_visible(pp::tests::Harness& harnes
PP_EXPECT(harness, visible.hides_embedded_title == hidden.hides_embedded_title); PP_EXPECT(harness, visible.hides_embedded_title == hidden.hides_embedded_title);
} }
void tools_menu_executor_dispatches_direct_commands(pp::tests::Harness& harness)
{
FakeToolsMenuServices services;
const auto panels = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(panels, services).ok());
PP_EXPECT(harness, services.panels_submenus == 1);
const auto options = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(options, services).ok());
PP_EXPECT(harness, services.options_submenus == 1);
const auto clear = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(clear, services).ok());
PP_EXPECT(harness, services.clear_grid_calls == 1);
const auto camera = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(camera, services).ok());
PP_EXPECT(harness, services.reset_camera_calls == 1);
const auto shortcuts = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(shortcuts, services).ok());
PP_EXPECT(harness, services.shortcut_dialogs == 1);
const auto sonarpen = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(sonarpen, services).ok());
PP_EXPECT(harness, services.sonarpen_starts == 1);
}
void tools_menu_executor_preserves_unavailable_no_op(pp::tests::Harness& harness)
{
FakeToolsMenuServices services;
const auto unavailable = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, false);
PP_EXPECT(harness, unavailable.action == pp::app::ToolsMenuAction::no_op_unavailable);
PP_EXPECT(harness, pp::app::execute_tools_menu_plan(unavailable, services).ok());
PP_EXPECT(harness, services.total_calls() == 0);
}
} }
int main() int main()
@@ -76,5 +142,7 @@ int main()
harness.run("tools menu gates platform only actions", tools_menu_gates_platform_only_actions); 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 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); harness.run("tools panel no ops when panel is already visible", tools_panel_no_ops_when_panel_is_already_visible);
harness.run("tools menu executor dispatches direct commands", tools_menu_executor_dispatches_direct_commands);
harness.run("tools menu executor preserves unavailable no op", tools_menu_executor_preserves_unavailable_no_op);
return harness.finish(); return harness.finish();
} }