Add export menu service boundary
This commit is contained in:
@@ -47,7 +47,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| 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 now consumes pure `pp_app_core` through `App::init_toolbar_main` and `pano_cli plan-canvas-clear`, but live execution still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved directly | 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 document/app services with toolbar callbacks acting only as adapters |
|
||||
| DEBT-0029 | Open | Modernization | Image import route planning now consumes pure `pp_app_core` through the File menu and `pano_cli plan-image-import`, but live execution still calls legacy `Canvas::import_equirectangular` or legacy import transform mode setup directly after image loading | 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 document/app/asset 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-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, but the live adapter 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 |
|
||||
| DEBT-0031 | Open | Modernization | Top-level File menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_file`, `pano_cli plan-file-menu`, and the `FileMenuServices` boundary, but the live adapter 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 injected app/document/platform services with `App::init_menu_file` acting only as a UI adapter and no legacy File 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 |
|
||||
|
||||
@@ -527,8 +527,9 @@ commands now dispatch through `FileMenuServices` before legacy dialogs, pickers,
|
||||
platform services, cloud code, and canvas workflows continue.
|
||||
`pano_cli plan-export-menu` exposes app-core planning for File menu export
|
||||
choices, including image, layer, cube-face, depth, animation-frame, MP4, and
|
||||
timelapse dialog routing plus license/canvas gating before legacy export dialogs
|
||||
and renderer/video execution continue.
|
||||
timelapse dialog routing plus license/canvas gating. Export menu commands now
|
||||
dispatch through `DocumentExportMenuServices` before legacy export dialogs and
|
||||
renderer/video execution continue.
|
||||
`pano_cli plan-grid-operation` exposes app-core planning for grid heightmap
|
||||
pick/load/reload/clear, lightmap render capability/limit checks, and heightmap
|
||||
commit used by the live grid panel before legacy image loading, OpenGL texture
|
||||
@@ -1256,7 +1257,8 @@ Results:
|
||||
menu routing as JSON automation.
|
||||
- `pp_app_core_document_export_tests` passed, now also covering export menu
|
||||
dialog routing, demo-mode MP4/timelapse license gating, and missing-canvas
|
||||
handling before legacy export dialogs continue.
|
||||
handling, plus export menu executor dispatch for all dialog, blocked, and
|
||||
unavailable actions before legacy export dialogs continue.
|
||||
- `pano_cli_plan_export_menu_png_smoke`,
|
||||
`pano_cli_plan_export_menu_mp4_demo_blocked_smoke`,
|
||||
`pano_cli_plan_export_menu_no_canvas_smoke`, and
|
||||
|
||||
@@ -62,6 +62,21 @@ struct DocumentExportMenuPlan {
|
||||
bool opens_dialog = true;
|
||||
};
|
||||
|
||||
class DocumentExportMenuServices {
|
||||
public:
|
||||
virtual ~DocumentExportMenuServices() = default;
|
||||
|
||||
virtual void show_jpeg_dialog() = 0;
|
||||
virtual void show_png_dialog() = 0;
|
||||
virtual void show_layers_dialog() = 0;
|
||||
virtual void show_cube_faces_dialog() = 0;
|
||||
virtual void show_depth_dialog() = 0;
|
||||
virtual void show_animation_frames_dialog() = 0;
|
||||
virtual void show_animation_mp4_dialog() = 0;
|
||||
virtual void show_timelapse_dialog() = 0;
|
||||
virtual void show_license_disabled() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
|
||||
bool requires_license,
|
||||
bool license_valid,
|
||||
@@ -226,4 +241,43 @@ struct DocumentExportMenuPlan {
|
||||
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan(
|
||||
const DocumentExportMenuPlan& plan,
|
||||
DocumentExportMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case DocumentExportMenuAction::show_jpeg_dialog:
|
||||
services.show_jpeg_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_png_dialog:
|
||||
services.show_png_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_layers_dialog:
|
||||
services.show_layers_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_cube_faces_dialog:
|
||||
services.show_cube_faces_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_depth_dialog:
|
||||
services.show_depth_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_animation_frames_dialog:
|
||||
services.show_animation_frames_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_animation_mp4_dialog:
|
||||
services.show_animation_mp4_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_timelapse_dialog:
|
||||
services.show_timelapse_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::show_license_disabled:
|
||||
services.show_license_disabled();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportMenuAction::unavailable_no_canvas:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document export menu action");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,46 +85,40 @@ bool apply_brush_preset_plan(App& app, const std::shared_ptr<Brush>& brush)
|
||||
|
||||
bool apply_document_export_menu_plan(App& app, pp::app::DocumentExportMenuKind kind)
|
||||
{
|
||||
class LegacyDocumentExportMenuServices final : public pp::app::DocumentExportMenuServices {
|
||||
public:
|
||||
explicit LegacyDocumentExportMenuServices(App& app) noexcept
|
||||
: app_(app)
|
||||
{
|
||||
}
|
||||
|
||||
void show_jpeg_dialog() override { app_.dialog_export(".jpg"); }
|
||||
void show_png_dialog() override { app_.dialog_export(".png"); }
|
||||
void show_layers_dialog() override { app_.dialog_export_layers(); }
|
||||
void show_cube_faces_dialog() override { app_.dialog_export_cube_faces(); }
|
||||
void show_depth_dialog() override { app_.dialog_export_depth(); }
|
||||
void show_animation_frames_dialog() override { app_.dialog_export_anim_frames(); }
|
||||
void show_animation_mp4_dialog() override { app_.dialog_export_mp4(); }
|
||||
void show_timelapse_dialog() override { app_.dialog_timelapse_export(); }
|
||||
void show_license_disabled() override
|
||||
{
|
||||
app_.message_box("License", "This function is disabled in demo mode.");
|
||||
}
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
};
|
||||
|
||||
const auto requires_license = pp::app::document_export_menu_requires_license(kind);
|
||||
const auto plan = pp::app::plan_document_export_menu_action(
|
||||
kind,
|
||||
app.canvas != nullptr,
|
||||
!requires_license || app.check_license());
|
||||
|
||||
switch (plan.action)
|
||||
{
|
||||
case pp::app::DocumentExportMenuAction::show_jpeg_dialog:
|
||||
app.dialog_export(".jpg");
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_png_dialog:
|
||||
app.dialog_export(".png");
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_layers_dialog:
|
||||
app.dialog_export_layers();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_cube_faces_dialog:
|
||||
app.dialog_export_cube_faces();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_depth_dialog:
|
||||
app.dialog_export_depth();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_animation_frames_dialog:
|
||||
app.dialog_export_anim_frames();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_animation_mp4_dialog:
|
||||
app.dialog_export_mp4();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_timelapse_dialog:
|
||||
app.dialog_timelapse_export();
|
||||
return true;
|
||||
case pp::app::DocumentExportMenuAction::show_license_disabled:
|
||||
app.message_box("License", "This function is disabled in demo mode.");
|
||||
return false;
|
||||
case pp::app::DocumentExportMenuAction::unavailable_no_canvas:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
LegacyDocumentExportMenuServices services(app);
|
||||
const auto status = pp::app::execute_document_export_menu_plan(plan, services);
|
||||
if (!status.ok())
|
||||
LOG("Document export menu action failed: %s", status.message);
|
||||
return status.ok() && plan.opens_dialog;
|
||||
}
|
||||
|
||||
class LegacyFileMenuServices final : public pp::app::FileMenuServices {
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
|
||||
namespace {
|
||||
|
||||
class FakeDocumentExportMenuServices final : public pp::app::DocumentExportMenuServices {
|
||||
public:
|
||||
void show_jpeg_dialog() override { jpeg_dialogs += 1; }
|
||||
void show_png_dialog() override { png_dialogs += 1; }
|
||||
void show_layers_dialog() override { layer_dialogs += 1; }
|
||||
void show_cube_faces_dialog() override { cube_dialogs += 1; }
|
||||
void show_depth_dialog() override { depth_dialogs += 1; }
|
||||
void show_animation_frames_dialog() override { animation_frame_dialogs += 1; }
|
||||
void show_animation_mp4_dialog() override { animation_mp4_dialogs += 1; }
|
||||
void show_timelapse_dialog() override { timelapse_dialogs += 1; }
|
||||
void show_license_disabled() override { license_messages += 1; }
|
||||
|
||||
[[nodiscard]] int total_calls() const noexcept
|
||||
{
|
||||
return jpeg_dialogs + png_dialogs + layer_dialogs + cube_dialogs + depth_dialogs
|
||||
+ animation_frame_dialogs + animation_mp4_dialogs + timelapse_dialogs
|
||||
+ license_messages;
|
||||
}
|
||||
|
||||
int jpeg_dialogs = 0;
|
||||
int png_dialogs = 0;
|
||||
int layer_dialogs = 0;
|
||||
int cube_dialogs = 0;
|
||||
int depth_dialogs = 0;
|
||||
int animation_frame_dialogs = 0;
|
||||
int animation_mp4_dialogs = 0;
|
||||
int timelapse_dialogs = 0;
|
||||
int license_messages = 0;
|
||||
};
|
||||
|
||||
void equirectangular_export_builds_file_target(pp::tests::Harness& harness)
|
||||
{
|
||||
const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
|
||||
@@ -123,6 +153,106 @@ void export_menu_reports_missing_canvas_for_unlicensed_image_exports(pp::tests::
|
||||
PP_EXPECT(harness, !plan.opens_dialog);
|
||||
}
|
||||
|
||||
void export_menu_executor_dispatches_all_dialog_actions(pp::tests::Harness& harness)
|
||||
{
|
||||
FakeDocumentExportMenuServices services;
|
||||
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::jpeg,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::png,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::layers,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::cube_faces,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::depth,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::animation_frames,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::animation_mp4,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::execute_document_export_menu_plan(
|
||||
pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::timelapse,
|
||||
true,
|
||||
true),
|
||||
services).ok());
|
||||
|
||||
PP_EXPECT(harness, services.jpeg_dialogs == 1);
|
||||
PP_EXPECT(harness, services.png_dialogs == 1);
|
||||
PP_EXPECT(harness, services.layer_dialogs == 1);
|
||||
PP_EXPECT(harness, services.cube_dialogs == 1);
|
||||
PP_EXPECT(harness, services.depth_dialogs == 1);
|
||||
PP_EXPECT(harness, services.animation_frame_dialogs == 1);
|
||||
PP_EXPECT(harness, services.animation_mp4_dialogs == 1);
|
||||
PP_EXPECT(harness, services.timelapse_dialogs == 1);
|
||||
}
|
||||
|
||||
void export_menu_executor_preserves_blocked_and_unavailable_actions(pp::tests::Harness& harness)
|
||||
{
|
||||
FakeDocumentExportMenuServices services;
|
||||
|
||||
const auto blocked = pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::animation_mp4,
|
||||
true,
|
||||
false);
|
||||
PP_EXPECT(harness, blocked.action == pp::app::DocumentExportMenuAction::show_license_disabled);
|
||||
PP_EXPECT(harness, pp::app::execute_document_export_menu_plan(blocked, services).ok());
|
||||
PP_EXPECT(harness, services.license_messages == 1);
|
||||
|
||||
const auto unavailable = pp::app::plan_document_export_menu_action(
|
||||
pp::app::DocumentExportMenuKind::png,
|
||||
false,
|
||||
true);
|
||||
PP_EXPECT(harness, unavailable.action == pp::app::DocumentExportMenuAction::unavailable_no_canvas);
|
||||
PP_EXPECT(harness, pp::app::execute_document_export_menu_plan(unavailable, services).ok());
|
||||
PP_EXPECT(harness, services.total_calls() == 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -139,5 +269,7 @@ int main()
|
||||
harness.run("export menu routes image dialogs without license", export_menu_routes_image_dialogs_without_license);
|
||||
harness.run("export menu blocks video when license is missing", export_menu_blocks_video_when_license_is_missing);
|
||||
harness.run("export menu reports missing canvas for unlicensed image exports", export_menu_reports_missing_canvas_for_unlicensed_image_exports);
|
||||
harness.run("export menu executor dispatches all dialog actions", export_menu_executor_dispatches_all_dialog_actions);
|
||||
harness.run("export menu executor preserves blocked and unavailable actions", export_menu_executor_preserves_blocked_and_unavailable_actions);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user