From e880f23040f3249b7d9c998c96aefdb4cd135fa6 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 13:00:22 +0200 Subject: [PATCH] Add file menu service boundary --- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 8 +- src/app_core/file_menu.h | 60 +++++++++++++ src/app_layout.cpp | 106 ++++++++++++++--------- tests/app_core/file_menu_tests.cpp | 132 +++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+), 42 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b7b29df..241cac7 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -48,7 +48,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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-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 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 | | 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 | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 888816a..651ae46 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -522,8 +522,9 @@ legacy `Canvas::import_equirectangular` or import transform-mode execution continues. `pano_cli plan-file-menu` exposes app-core planning for the top-level File menu commands, including new/open/import, save/save-as/save-version, share, resize, -cloud upload/browse, JPEG export, and export-submenu routing before legacy -dialogs, pickers, platform services, cloud code, and canvas workflows continue. +cloud upload/browse, JPEG export, and export-submenu routing. Direct File menu +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 @@ -1245,7 +1246,8 @@ Results: > Import route planning as JSON automation. - `pp_app_core_file_menu_tests` passed, covering top-level File menu routing for creation/open/import, save intents, export/submenu/cloud actions, and unknown - command rejection. + command rejection, plus executor dispatch for dialog, picker, save, export, + share, resize, and cloud actions. - `pano_cli_plan_file_menu_import_smoke`, `pano_cli_plan_file_menu_save_as_smoke`, `pano_cli_plan_file_menu_export_smoke`, diff --git a/src/app_core/file_menu.h b/src/app_core/file_menu.h index 648a31c..e2d794d 100644 --- a/src/app_core/file_menu.h +++ b/src/app_core/file_menu.h @@ -45,6 +45,23 @@ struct FileMenuPlan { DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg; }; +class FileMenuServices { +public: + virtual ~FileMenuServices() = default; + + virtual void show_new_document_dialog() = 0; + virtual void pick_image_for_import() = 0; + virtual void pick_project_file() = 0; + virtual void show_cloud_browser_dialog() = 0; + virtual void save_document(DocumentSaveIntent intent) = 0; + virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0; + virtual void show_export_submenu() = 0; + virtual void share_document() = 0; + virtual void show_resize_dialog() = 0; + virtual void upload_to_cloud() = 0; + virtual void browse_cloud_documents() = 0; +}; + [[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept { FileMenuPlan plan; @@ -146,4 +163,47 @@ struct FileMenuPlan { pp::foundation::Status::invalid_argument("unknown file menu command")); } +[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan( + const FileMenuPlan& plan, + FileMenuServices& services) +{ + switch (plan.action) { + case FileMenuAction::show_new_document_dialog: + services.show_new_document_dialog(); + return pp::foundation::Status::success(); + case FileMenuAction::pick_image_for_import: + services.pick_image_for_import(); + return pp::foundation::Status::success(); + case FileMenuAction::pick_project_file: + services.pick_project_file(); + return pp::foundation::Status::success(); + case FileMenuAction::show_cloud_browser_dialog: + services.show_cloud_browser_dialog(); + return pp::foundation::Status::success(); + case FileMenuAction::save_document: + services.save_document(plan.save_intent); + return pp::foundation::Status::success(); + case FileMenuAction::show_export_jpeg_dialog: + services.show_export_jpeg_dialog(plan.export_kind); + return pp::foundation::Status::success(); + case FileMenuAction::show_export_submenu: + services.show_export_submenu(); + return pp::foundation::Status::success(); + case FileMenuAction::share_document: + services.share_document(); + return pp::foundation::Status::success(); + case FileMenuAction::show_resize_dialog: + services.show_resize_dialog(); + return pp::foundation::Status::success(); + case FileMenuAction::upload_to_cloud: + services.upload_to_cloud(); + return pp::foundation::Status::success(); + case FileMenuAction::browse_cloud_documents: + services.browse_cloud_documents(); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown file menu action"); +} + } // namespace pp::app diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 624b2af..2b84147 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -127,18 +127,22 @@ bool apply_document_export_menu_plan(App& app, pp::app::DocumentExportMenuKind k return false; } -void apply_file_menu_plan(App& app, pp::app::FileMenuCommand command) -{ - const auto plan = pp::app::plan_file_menu_command(command); - switch (plan.action) +class LegacyFileMenuServices final : public pp::app::FileMenuServices { +public: + explicit LegacyFileMenuServices(App& app) noexcept + : app_(app) { - case pp::app::FileMenuAction::show_new_document_dialog: - app.dialog_newdoc(); - break; - case pp::app::FileMenuAction::pick_image_for_import: + } + + void show_new_document_dialog() override { - auto* app_ptr = &app; - app.pick_image([app_ptr](std::string path) { + app_.dialog_newdoc(); + } + + void pick_image_for_import() override + { + auto* app_ptr = &app_; + app_.pick_image([app_ptr](std::string path) { Image img; img.load_file(path); const auto import_plan = pp::app::plan_document_image_import(img.width, img.height); @@ -157,40 +161,66 @@ void apply_file_menu_plan(App& app, pp::app::FileMenuCommand command) Canvas::set_mode(kCanvasMode::Import); } }); - break; } - case pp::app::FileMenuAction::pick_project_file: + + void pick_project_file() override { - auto* app_ptr = &app; - app.pick_file({ "ppi" }, [app_ptr](std::string path) { + auto* app_ptr = &app_; + app_.pick_file({ "ppi" }, [app_ptr](std::string path) { app_ptr->open_document(path); }); - break; } - case pp::app::FileMenuAction::show_cloud_browser_dialog: - app.dialog_browse(); - break; - case pp::app::FileMenuAction::save_document: - app.save_document(plan.save_intent); - break; - case pp::app::FileMenuAction::show_export_jpeg_dialog: - apply_document_export_menu_plan(app, plan.export_kind); - break; - case pp::app::FileMenuAction::show_export_submenu: - break; - case pp::app::FileMenuAction::share_document: - app.share_file(app.doc_path); - break; - case pp::app::FileMenuAction::show_resize_dialog: - app.dialog_resize(); - break; - case pp::app::FileMenuAction::upload_to_cloud: - app.cloud_upload(); - break; - case pp::app::FileMenuAction::browse_cloud_documents: - app.cloud_browse(); - break; + + void show_cloud_browser_dialog() override + { + app_.dialog_browse(); } + + void save_document(pp::app::DocumentSaveIntent intent) override + { + app_.save_document(intent); + } + + void show_export_jpeg_dialog(pp::app::DocumentExportMenuKind kind) override + { + apply_document_export_menu_plan(app_, kind); + } + + void show_export_submenu() override + { + } + + void share_document() override + { + app_.share_file(app_.doc_path); + } + + void show_resize_dialog() override + { + app_.dialog_resize(); + } + + void upload_to_cloud() override + { + app_.cloud_upload(); + } + + void browse_cloud_documents() override + { + app_.cloud_browse(); + } + +private: + App& app_; +}; + +void apply_file_menu_plan(App& app, pp::app::FileMenuCommand command) +{ + const auto plan = pp::app::plan_file_menu_command(command); + LegacyFileMenuServices services(app); + const auto status = pp::app::execute_file_menu_plan(plan, services); + if (!status.ok()) + LOG("File menu action failed: %s", status.message); } pp::app::DocumentLayerMenuPlan make_layer_menu_plan( diff --git a/tests/app_core/file_menu_tests.cpp b/tests/app_core/file_menu_tests.cpp index f919d0e..2bafbbf 100644 --- a/tests/app_core/file_menu_tests.cpp +++ b/tests/app_core/file_menu_tests.cpp @@ -3,6 +3,43 @@ namespace { +class FakeFileMenuServices final : public pp::app::FileMenuServices { +public: + void show_new_document_dialog() override { new_document_dialogs += 1; } + void pick_image_for_import() override { image_picks += 1; } + void pick_project_file() override { project_picks += 1; } + void show_cloud_browser_dialog() override { cloud_browser_dialogs += 1; } + void save_document(pp::app::DocumentSaveIntent intent) override + { + save_calls += 1; + last_save_intent = intent; + } + void show_export_jpeg_dialog(pp::app::DocumentExportMenuKind kind) override + { + export_jpeg_dialogs += 1; + last_export_kind = kind; + } + void show_export_submenu() override { export_submenus += 1; } + void share_document() override { share_calls += 1; } + void show_resize_dialog() override { resize_dialogs += 1; } + void upload_to_cloud() override { cloud_uploads += 1; } + void browse_cloud_documents() override { cloud_browses += 1; } + + int new_document_dialogs = 0; + int image_picks = 0; + int project_picks = 0; + int cloud_browser_dialogs = 0; + int save_calls = 0; + int export_jpeg_dialogs = 0; + int export_submenus = 0; + int share_calls = 0; + int resize_dialogs = 0; + int cloud_uploads = 0; + int cloud_browses = 0; + pp::app::DocumentSaveIntent last_save_intent = pp::app::DocumentSaveIntent::save; + pp::app::DocumentExportMenuKind last_export_kind = pp::app::DocumentExportMenuKind::jpeg; +}; + void file_menu_routes_document_creation_and_opening(pp::tests::Harness& harness) { const auto new_doc = pp::app::plan_file_menu_command(pp::app::FileMenuCommand::new_document); @@ -57,6 +94,95 @@ void file_menu_parser_accepts_legacy_button_names(pp::tests::Harness& harness) PP_EXPECT(harness, !invalid); } +void file_menu_executor_dispatches_dialog_picker_and_document_actions(pp::tests::Harness& harness) +{ + FakeFileMenuServices services; + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::new_document), + services).ok()); + PP_EXPECT(harness, services.new_document_dialogs == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::import_image), + services).ok()); + PP_EXPECT(harness, services.image_picks == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::open_project), + services).ok()); + PP_EXPECT(harness, services.project_picks == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::save_as), + services).ok()); + PP_EXPECT(harness, services.save_calls == 1); + PP_EXPECT(harness, services.last_save_intent == pp::app::DocumentSaveIntent::save_as); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::share), + services).ok()); + PP_EXPECT(harness, services.share_calls == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::resize), + services).ok()); + PP_EXPECT(harness, services.resize_dialogs == 1); +} + +void file_menu_executor_dispatches_export_and_cloud_actions(pp::tests::Harness& harness) +{ + FakeFileMenuServices services; + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::export_jpeg), + services).ok()); + PP_EXPECT(harness, services.export_jpeg_dialogs == 1); + PP_EXPECT(harness, services.last_export_kind == pp::app::DocumentExportMenuKind::jpeg); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::export_submenu), + services).ok()); + PP_EXPECT(harness, services.export_submenus == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::browse_cloud), + services).ok()); + PP_EXPECT(harness, services.cloud_browser_dialogs == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::cloud_upload), + services).ok()); + PP_EXPECT(harness, services.cloud_uploads == 1); + + PP_EXPECT( + harness, + pp::app::execute_file_menu_plan( + pp::app::plan_file_menu_command(pp::app::FileMenuCommand::cloud_browse), + services).ok()); + PP_EXPECT(harness, services.cloud_browses == 1); +} + } int main() @@ -66,5 +192,11 @@ int main() harness.run("file menu preserves save intents", file_menu_preserves_save_intents); harness.run("file menu routes export and cloud actions", file_menu_routes_export_and_cloud_actions); harness.run("file menu parser accepts legacy button names", file_menu_parser_accepts_legacy_button_names); + harness.run( + "file menu executor dispatches dialog picker and document actions", + file_menu_executor_dispatches_dialog_picker_and_document_actions); + harness.run( + "file menu executor dispatches export and cloud actions", + file_menu_executor_dispatches_export_and_cloud_actions); return harness.finish(); }