Add file menu service boundary

This commit is contained in:
2026-06-03 13:00:22 +02:00
parent defa9fc212
commit e880f23040
5 changed files with 266 additions and 42 deletions

View File

@@ -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 |

View File

@@ -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`,

View File

@@ -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

View File

@@ -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(

View File

@@ -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();
}