Extract app preference planning into app core
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
114
src/app_core/app_preferences.h
Normal file
114
src/app_core/app_preferences.h
Normal 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 };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
92
tests/app_core/app_preferences_tests.cpp
Normal file
92
tests/app_core/app_preferences_tests.cpp
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user