diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 30a8dd1..5539c1c 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -169,10 +169,10 @@ Known local toolchain state: `DEBT-0035`. - `src/legacy_app_preference_services.*` is the current app-shell bridge for options-menu preference execution. It keeps UI scale, viewport scale, RTL, - VR-controller, auto-timelapse, and canvas cursor-mode callbacks on the - `pp_app_core` `AppPreferenceServices` contract while retained settings - persistence, recording lifecycle, and legacy canvas/UI execution remain - tracked by `DEBT-0045`. + VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks on + the `pp_app_core` `AppPreferenceServices` contract while retained settings + persistence, VR start/stop, recording lifecycle, and legacy canvas/UI + execution remain tracked by `DEBT-0045`. - `src/legacy_canvas_tool_services.*` is the current app-shell bridge for canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core` @@ -584,8 +584,9 @@ Known local toolchain state: planning before platform clipboard callbacks. - `pp_app_core_app_preferences_tests` covers UI scale/font-scale planning, scale-option selection, viewport scale planning, RTL direction planning, - timelapse start/stop/no-op decisions, simple stored preferences, and - `AppPreferenceServices` execution dispatch for options-menu side effects. + timelapse start/stop/no-op decisions, VR mode success/failure dispatch, + simple stored preferences, and `AppPreferenceServices` execution dispatch for + options-menu side effects. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, external file display, file sharing, and picker callbacks without platform diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 7656b69..32ff59f 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -62,7 +62,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy overwrite prompts, calls legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters | | DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | -| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`, but the bridge still calls legacy `App::set_ui_scale`, `App::set_ui_rtl`, `NodeCanvas::set_density`, `NodeCanvas::set_cursor_visibility`, `App::rec_start`, `App::rec_stop`, and `Settings::save` directly | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | +| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`, but the bridge still calls legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::vr_start`, `App::vr_stop`, `NodeCanvas::set_density`, `NodeCanvas::set_cursor_visibility`, `App::rec_start`, `App::rec_stop`, and `Settings::save` directly | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 3d01db0..b39acfd 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -177,7 +177,7 @@ project-open, app-close, save, save-as, and save-version flows; contracts while legacy canvas/project loading remains in place. `pp_app_core` also owns tested app preference plans for UI scale/font scale, scale option selection, viewport scale, RTL layout direction, timelapse -recording toggles, VR controller enablement, and canvas cursor mode; +recording toggles, VR mode start/stop, VR controller enablement, and canvas cursor mode; the live tools/options menu and `pano_cli plan-app-preferences` consume those contracts. Options-menu preference execution now dispatches through `AppPreferenceServices` and `src/legacy_app_preference_services.*` before @@ -606,7 +606,7 @@ legacy UI/panel/canvas/platform adapters continue execution. The live animation panel route now also checks animation panel visibility and applies animation panel layout state instead of using the grid panel by mistake. Options-menu preference callbacks now dispatch UI scale, viewport scale, RTL, -VR-controller, auto-timelapse, and cursor-mode side effects through +VR mode, VR-controller, auto-timelapse, and cursor-mode side effects through `AppPreferenceServices` in `src/legacy_app_preference_services.*` before retained settings writes, recording lifecycle calls, and legacy canvas/UI adapters continue. @@ -1357,7 +1357,7 @@ Results: after options-menu preference execution moved behind app preference services. - Focused preference CTest coverage passed for `pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests - after the live bridge split. + after the live bridge split, including VR mode failed-start status coverage. - `pp_app_core_document_recording_tests` passed, covering recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, and oversized progress-total clamping. diff --git a/src/app_core/app_preferences.h b/src/app_core/app_preferences.h index 566610c..0b29914 100644 --- a/src/app_core/app_preferences.h +++ b/src/app_core/app_preferences.h @@ -53,6 +53,7 @@ public: virtual void apply_ui_scale(const ScaleApplicationPlan& plan) = 0; virtual void apply_viewport_scale(const ScaleApplicationPlan& plan) = 0; virtual void apply_interface_direction(const InterfaceDirectionPlan& plan) = 0; + virtual bool apply_vr_mode_preference(const StoredBooleanPreferencePlan& plan) = 0; virtual void apply_vr_controllers_preference(const StoredBooleanPreferencePlan& plan) = 0; virtual void apply_timelapse_preference(const TimelapsePreferencePlan& plan) = 0; virtual void apply_canvas_cursor_mode(const StoredIntegerPreferencePlan& plan) = 0; @@ -120,6 +121,11 @@ public: return { enabled }; } +[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_mode_preference(bool enabled) noexcept +{ + return { enabled }; +} + [[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept { return { mode }; @@ -151,6 +157,16 @@ public: return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Status execute_vr_mode_preference( + bool enabled, + AppPreferenceServices& services) +{ + if (!services.apply_vr_mode_preference(plan_vr_mode_preference(enabled))) { + return pp::foundation::Status::invalid_argument("VR mode could not start"); + } + return pp::foundation::Status::success(); +} + [[nodiscard]] inline pp::foundation::Status execute_vr_controllers_preference( bool enabled, AppPreferenceServices& services) diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 359078b..589bca7 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -1159,18 +1159,13 @@ void App::init_menu_tools() vr_btn->find("tools-vr-check")->on_value_changed = [this, main](Node* target, bool checked) { - if (checked) - { - if (!vr_start()) - { - auto cb = static_cast(target); - cb->set_value(false); - message_box("VR Failed", "Couldn't start Virtual Reality mode"); - } - } - else - { - vr_stop(); + const auto status = pp::panopainter::execute_legacy_vr_mode_preference( + *this, + checked); + if (!status.ok()) { + auto cb = static_cast(target); + cb->set_value(false); + message_box("VR Failed", "Couldn't start Virtual Reality mode"); } }; } diff --git a/src/legacy_app_preference_services.cpp b/src/legacy_app_preference_services.cpp index f06cfba..2d58972 100644 --- a/src/legacy_app_preference_services.cpp +++ b/src/legacy_app_preference_services.cpp @@ -37,6 +37,16 @@ public: app_.set_ui_rtl(plan.direction == pp::app::InterfaceDirection::right_to_left); } + bool apply_vr_mode_preference(const pp::app::StoredBooleanPreferencePlan& plan) override + { + if (plan.value) { + return app_.vr_start(); + } + + app_.vr_stop(); + return true; + } + void apply_vr_controllers_preference(const pp::app::StoredBooleanPreferencePlan& plan) override { app_.vr_controllers_enabled = plan.value; @@ -90,6 +100,12 @@ pp::foundation::Status execute_legacy_interface_direction_preference(App& app, b return pp::app::execute_interface_direction_preference(right_to_left, services); } +pp::foundation::Status execute_legacy_vr_mode_preference(App& app, bool enabled) +{ + LegacyAppPreferenceServices services(app); + return pp::app::execute_vr_mode_preference(enabled, services); +} + pp::foundation::Status execute_legacy_vr_controllers_preference(App& app, bool enabled) { LegacyAppPreferenceServices services(app); diff --git a/src/legacy_app_preference_services.h b/src/legacy_app_preference_services.h index 91a16d8..90b9d07 100644 --- a/src/legacy_app_preference_services.h +++ b/src/legacy_app_preference_services.h @@ -15,6 +15,9 @@ namespace pp::panopainter { [[nodiscard]] pp::foundation::Status execute_legacy_interface_direction_preference( App& app, bool right_to_left); +[[nodiscard]] pp::foundation::Status execute_legacy_vr_mode_preference( + App& app, + bool enabled); [[nodiscard]] pp::foundation::Status execute_legacy_vr_controllers_preference( App& app, bool enabled); diff --git a/tests/app_core/app_preferences_tests.cpp b/tests/app_core/app_preferences_tests.cpp index 7466e7e..5323231 100644 --- a/tests/app_core/app_preferences_tests.cpp +++ b/tests/app_core/app_preferences_tests.cpp @@ -28,6 +28,13 @@ public: call_order += "direction;"; } + bool apply_vr_mode_preference(const pp::app::StoredBooleanPreferencePlan& plan) override + { + vr_mode = plan.value; + call_order += "vr-mode;"; + return vr_mode_result; + } + void apply_vr_controllers_preference(const pp::app::StoredBooleanPreferencePlan& plan) override { vr_controllers = plan.value; @@ -52,6 +59,8 @@ public: float viewport_scale = 0.0F; float viewport_font_scale = 0.0F; pp::app::InterfaceDirection interface_direction = pp::app::InterfaceDirection::left_to_right; + bool vr_mode = false; + bool vr_mode_result = true; bool vr_controllers = false; bool timelapse_enabled = false; pp::app::TimelapseRecordingAction timelapse_action = pp::app::TimelapseRecordingAction::no_op; @@ -120,6 +129,8 @@ void timelapse_preference_starts_and_stops_only_on_state_change(pp::tests::Harne void simple_preferences_preserve_values_for_storage(pp::tests::Harness& harness) { + PP_EXPECT(harness, pp::app::plan_vr_mode_preference(true).value); + PP_EXPECT(harness, !pp::app::plan_vr_mode_preference(false).value); PP_EXPECT(harness, pp::app::plan_vr_controllers_preference(true).value); PP_EXPECT(harness, !pp::app::plan_vr_controllers_preference(false).value); PP_EXPECT(harness, pp::app::plan_canvas_cursor_mode(2).value == 2); @@ -132,6 +143,7 @@ void preference_executor_dispatches_side_effect_plans(pp::tests::Harness& harnes PP_EXPECT(harness, pp::app::execute_ui_scale_preference(1.5F, 2.0F, services).ok()); PP_EXPECT(harness, pp::app::execute_viewport_scale_preference(1.25F, 1.0F, services).ok()); PP_EXPECT(harness, pp::app::execute_interface_direction_preference(true, services).ok()); + PP_EXPECT(harness, pp::app::execute_vr_mode_preference(true, services).ok()); PP_EXPECT(harness, pp::app::execute_vr_controllers_preference(true, services).ok()); PP_EXPECT(harness, pp::app::execute_timelapse_preference(true, false, services).ok()); PP_EXPECT(harness, pp::app::execute_canvas_cursor_mode_preference(2, services).ok()); @@ -141,13 +153,14 @@ void preference_executor_dispatches_side_effect_plans(pp::tests::Harness& harnes PP_EXPECT(harness, services.viewport_scale == 1.25F); PP_EXPECT(harness, services.viewport_font_scale == 1.25F); PP_EXPECT(harness, services.interface_direction == pp::app::InterfaceDirection::right_to_left); + PP_EXPECT(harness, services.vr_mode); PP_EXPECT(harness, services.vr_controllers); PP_EXPECT(harness, services.timelapse_enabled); PP_EXPECT(harness, services.timelapse_action == pp::app::TimelapseRecordingAction::start_recording); PP_EXPECT(harness, services.cursor_mode == 2); PP_EXPECT( harness, - services.call_order == "ui-scale;viewport-scale;direction;vr-controllers;timelapse;cursor;"); + services.call_order == "ui-scale;viewport-scale;direction;vr-mode;vr-controllers;timelapse;cursor;"); } void preference_executor_preserves_timelapse_stop_action(pp::tests::Harness& harness) @@ -159,6 +172,17 @@ void preference_executor_preserves_timelapse_stop_action(pp::tests::Harness& har PP_EXPECT(harness, services.timelapse_action == pp::app::TimelapseRecordingAction::stop_recording); } +void preference_executor_reports_failed_vr_start(pp::tests::Harness& harness) +{ + FakeAppPreferenceServices services; + services.vr_mode_result = false; + + const auto status = pp::app::execute_vr_mode_preference(true, services); + PP_EXPECT(harness, !status.ok()); + PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, services.vr_mode); +} + } int main() @@ -178,5 +202,6 @@ int main() harness.run("simple preferences preserve values for storage", simple_preferences_preserve_values_for_storage); harness.run("preference executor dispatches side effect plans", preference_executor_dispatches_side_effect_plans); harness.run("preference executor preserves timelapse stop action", preference_executor_preserves_timelapse_stop_action); + harness.run("preference executor reports failed VR start", preference_executor_reports_failed_vr_start); return harness.finish(); }