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)
|
pp_project_warnings)
|
||||||
|
|
||||||
add_library(pp_app_core STATIC
|
add_library(pp_app_core STATIC
|
||||||
|
src/app_core/app_preferences.h
|
||||||
src/app_core/document_cloud.h
|
src/app_core/document_cloud.h
|
||||||
src/app_core/document_export.cpp
|
src/app_core/document_export.cpp
|
||||||
src/app_core/document_platform_io.h
|
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-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-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-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-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 |
|
| 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,
|
`App::open_document`, `App::request_close`, file-menu save actions,
|
||||||
`NodeCanvas` save hotkeys, and `pano_cli simulate-app-session` consume those
|
`NodeCanvas` save hotkeys, and `pano_cli simulate-app-session` consume those
|
||||||
contracts while legacy canvas/project loading remains in place.
|
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
|
`panopainter_app` is now a real static target that owns app orchestration
|
||||||
sources, app version metadata, and version-header generation.
|
sources, app version metadata, and version-header generation.
|
||||||
`pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas,
|
`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_progress_bar.h"
|
||||||
#include "node_dialog_picker.h"
|
#include "node_dialog_picker.h"
|
||||||
#include "node_panel_floating.h"
|
#include "node_panel_floating.h"
|
||||||
|
#include "app_core/app_preferences.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "serializer.h"
|
#include "serializer.h"
|
||||||
#include "font.h"
|
#include "font.h"
|
||||||
#include "node_remote_page.h"
|
#include "node_remote_page.h"
|
||||||
#include "node_shorcuts.h"
|
#include "node_shorcuts.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
void App::title_update()
|
void App::title_update()
|
||||||
{
|
{
|
||||||
static char str[256];
|
static char str[256];
|
||||||
@@ -924,10 +927,14 @@ void App::init_menu_tools()
|
|||||||
|
|
||||||
if (auto ui_scale = popup_time->find<NodeComboBox>("tools-ui-scale"))
|
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++)
|
for (int i = 0; i < ui_scale->m_data.size(); i++)
|
||||||
if (App::I->zoom >= ui_scale->get_float(i))
|
scale_options.push_back(ui_scale->get_float(i));
|
||||||
ui_scale->set_index(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)
|
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"))
|
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++)
|
for (int i = 0; i < vp_scale->m_data.size(); i++)
|
||||||
if (App::I->canvas->m_density >= vp_scale->get_float(i))
|
scale_options.push_back(vp_scale->get_float(i));
|
||||||
vp_scale->set_index(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)
|
vp_scale->on_select = [vp_scale](Node* target, int index)
|
||||||
{
|
{
|
||||||
float d = vp_scale->get_float(index);
|
const auto plan = pp::app::plan_viewport_scale(vp_scale->get_float(index));
|
||||||
App::I->canvas->set_density(d);
|
App::I->canvas->set_density(plan.scale);
|
||||||
Settings::set("vp-scale", Serializer::Float(d));
|
Settings::set("vp-scale", Serializer::Float(plan.scale));
|
||||||
Settings::save();
|
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_btn->find<NodeCheckBox>("tools-vr-controllers-check")->on_value_changed = [this, main](Node* target, bool checked)
|
||||||
{
|
{
|
||||||
vr_controllers_enabled = checked;
|
const auto plan = pp::app::plan_vr_controllers_preference(checked);
|
||||||
Settings::set("vr-controllers-enabled", Serializer::Boolean(checked));
|
vr_controllers_enabled = plan.value;
|
||||||
|
Settings::set("vr-controllers-enabled", Serializer::Boolean(plan.value));
|
||||||
Settings::save();
|
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)
|
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();
|
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();
|
App::I->rec_start();
|
||||||
Settings::set("auto-timelapse", Serializer::Boolean(checked));
|
Settings::set("auto-timelapse", Serializer::Boolean(plan.enabled));
|
||||||
Settings::save();
|
Settings::save();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1044,8 +1057,9 @@ void App::init_menu_tools()
|
|||||||
|
|
||||||
mode->on_select = [mode](Node* target, int index)
|
mode->on_select = [mode](Node* target, int index)
|
||||||
{
|
{
|
||||||
App::I->canvas->set_cursor_visibility((NodeCanvas::kCursorVisibility)index);
|
const auto plan = pp::app::plan_canvas_cursor_mode(index);
|
||||||
Settings::set("show-cursor", Serializer::Integer(index));
|
App::I->canvas->set_cursor_visibility((NodeCanvas::kCursorVisibility)plan.value);
|
||||||
|
Settings::set("show-cursor", Serializer::Integer(plan.value));
|
||||||
Settings::save();
|
Settings::save();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1418,17 +1432,20 @@ void App::initLayout()
|
|||||||
|
|
||||||
void App::set_ui_scale(float scale)
|
void App::set_ui_scale(float scale)
|
||||||
{
|
{
|
||||||
zoom = scale;
|
const auto plan = pp::app::plan_ui_scale(scale, display_density);
|
||||||
FontManager::change_scale(zoom * display_density);
|
zoom = plan.scale;
|
||||||
Settings::set("ui-scale", Serializer::Float(zoom));
|
FontManager::change_scale(plan.font_scale);
|
||||||
|
Settings::set("ui-scale", Serializer::Float(plan.scale));
|
||||||
Settings::save();
|
Settings::save();
|
||||||
App::I->title_update();
|
App::I->title_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::set_ui_rtl(bool rtl)
|
void App::set_ui_rtl(bool rtl)
|
||||||
{
|
{
|
||||||
ui_rtl = rtl;
|
const auto plan = pp::app::plan_interface_direction(rtl);
|
||||||
layout[main_id]->find("central-row")->SetRTL(rtl ? YGDirectionRTL : YGDirectionLTR);
|
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
|
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
|
set_tests_properties(pp_app_core_document_recording_tests PROPERTIES
|
||||||
LABELS "app;desktop-fast;fuzz")
|
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
|
add_executable(pp_app_core_document_sharing_tests
|
||||||
app_core/document_sharing_tests.cpp)
|
app_core/document_sharing_tests.cpp)
|
||||||
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
|
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
|
||||||
@@ -601,6 +611,32 @@ if(TARGET pano_cli)
|
|||||||
LABELS "app;integration;desktop-fast;fuzz"
|
LABELS "app;integration;desktop-fast;fuzz"
|
||||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-recording-session\".*\"platformDeletesRecordedFiles\":true.*\"deleteRecordedFiles\":true.*\"frameCountAfterClear\":0")
|
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
|
add_test(NAME pano_cli_plan_share_file_unsaved_smoke
|
||||||
COMMAND pano_cli plan-share-file)
|
COMMAND pano_cli plan-share-file)
|
||||||
set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES
|
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_export.h"
|
||||||
#include "app_core/document_cloud.h"
|
#include "app_core/document_cloud.h"
|
||||||
#include "app_core/document_platform_io.h"
|
#include "app_core/document_platform_io.h"
|
||||||
@@ -186,6 +187,19 @@ struct PlanClipboardWriteArgs {
|
|||||||
std::string text;
|
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 {
|
struct SimulateAppSessionArgs {
|
||||||
bool has_canvas = true;
|
bool has_canvas = true;
|
||||||
bool new_document = false;
|
bool new_document = false;
|
||||||
@@ -587,6 +601,32 @@ const char* clipboard_write_action_name(pp::app::ClipboardWriteAction action) no
|
|||||||
return "write-text";
|
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)
|
pp::foundation::Result<float> parse_float_arg(std::string_view text)
|
||||||
{
|
{
|
||||||
float value = 0.0F;
|
float value = 0.0F;
|
||||||
@@ -627,6 +667,7 @@ void print_help()
|
|||||||
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
|
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
|
||||||
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\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-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-share-file [--path FILE]\n"
|
||||||
<< " plan-picked-path [--path FILE]\n"
|
<< " plan-picked-path [--path FILE]\n"
|
||||||
<< " plan-display-file [--path FILE]\n"
|
<< " plan-display-file [--path FILE]\n"
|
||||||
@@ -1967,6 +2008,105 @@ int plan_recording_session(int argc, char** argv)
|
|||||||
return 0;
|
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(
|
pp::foundation::Status parse_plan_share_file_args(
|
||||||
int argc,
|
int argc,
|
||||||
char** argv,
|
char** argv,
|
||||||
@@ -4359,6 +4499,10 @@ int main(int argc, char** argv)
|
|||||||
return plan_recording_session(argc, argv);
|
return plan_recording_session(argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == "plan-app-preferences") {
|
||||||
|
return plan_app_preferences(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
if (command == "plan-share-file") {
|
if (command == "plan-share-file") {
|
||||||
return plan_share_file(argc, argv);
|
return plan_share_file(argc, argv);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user