Add tools menu service boundary
This commit is contained in:
@@ -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-0031 | Open | Modernization | Top-level File menu command planning now consumes pure `pp_app_core` through `App::init_menu_file` and `pano_cli plan-file-menu`, but live execution still invokes legacy dialogs, platform pickers, cloud code, share code, and canvas import/export paths directly | Preserve File menu behavior while app workflows move toward app/document/platform command services | `pp_app_core_file_menu_tests`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-file-menu --command import`; `pano_cli plan-file-menu --command cloud-upload`; `ctest --preset desktop-fast --build-config Debug` | File menu routing, picker dispatch, save/share/cloud/resize/export execution, and image/project import execution are owned by app/document/platform services with `App::init_menu_file` acting only as a UI adapter |
|
||||
| DEBT-0032 | Open | Modernization | Layer menu command planning now consumes pure `pp_app_core` through `App::init_menu_layer` and `pano_cli plan-layer-menu`, but live execution still calls legacy `Canvas::clear`, `App::dialog_layer_rename`, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer clear, rename, merge-down execution, animation gating, and selected-layer state are owned by document/app services with Layer-menu callbacks acting only as UI adapters |
|
||||
| DEBT-0033 | Open | Modernization | Tools menu and floating-panel planning now consumes pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, and `pano_cli plan-tools-panel`, but live execution still constructs legacy `NodePanelFloating` panels, mutates legacy panel nodes, clears `CanvasModeGrid`, resets `NodeCanvas` camera state, opens legacy shortcuts UI, and calls the iOS SonarPen bridge directly | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter |
|
||||
| DEBT-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-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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
grid-clear, and platform-only SonarPen gating. Direct Tools commands now
|
||||
dispatch through `ToolsMenuServices` before the legacy UI/panel/canvas/platform
|
||||
adapters continue execution. The live animation panel route now also checks
|
||||
animation panel visibility and applies animation panel layout state instead of
|
||||
using the grid panel by mistake.
|
||||
`pano_cli plan-about-menu` exposes app-core planning for About menu help,
|
||||
about, what's-new, crash-test, and performance-test commands, including
|
||||
versioned what's-new labels, diagnostic gating, and no-canvas performance-test
|
||||
@@ -1277,9 +1278,9 @@ Results:
|
||||
`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.
|
||||
root-closing commands, platform-only SonarPen gating, executor dispatch,
|
||||
unavailable no-op actions, 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`,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
@@ -57,6 +59,18 @@ struct ToolsPanelPlan {
|
||||
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(
|
||||
ToolsMenuCommand command,
|
||||
bool sonarpen_available = false) noexcept
|
||||
@@ -138,4 +152,34 @@ struct ToolsPanelPlan {
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -376,6 +376,49 @@ private:
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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*) {
|
||||
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();
|
||||
}
|
||||
execute_tools_menu_plan(*this, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
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*) {
|
||||
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();
|
||||
execute_tools_menu_plan(*this, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
popup_exp->mouse_release();
|
||||
@@ -1605,8 +1651,7 @@ void App::init_menu_tools()
|
||||
|
||||
popup_exp->find<NodeButtonCustom>("shortcuts")->on_click = [this, popup_exp](Node*) {
|
||||
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
|
||||
if (plan.action == pp::app::ToolsMenuAction::show_shortcuts_dialog)
|
||||
dialog_shortcuts();
|
||||
execute_tools_menu_plan(*this, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
popup_exp->mouse_release();
|
||||
@@ -1625,8 +1670,7 @@ void App::init_menu_tools()
|
||||
#if __IOS__
|
||||
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);
|
||||
if (plan.action == pp::app::ToolsMenuAction::start_sonarpen)
|
||||
[ios_app sonarpen_start];
|
||||
execute_tools_menu_plan(*this, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
popup_exp->mouse_release();
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -76,5 +142,7 @@ int main()
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user