Add history command service boundary

This commit is contained in:
2026-06-03 13:22:16 +02:00
parent 6d0cc4eb15
commit 6427f218e7
7 changed files with 228 additions and 22 deletions

View File

@@ -43,7 +43,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | 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-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | 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 and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | Preserve quick-panel behavior while quick brush/color 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`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by app/brush/UI services with `NodePanelQuick` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | Preserve quick-panel behavior while quick brush/color 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`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by app/brush/UI services with `NodePanelQuick` acting only as UI adapter |
| DEBT-0026 | Open | Modernization | Toolbar and canvas history command planning now consumes pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, and `pano_cli plan-history-operation`, but live execution still mutates legacy `ActionManager` stacks and `Canvas::I` unsaved state 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 document/app history services with toolbar and canvas input acting only as adapters | | DEBT-0026 | Open | Modernization | Toolbar history command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, but the live adapter 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, and active-state planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, and `pano_cli plan-canvas-tool-state`, but live execution/state storage still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, and transform copy/cut action objects directly | Preserve current toolbar, stylus eraser, and keyboard draw/erase behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, and transform action execution are owned by app/document/canvas services with toolbar/canvas callbacks acting only as adapters | | DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, canvas input mode switching, and active-state planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, and `pano_cli plan-canvas-tool-state`, but live execution/state storage still mutates or reads legacy `Canvas` mode state, pen picking state, touch-lock state, and transform copy/cut action objects directly | Preserve current toolbar, stylus eraser, and keyboard draw/erase behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, and transform action execution are owned by app/document/canvas services with toolbar/canvas callbacks acting only as adapters |
| DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, but the live adapter 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`; `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-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, but the live adapter 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`; `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, but the live adapter 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-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, but the live adapter 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 |
@@ -52,7 +52,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary, but the live adapter 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 injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu adapter | | DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary, but the live adapter 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 injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu 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-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, and history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, but the live adapter still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | 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 |
## Closed Debt ## Closed Debt

View File

@@ -538,15 +538,16 @@ pick/load/reload/clear, lightmap render capability/limit checks, and heightmap
commit used by the live grid panel before legacy image loading, OpenGL texture commit used by the live grid panel before legacy image loading, OpenGL texture
updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue.
`pano_cli plan-history-operation` exposes app-core planning for undo, redo, and `pano_cli plan-history-operation` exposes app-core planning for undo, redo, and
clear-history availability used by toolbar buttons and canvas shortcuts before clear-history availability used by toolbar buttons and canvas shortcuts; live
legacy `ActionManager` stack execution continues. toolbar execution now dispatches through `HistoryUiServices` before the legacy
`ActionManager` stack adapter continues.
`pano_cli plan-main-toolbar` exposes app-core planning for the live main `pano_cli plan-main-toolbar` exposes app-core planning for the live main
toolbar/status-bar shell, including open/save dialogs, undo/redo availability, toolbar/status-bar shell, including open/save dialogs, undo/redo availability,
clear-history availability, clear-canvas no-canvas blocking, message-box clear-history availability, clear-canvas no-canvas blocking, message-box
creation, and settings dialog routing. `pp_app_core` now also owns a creation, and settings dialog routing. `pp_app_core` now also owns a
`MainToolbarServices` executor boundary, so `App::init_toolbar_main` dispatches `MainToolbarServices` executor boundary, so `App::init_toolbar_main` dispatches
through a legacy adapter before legacy dialogs, `ActionManager`, and `Canvas` through a legacy adapter before legacy dialogs, history/canvas adapters, and
execution continue. settings UI execution continue.
`pano_cli plan-quick-operation` exposes app-core planning for quick brush/color `pano_cli plan-quick-operation` exposes app-core planning for quick brush/color
slot selection versus popup opening, plus quick mini-state restore/reset slot selection versus popup opening, plus quick mini-state restore/reset
validation used by the live quick panel before legacy `Brush`, color picker, validation used by the live quick panel before legacy `Brush`, color picker,
@@ -1272,7 +1273,8 @@ Results:
routing as JSON automation. routing as JSON automation.
- `pp_app_core_history_ui_tests` passed, covering undo/redo availability, - `pp_app_core_history_ui_tests` passed, covering undo/redo availability,
no-op history commands, clear-history stack/memory state, memory-only clear, no-op history commands, clear-history stack/memory state, memory-only clear,
and negative metric rejection. negative metric rejection, service dispatch order, empty-history no-op
execution, and invalid execution metric rejection.
- `pano_cli_plan_history_operation_undo_smoke`, - `pano_cli_plan_history_operation_undo_smoke`,
`pano_cli_plan_history_operation_redo_empty_smoke`, `pano_cli_plan_history_operation_redo_empty_smoke`,
`pano_cli_plan_history_operation_clear_smoke`, and `pano_cli_plan_history_operation_clear_smoke`, and

View File

@@ -23,6 +23,15 @@ struct HistoryUiPlan {
bool no_op = false; bool no_op = false;
}; };
class HistoryUiServices {
public:
virtual ~HistoryUiServices() = default;
virtual void invoke_undo() = 0;
virtual void invoke_redo() = 0;
virtual void clear_history() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept [[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
{ {
if (value < 0) { if (value < 0) {
@@ -107,4 +116,46 @@ struct HistoryUiPlan {
return pp::foundation::Result<HistoryUiPlan>::success(plan); return pp::foundation::Result<HistoryUiPlan>::success(plan);
} }
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
const HistoryUiPlan& plan,
HistoryUiServices& services)
{
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return undo_status;
}
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return redo_status;
}
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return memory_status;
}
if (plan.no_op) {
return pp::foundation::Status::success();
}
switch (plan.operation) {
case HistoryUiOperation::undo:
if (plan.invokes_undo) {
services.invoke_undo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::redo:
if (plan.invokes_redo) {
services.invoke_redo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::clear:
if (plan.clears_history) {
services.clear_history();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown history operation");
}
} // namespace pp::app } // namespace pp::app

View File

@@ -41,6 +41,7 @@ struct MainToolbarPlan {
bool records_undo = false; bool records_undo = false;
bool marks_unsaved = false; bool marks_unsaved = false;
bool no_op = false; bool no_op = false;
HistoryUiPlan history;
DocumentCanvasClearPlan canvas_clear; DocumentCanvasClearPlan canvas_clear;
}; };
@@ -50,9 +51,9 @@ public:
virtual void show_open_dialog() = 0; virtual void show_open_dialog() = 0;
virtual void show_save_dialog() = 0; virtual void show_save_dialog() = 0;
virtual void invoke_undo() = 0; virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
virtual void invoke_redo() = 0; virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
virtual void clear_history() = 0; virtual void clear_history(const HistoryUiPlan& plan) = 0;
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0; virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
virtual void show_message_box() = 0; virtual void show_message_box() = 0;
virtual void show_settings_dialog() = 0; virtual void show_settings_dialog() = 0;
@@ -92,6 +93,7 @@ public:
plan.updates_memory_label = history.value().updates_memory_label; plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title; plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op; plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan); return pp::foundation::Result<MainToolbarPlan>::success(plan);
} }
@@ -108,6 +110,7 @@ public:
plan.updates_memory_label = history.value().updates_memory_label; plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title; plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op; plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan); return pp::foundation::Result<MainToolbarPlan>::success(plan);
} }
@@ -123,6 +126,7 @@ public:
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)"; plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
plan.updates_memory_label = history.value().updates_memory_label; plan.updates_memory_label = history.value().updates_memory_label;
plan.no_op = history.value().no_op; plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan); return pp::foundation::Result<MainToolbarPlan>::success(plan);
} }
@@ -171,13 +175,13 @@ public:
services.show_save_dialog(); services.show_save_dialog();
return pp::foundation::Status::success(); return pp::foundation::Status::success();
case MainToolbarAction::invoke_undo: case MainToolbarAction::invoke_undo:
services.invoke_undo(); services.invoke_undo(plan.history);
return pp::foundation::Status::success(); return pp::foundation::Status::success();
case MainToolbarAction::invoke_redo: case MainToolbarAction::invoke_redo:
services.invoke_redo(); services.invoke_redo(plan.history);
return pp::foundation::Status::success(); return pp::foundation::Status::success();
case MainToolbarAction::clear_history: case MainToolbarAction::clear_history:
services.clear_history(); services.clear_history(plan.history);
return pp::foundation::Status::success(); return pp::foundation::Status::success();
case MainToolbarAction::clear_canvas: case MainToolbarAction::clear_canvas:
services.clear_canvas(plan.canvas_clear); services.clear_canvas(plan.canvas_clear);

View File

@@ -317,19 +317,19 @@ public:
app_.dialog_save(); app_.dialog_save();
} }
void invoke_undo() override void invoke_undo(const pp::app::HistoryUiPlan& plan) override
{ {
ActionManager::undo(); execute_history_plan(plan);
} }
void invoke_redo() override void invoke_redo(const pp::app::HistoryUiPlan& plan) override
{ {
ActionManager::redo(); execute_history_plan(plan);
} }
void clear_history() override void clear_history(const pp::app::HistoryUiPlan& plan) override
{ {
ActionManager::clear(); execute_history_plan(plan);
} }
void clear_canvas(const pp::app::DocumentCanvasClearPlan& plan) override void clear_canvas(const pp::app::DocumentCanvasClearPlan& plan) override
@@ -376,6 +376,32 @@ public:
} }
private: private:
class LegacyHistoryUiServices final : public pp::app::HistoryUiServices {
public:
void invoke_undo() override
{
ActionManager::undo();
}
void invoke_redo() override
{
ActionManager::redo();
}
void clear_history() override
{
ActionManager::clear();
}
};
void execute_history_plan(const pp::app::HistoryUiPlan& plan)
{
LegacyHistoryUiServices services;
const auto status = pp::app::execute_history_ui_plan(plan, services);
if (!status.ok())
LOG("History action failed: %s", status.message);
}
App& app_; App& app_;
}; };

View File

@@ -1,8 +1,36 @@
#include "app_core/history_ui.h" #include "app_core/history_ui.h"
#include "test_harness.h" #include "test_harness.h"
#include <string>
namespace { namespace {
class FakeHistoryUiServices final : public pp::app::HistoryUiServices {
public:
void invoke_undo() override
{
undo_calls += 1;
call_order += "undo;";
}
void invoke_redo() override
{
redo_calls += 1;
call_order += "redo;";
}
void clear_history() override
{
clear_calls += 1;
call_order += "clear;";
}
int undo_calls = 0;
int redo_calls = 0;
int clear_calls = 0;
std::string call_order;
};
void undo_and_redo_plan_availability(pp::tests::Harness& harness) void undo_and_redo_plan_availability(pp::tests::Harness& harness)
{ {
const auto undo = pp::app::plan_history_undo(2); const auto undo = pp::app::plan_history_undo(2);
@@ -78,6 +106,76 @@ void rejects_negative_metrics(pp::tests::Harness& harness)
PP_EXPECT(harness, !pp::app::plan_history_clear(0, 0, -1)); PP_EXPECT(harness, !pp::app::plan_history_clear(0, 0, -1));
} }
void executor_dispatches_undo_redo_and_clear(pp::tests::Harness& harness)
{
FakeHistoryUiServices services;
const auto undo = pp::app::plan_history_undo(2);
PP_EXPECT(harness, undo);
if (undo) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(undo.value(), services).ok());
}
const auto redo = pp::app::plan_history_redo(1);
PP_EXPECT(harness, redo);
if (redo) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(redo.value(), services).ok());
}
const auto clear = pp::app::plan_history_clear(2, 1, 4096);
PP_EXPECT(harness, clear);
if (clear) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(clear.value(), services).ok());
}
PP_EXPECT(harness, services.undo_calls == 1);
PP_EXPECT(harness, services.redo_calls == 1);
PP_EXPECT(harness, services.clear_calls == 1);
PP_EXPECT(harness, services.call_order == "undo;redo;clear;");
}
void executor_preserves_empty_history_noops(pp::tests::Harness& harness)
{
FakeHistoryUiServices services;
const auto undo = pp::app::plan_history_undo(0);
PP_EXPECT(harness, undo);
if (undo) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(undo.value(), services).ok());
}
const auto redo = pp::app::plan_history_redo(0);
PP_EXPECT(harness, redo);
if (redo) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(redo.value(), services).ok());
}
const auto clear = pp::app::plan_history_clear(0, 0, 0);
PP_EXPECT(harness, clear);
if (clear) {
PP_EXPECT(harness, pp::app::execute_history_ui_plan(clear.value(), services).ok());
}
PP_EXPECT(harness, services.undo_calls == 0);
PP_EXPECT(harness, services.redo_calls == 0);
PP_EXPECT(harness, services.clear_calls == 0);
PP_EXPECT(harness, services.call_order.empty());
}
void executor_rejects_invalid_metrics(pp::tests::Harness& harness)
{
FakeHistoryUiServices services;
pp::app::HistoryUiPlan plan;
plan.operation = pp::app::HistoryUiOperation::undo;
plan.undo_count = -1;
plan.invokes_undo = true;
const auto status = pp::app::execute_history_ui_plan(plan, services);
PP_EXPECT(harness, !status.ok());
PP_EXPECT(harness, status.code == pp::foundation::StatusCode::out_of_range);
PP_EXPECT(harness, services.undo_calls == 0);
}
} // namespace } // namespace
int main() int main()
@@ -86,5 +184,8 @@ int main()
harness.run("undo and redo plan availability", undo_and_redo_plan_availability); harness.run("undo and redo plan availability", undo_and_redo_plan_availability);
harness.run("clear plan tracks stacks and memory", clear_plan_tracks_stacks_and_memory); harness.run("clear plan tracks stacks and memory", clear_plan_tracks_stacks_and_memory);
harness.run("rejects negative metrics", rejects_negative_metrics); harness.run("rejects negative metrics", rejects_negative_metrics);
harness.run("executor dispatches undo redo and clear", executor_dispatches_undo_redo_and_clear);
harness.run("executor preserves empty history noops", executor_preserves_empty_history_noops);
harness.run("executor rejects invalid metrics", executor_rejects_invalid_metrics);
return harness.finish(); return harness.finish();
} }

View File

@@ -7,9 +7,24 @@ class FakeMainToolbarServices final : public pp::app::MainToolbarServices {
public: public:
void show_open_dialog() override { open_dialogs += 1; } void show_open_dialog() override { open_dialogs += 1; }
void show_save_dialog() override { save_dialogs += 1; } void show_save_dialog() override { save_dialogs += 1; }
void invoke_undo() override { undo_calls += 1; } void invoke_undo(const pp::app::HistoryUiPlan& plan) override
void invoke_redo() override { redo_calls += 1; } {
void clear_history() override { clear_history_calls += 1; } undo_calls += 1;
last_history = plan;
}
void invoke_redo(const pp::app::HistoryUiPlan& plan) override
{
redo_calls += 1;
last_history = plan;
}
void clear_history(const pp::app::HistoryUiPlan& plan) override
{
clear_history_calls += 1;
last_history = plan;
}
void clear_canvas(const pp::app::DocumentCanvasClearPlan& plan) override void clear_canvas(const pp::app::DocumentCanvasClearPlan& plan) override
{ {
clear_canvas_calls += 1; clear_canvas_calls += 1;
@@ -38,6 +53,7 @@ public:
int clear_canvas_calls = 0; int clear_canvas_calls = 0;
int message_boxes = 0; int message_boxes = 0;
int settings_dialogs = 0; int settings_dialogs = 0;
pp::app::HistoryUiPlan last_history;
pp::app::DocumentCanvasClearPlan last_clear; pp::app::DocumentCanvasClearPlan last_clear;
}; };
@@ -74,6 +90,8 @@ void history_commands_reuse_history_breakpoints(pp::tests::Harness& harness)
PP_EXPECT(harness, undo.value().action == pp::app::MainToolbarAction::invoke_undo); PP_EXPECT(harness, undo.value().action == pp::app::MainToolbarAction::invoke_undo);
PP_EXPECT(harness, undo.value().updates_memory_label); PP_EXPECT(harness, undo.value().updates_memory_label);
PP_EXPECT(harness, undo.value().updates_title); PP_EXPECT(harness, undo.value().updates_title);
PP_EXPECT(harness, undo.value().history.operation == pp::app::HistoryUiOperation::undo);
PP_EXPECT(harness, undo.value().history.invokes_undo);
PP_EXPECT(harness, !undo.value().no_op); PP_EXPECT(harness, !undo.value().no_op);
} }
@@ -95,6 +113,8 @@ void history_commands_reuse_history_breakpoints(pp::tests::Harness& harness)
PP_EXPECT(harness, clear.value().action == pp::app::MainToolbarAction::clear_history); PP_EXPECT(harness, clear.value().action == pp::app::MainToolbarAction::clear_history);
PP_EXPECT(harness, clear.value().updates_memory_label); PP_EXPECT(harness, clear.value().updates_memory_label);
PP_EXPECT(harness, !clear.value().updates_title); PP_EXPECT(harness, !clear.value().updates_title);
PP_EXPECT(harness, clear.value().history.operation == pp::app::HistoryUiOperation::clear);
PP_EXPECT(harness, clear.value().history.clears_history);
} }
} }
@@ -161,6 +181,8 @@ void executor_dispatches_to_service_boundary(pp::tests::Harness& harness)
const auto status = pp::app::execute_main_toolbar_plan(undo.value(), services); const auto status = pp::app::execute_main_toolbar_plan(undo.value(), services);
PP_EXPECT(harness, status.ok()); PP_EXPECT(harness, status.ok());
PP_EXPECT(harness, services.undo_calls == 1); PP_EXPECT(harness, services.undo_calls == 1);
PP_EXPECT(harness, services.last_history.operation == pp::app::HistoryUiOperation::undo);
PP_EXPECT(harness, services.last_history.undo_count == 1);
} }
auto clear_canvas = pp::app::plan_main_toolbar_command( auto clear_canvas = pp::app::plan_main_toolbar_command(