Extract app preference planning into app core

This commit is contained in:
2026-06-03 09:36:38 +02:00
parent 19cb14b5dc
commit a64a63def7
8 changed files with 431 additions and 22 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,114 @@
#pragma once
#include <cstddef>
#include <span>
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<const float> 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 };
}
}

View File

@@ -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 <vector>
void App::title_update()
{
static char str[256];
@@ -924,10 +927,14 @@ void App::init_menu_tools()
if (auto ui_scale = popup_time->find<NodeComboBox>("tools-ui-scale"))
{
// set index to current zoom level (or at least the closest in list)
std::vector<float> 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<int>(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<NodeComboBox>("tools-vp-scale"))
{
// set index to current zoom level (or at least the closest in list)
std::vector<float> 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<int>(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<NodeCheckBox>("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<NodeCheckBox>("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

View File

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

View File

@@ -0,0 +1,92 @@
#include "app_core/app_preferences.h"
#include "test_harness.h"
#include <array>
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<float, 4> 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<float, 0> 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<float, 2> 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();
}

View File

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