From a64a63def7e7b91c634b77cfb443e1f4e2d391f7 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 09:36:38 +0200 Subject: [PATCH] Extract app preference planning into app core --- CMakeLists.txt | 1 + docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 5 + src/app_core/app_preferences.h | 114 ++++++++++++++++++ src/app_layout.cpp | 59 ++++++---- tests/CMakeLists.txt | 36 ++++++ tests/app_core/app_preferences_tests.cpp | 92 +++++++++++++++ tools/pano_cli/main.cpp | 144 +++++++++++++++++++++++ 8 files changed, 431 insertions(+), 22 deletions(-) create mode 100644 src/app_core/app_preferences.h create mode 100644 tests/app_core/app_preferences_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88e4234..a7e4c03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -223,6 +223,7 @@ target_link_libraries(pp_platform_api pp_project_warnings) add_library(pp_app_core STATIC + src/app_core/app_preferences.h src/app_core/document_cloud.h src/app_core/document_export.cpp src/app_core/document_platform_io.h diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 098e0a9..0586c49 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/share/platform-I/O/display/keyboard/cloud contracts, but document creation/loading, brush import execution, saving, export execution, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `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 plan-export-start --requires-license --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/share/platform-I/O/display/keyboard/cloud contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `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 plan-export-start --requires-license --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index a6ce05d..c536be0 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -175,6 +175,11 @@ project-open, app-close, save, save-as, and save-version flows; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, and `pano_cli simulate-app-session` consume those 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; +the live tools/options menu and `pano_cli plan-app-preferences` consume those +contracts while legacy widgets and settings persistence execute them. `panopainter_app` is now a real static target that owns app orchestration sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, diff --git a/src/app_core/app_preferences.h b/src/app_core/app_preferences.h new file mode 100644 index 0000000..0bf6788 --- /dev/null +++ b/src/app_core/app_preferences.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include + +namespace pp::app { + +enum class InterfaceDirection { + left_to_right, + right_to_left, +}; + +enum class TimelapseRecordingAction { + no_op, + start_recording, + stop_recording, +}; + +struct ScaleApplicationPlan { + float scale = 1.0F; + float display_density = 1.0F; + float font_scale = 1.0F; +}; + +struct ScaleOptionSelection { + bool has_selection = false; + std::size_t index = 0; +}; + +struct InterfaceDirectionPlan { + InterfaceDirection direction = InterfaceDirection::left_to_right; +}; + +struct TimelapsePreferencePlan { + bool enabled = true; + TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op; +}; + +struct StoredIntegerPreferencePlan { + int value = 0; +}; + +struct StoredBooleanPreferencePlan { + bool value = false; +}; + +[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale( + float requested_scale, + float display_density) noexcept +{ + return { + requested_scale, + display_density, + requested_scale * display_density, + }; +} + +[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale( + float requested_scale, + float display_density = 1.0F) noexcept +{ + return { + requested_scale, + display_density, + requested_scale * display_density, + }; +} + +[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection( + float current_scale, + std::span options) noexcept +{ + ScaleOptionSelection selection; + for (std::size_t index = 0; index < options.size(); ++index) { + if (current_scale >= options[index]) { + selection.has_selection = true; + selection.index = index; + } + } + return selection; +} + +[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept +{ + return { + right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right, + }; +} + +[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference( + bool enabled, + bool recording_running) noexcept +{ + if (enabled && !recording_running) { + return { enabled, TimelapseRecordingAction::start_recording }; + } + if (!enabled && recording_running) { + return { enabled, TimelapseRecordingAction::stop_recording }; + } + return { enabled, TimelapseRecordingAction::no_op }; +} + +[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference( + bool enabled) noexcept +{ + return { enabled }; +} + +[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept +{ + return { mode }; +} + +} diff --git a/src/app_layout.cpp b/src/app_layout.cpp index bd83659..93f1cd0 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -6,12 +6,15 @@ #include "node_progress_bar.h" #include "node_dialog_picker.h" #include "node_panel_floating.h" +#include "app_core/app_preferences.h" #include "settings.h" #include "serializer.h" #include "font.h" #include "node_remote_page.h" #include "node_shorcuts.h" +#include + void App::title_update() { static char str[256]; @@ -924,10 +927,14 @@ void App::init_menu_tools() if (auto ui_scale = popup_time->find("tools-ui-scale")) { - // set index to current zoom level (or at least the closest in list) + std::vector scale_options; + scale_options.reserve(ui_scale->m_data.size()); for (int i = 0; i < ui_scale->m_data.size(); i++) - if (App::I->zoom >= ui_scale->get_float(i)) - ui_scale->set_index(i); + scale_options.push_back(ui_scale->get_float(i)); + + const auto selection = pp::app::plan_scale_option_selection(App::I->zoom, scale_options); + if (selection.has_selection) + ui_scale->set_index(static_cast(selection.index)); ui_scale->on_select = [ui_scale](Node* target, int index) { @@ -937,16 +944,20 @@ void App::init_menu_tools() if (auto vp_scale = popup_time->find("tools-vp-scale")) { - // set index to current zoom level (or at least the closest in list) + std::vector scale_options; + scale_options.reserve(vp_scale->m_data.size()); for (int i = 0; i < vp_scale->m_data.size(); i++) - if (App::I->canvas->m_density >= vp_scale->get_float(i)) - vp_scale->set_index(i); + scale_options.push_back(vp_scale->get_float(i)); + + const auto selection = pp::app::plan_scale_option_selection(App::I->canvas->m_density, scale_options); + if (selection.has_selection) + vp_scale->set_index(static_cast(selection.index)); vp_scale->on_select = [vp_scale](Node* target, int index) { - float d = vp_scale->get_float(index); - App::I->canvas->set_density(d); - Settings::set("vp-scale", Serializer::Float(d)); + const auto plan = pp::app::plan_viewport_scale(vp_scale->get_float(index)); + App::I->canvas->set_density(plan.scale); + Settings::set("vp-scale", Serializer::Float(plan.scale)); Settings::save(); }; } @@ -1010,8 +1021,9 @@ void App::init_menu_tools() vr_btn->find("tools-vr-controllers-check")->on_value_changed = [this, main](Node* target, bool checked) { - vr_controllers_enabled = checked; - Settings::set("vr-controllers-enabled", Serializer::Boolean(checked)); + const auto plan = pp::app::plan_vr_controllers_preference(checked); + vr_controllers_enabled = plan.value; + Settings::set("vr-controllers-enabled", Serializer::Boolean(plan.value)); Settings::save(); }; } @@ -1029,11 +1041,12 @@ void App::init_menu_tools() btn->find("tools-timelapse-check")->on_value_changed = [this, main](Node*, bool checked) { - if (!checked && App::I->rec_running) + const auto plan = pp::app::plan_timelapse_preference(checked, App::I->rec_running); + if (plan.recording_action == pp::app::TimelapseRecordingAction::stop_recording) App::I->rec_stop(); - else if (checked && !App::I->rec_running) + else if (plan.recording_action == pp::app::TimelapseRecordingAction::start_recording) App::I->rec_start(); - Settings::set("auto-timelapse", Serializer::Boolean(checked)); + Settings::set("auto-timelapse", Serializer::Boolean(plan.enabled)); Settings::save(); }; } @@ -1044,8 +1057,9 @@ void App::init_menu_tools() mode->on_select = [mode](Node* target, int index) { - App::I->canvas->set_cursor_visibility((NodeCanvas::kCursorVisibility)index); - Settings::set("show-cursor", Serializer::Integer(index)); + const auto plan = pp::app::plan_canvas_cursor_mode(index); + App::I->canvas->set_cursor_visibility((NodeCanvas::kCursorVisibility)plan.value); + Settings::set("show-cursor", Serializer::Integer(plan.value)); Settings::save(); }; } @@ -1418,17 +1432,20 @@ void App::initLayout() void App::set_ui_scale(float scale) { - zoom = scale; - FontManager::change_scale(zoom * display_density); - Settings::set("ui-scale", Serializer::Float(zoom)); + const auto plan = pp::app::plan_ui_scale(scale, display_density); + zoom = plan.scale; + FontManager::change_scale(plan.font_scale); + Settings::set("ui-scale", Serializer::Float(plan.scale)); Settings::save(); App::I->title_update(); } void App::set_ui_rtl(bool rtl) { - ui_rtl = rtl; - layout[main_id]->find("central-row")->SetRTL(rtl ? YGDirectionRTL : YGDirectionLTR); + const auto plan = pp::app::plan_interface_direction(rtl); + ui_rtl = plan.direction == pp::app::InterfaceDirection::right_to_left; + layout[main_id]->find("central-row")->SetRTL( + ui_rtl ? YGDirectionRTL : YGDirectionLTR); } bool App::get_ui_rtl() const diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4915f50..a1f336f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -318,6 +318,16 @@ add_test(NAME pp_app_core_document_recording_tests COMMAND pp_app_core_document_ set_tests_properties(pp_app_core_document_recording_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_preferences_tests + app_core/app_preferences_tests.cpp) +target_link_libraries(pp_app_core_app_preferences_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_preferences_tests COMMAND pp_app_core_app_preferences_tests) +set_tests_properties(pp_app_core_app_preferences_tests PROPERTIES + LABELS "app;desktop-fast;fuzz") + add_executable(pp_app_core_document_sharing_tests app_core/document_sharing_tests.cpp) target_link_libraries(pp_app_core_document_sharing_tests PRIVATE @@ -601,6 +611,32 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" PASS_REGULAR_EXPRESSION "\"command\":\"plan-recording-session\".*\"platformDeletesRecordedFiles\":true.*\"deleteRecordedFiles\":true.*\"frameCountAfterClear\":0") + add_test(NAME pano_cli_plan_app_preferences_smoke + COMMAND pano_cli plan-app-preferences + --ui-scale 1.5 + --display-density 2 + --current-scale 1.6 + --scale-option 0.75 + --scale-option 1 + --scale-option 1.5 + --viewport-scale 0.5 + --rtl + --cursor-mode 2) + set_tests_properties(pano_cli_plan_app_preferences_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-preferences\".*\"fontScale\":3.*\"scaleSelection\":\\{\"hasSelection\":true,\"index\":2\\}.*\"viewportScale\":\\{\"scale\":0.5\\}.*\"direction\":\"right-to-left\".*\"recordingAction\":\"start-recording\".*\"vrControllers\":\\{\"enabled\":true\\}.*\"cursor\":\\{\"mode\":2\\}") + + add_test(NAME pano_cli_plan_app_preferences_stops_timelapse_smoke + COMMAND pano_cli plan-app-preferences + --current-scale 0.5 + --scale-option 1 + --timelapse-disabled + --recording-running + --vr-controllers-disabled) + set_tests_properties(pano_cli_plan_app_preferences_stops_timelapse_smoke PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-preferences\".*\"scaleSelection\":\\{\"hasSelection\":false,\"index\":0\\}.*\"direction\":\"left-to-right\".*\"timelapse\":\\{\"enabled\":false,\"recordingAction\":\"stop-recording\"\\}.*\"vrControllers\":\\{\"enabled\":false\\}") + add_test(NAME pano_cli_plan_share_file_unsaved_smoke COMMAND pano_cli plan-share-file) set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES diff --git a/tests/app_core/app_preferences_tests.cpp b/tests/app_core/app_preferences_tests.cpp new file mode 100644 index 0000000..2fef3e2 --- /dev/null +++ b/tests/app_core/app_preferences_tests.cpp @@ -0,0 +1,92 @@ +#include "app_core/app_preferences.h" +#include "test_harness.h" + +#include + +namespace { + +void ui_scale_computes_font_scale_from_display_density(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_ui_scale(1.5F, 2.0F); + PP_EXPECT(harness, plan.scale == 1.5F); + PP_EXPECT(harness, plan.display_density == 2.0F); + PP_EXPECT(harness, plan.font_scale == 3.0F); +} + +void scale_option_selection_uses_last_option_not_above_current(pp::tests::Harness& harness) +{ + constexpr std::array options { 0.75F, 1.0F, 1.5F, 2.0F }; + const auto plan = pp::app::plan_scale_option_selection(1.6F, options); + PP_EXPECT(harness, plan.has_selection); + PP_EXPECT(harness, plan.index == 2U); +} + +void scale_option_selection_handles_empty_or_too_large_options(pp::tests::Harness& harness) +{ + constexpr std::array empty {}; + const auto empty_plan = pp::app::plan_scale_option_selection(1.0F, empty); + PP_EXPECT(harness, !empty_plan.has_selection); + PP_EXPECT(harness, empty_plan.index == 0U); + + constexpr std::array options { 2.0F, 3.0F }; + const auto low_plan = pp::app::plan_scale_option_selection(1.0F, options); + PP_EXPECT(harness, !low_plan.has_selection); + PP_EXPECT(harness, low_plan.index == 0U); +} + +void interface_direction_tracks_requested_layout_direction(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_interface_direction(false).direction == pp::app::InterfaceDirection::left_to_right); + PP_EXPECT( + harness, + pp::app::plan_interface_direction(true).direction == pp::app::InterfaceDirection::right_to_left); +} + +void timelapse_preference_starts_and_stops_only_on_state_change(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_timelapse_preference(true, false).recording_action + == pp::app::TimelapseRecordingAction::start_recording); + PP_EXPECT( + harness, + pp::app::plan_timelapse_preference(false, true).recording_action + == pp::app::TimelapseRecordingAction::stop_recording); + PP_EXPECT( + harness, + pp::app::plan_timelapse_preference(true, true).recording_action + == pp::app::TimelapseRecordingAction::no_op); + PP_EXPECT( + harness, + pp::app::plan_timelapse_preference(false, false).recording_action + == pp::app::TimelapseRecordingAction::no_op); +} + +void simple_preferences_preserve_values_for_storage(pp::tests::Harness& harness) +{ + 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); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("ui scale computes font scale from display density", ui_scale_computes_font_scale_from_display_density); + harness.run( + "scale option selection uses last option not above current", + scale_option_selection_uses_last_option_not_above_current); + harness.run( + "scale option selection handles empty or too large options", + scale_option_selection_handles_empty_or_too_large_options); + harness.run("interface direction tracks requested layout direction", interface_direction_tracks_requested_layout_direction); + harness.run( + "timelapse preference starts and stops only on state change", + timelapse_preference_starts_and_stops_only_on_state_change); + harness.run("simple preferences preserve values for storage", simple_preferences_preserve_values_for_storage); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index d6c4ad1..902095c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,3 +1,4 @@ +#include "app_core/app_preferences.h" #include "app_core/document_export.h" #include "app_core/document_cloud.h" #include "app_core/document_platform_io.h" @@ -186,6 +187,19 @@ struct PlanClipboardWriteArgs { std::string text; }; +struct PlanAppPreferencesArgs { + float ui_scale = 1.0F; + float display_density = 1.0F; + float current_scale = 1.0F; + std::vector scale_options; + float viewport_scale = 1.0F; + bool right_to_left = false; + bool timelapse_enabled = true; + bool recording_running = false; + bool vr_controllers_enabled = true; + int cursor_mode = 0; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -587,6 +601,32 @@ const char* clipboard_write_action_name(pp::app::ClipboardWriteAction action) no return "write-text"; } +const char* interface_direction_name(pp::app::InterfaceDirection direction) noexcept +{ + switch (direction) { + case pp::app::InterfaceDirection::left_to_right: + return "left-to-right"; + case pp::app::InterfaceDirection::right_to_left: + return "right-to-left"; + } + + return "left-to-right"; +} + +const char* timelapse_recording_action_name(pp::app::TimelapseRecordingAction action) noexcept +{ + switch (action) { + case pp::app::TimelapseRecordingAction::no_op: + return "no-op"; + case pp::app::TimelapseRecordingAction::start_recording: + return "start-recording"; + case pp::app::TimelapseRecordingAction::stop_recording: + return "stop-recording"; + } + + return "no-op"; +} + pp::foundation::Result parse_float_arg(std::string_view text) { float value = 0.0F; @@ -627,6 +667,7 @@ void print_help() << " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n" << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n" + << " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" @@ -1967,6 +2008,105 @@ int plan_recording_session(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_preferences_args( + int argc, + char** argv, + PlanAppPreferencesArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--ui-scale" || key == "--display-density" || key == "--current-scale" + || key == "--scale-option" || key == "--viewport-scale") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--ui-scale") { + args.ui_scale = value.value(); + } else if (key == "--display-density") { + args.display_density = value.value(); + } else if (key == "--current-scale") { + args.current_scale = value.value(); + } else if (key == "--scale-option") { + args.scale_options.push_back(value.value()); + } else { + args.viewport_scale = value.value(); + } + } else if (key == "--rtl") { + args.right_to_left = true; + } else if (key == "--timelapse-disabled") { + args.timelapse_enabled = false; + } else if (key == "--recording-running") { + args.recording_running = true; + } else if (key == "--vr-controllers-disabled") { + args.vr_controllers_enabled = false; + } else if (key == "--cursor-mode") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + args.cursor_mode = static_cast(value.value()); + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_preferences(int argc, char** argv) +{ + PlanAppPreferencesArgs args; + const auto status = parse_plan_app_preferences_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-preferences", status.message); + return 2; + } + + const auto ui_scale = pp::app::plan_ui_scale(args.ui_scale, args.display_density); + const auto scale_selection = pp::app::plan_scale_option_selection( + args.current_scale, + args.scale_options); + const auto viewport_scale = pp::app::plan_viewport_scale(args.viewport_scale); + const auto direction = pp::app::plan_interface_direction(args.right_to_left); + const auto timelapse = pp::app::plan_timelapse_preference( + args.timelapse_enabled, + args.recording_running); + const auto vr_controllers = pp::app::plan_vr_controllers_preference(args.vr_controllers_enabled); + const auto cursor_mode = pp::app::plan_canvas_cursor_mode(args.cursor_mode); + + std::cout << "{\"ok\":true,\"command\":\"plan-app-preferences\"" + << ",\"state\":{\"uiScale\":" << args.ui_scale + << ",\"displayDensity\":" << args.display_density + << ",\"currentScale\":" << args.current_scale + << ",\"scaleOptions\":" << args.scale_options.size() + << ",\"viewportScale\":" << args.viewport_scale + << ",\"rtl\":" << json_bool(args.right_to_left) + << ",\"timelapseEnabled\":" << json_bool(args.timelapse_enabled) + << ",\"recordingRunning\":" << json_bool(args.recording_running) + << ",\"vrControllersEnabled\":" << json_bool(args.vr_controllers_enabled) + << ",\"cursorMode\":" << args.cursor_mode + << "},\"uiScale\":{\"scale\":" << ui_scale.scale + << ",\"displayDensity\":" << ui_scale.display_density + << ",\"fontScale\":" << ui_scale.font_scale + << "},\"scaleSelection\":{\"hasSelection\":" << json_bool(scale_selection.has_selection) + << ",\"index\":" << scale_selection.index + << "},\"viewportScale\":{\"scale\":" << viewport_scale.scale + << "},\"direction\":\"" << interface_direction_name(direction.direction) + << "\",\"timelapse\":{\"enabled\":" << json_bool(timelapse.enabled) + << ",\"recordingAction\":\"" << timelapse_recording_action_name(timelapse.recording_action) + << "\"},\"vrControllers\":{\"enabled\":" << json_bool(vr_controllers.value) + << "},\"cursor\":{\"mode\":" << cursor_mode.value + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_share_file_args( int argc, char** argv, @@ -4359,6 +4499,10 @@ int main(int argc, char** argv) return plan_recording_session(argc, argv); } + if (command == "plan-app-preferences") { + return plan_app_preferences(argc, argv); + } + if (command == "plan-share-file") { return plan_share_file(argc, argv); }