Centralize legacy app startup

This commit is contained in:
2026-06-04 14:32:39 +02:00
parent 884a6d4940
commit 2bd1b12ade
12 changed files with 521 additions and 13 deletions

View File

@@ -226,6 +226,7 @@ add_library(pp_app_core STATIC
src/app_core/about_menu.h
src/app_core/app_preferences.h
src/app_core/app_status.h
src/app_core/app_startup.h
src/app_core/brush_ui.h
src/app_core/canvas_hotkey.h
src/app_core/canvas_tool_ui.h

View File

@@ -84,6 +84,8 @@ set(PP_PANOPAINTER_APP_SOURCES
src/app_vr.cpp
src/legacy_app_preference_services.cpp
src/legacy_app_preference_services.h
src/legacy_app_startup_services.cpp
src/legacy_app_startup_services.h
src/legacy_cloud_services.cpp
src/legacy_cloud_services.h
src/legacy_document_export_services.cpp

View File

@@ -173,6 +173,12 @@ Known local toolchain state:
the `pp_app_core` `AppPreferenceServices` contract while retained settings
persistence, VR start/stop, recording lifecycle, and legacy canvas/UI
execution remain tracked by `DEBT-0045`.
- `src/legacy_app_startup_services.*` is the current app-shell bridge for
startup preference/runtime execution. It keeps run-counter persistence,
startup preference save, auto-timelapse startup, stored VR-controller state,
and license-warning decisions on the `pp_app_core` `AppStartupServices`
contract while retained `Settings`, `App::rec_start`, and message-box
execution remain tracked by `DEBT-0046`.
- `src/legacy_canvas_tool_services.*` is the current app-shell bridge for
canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and
canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core`
@@ -587,6 +593,10 @@ Known local toolchain state:
timelapse start/stop/no-op decisions, VR mode success/failure dispatch,
simple stored preferences, and `AppPreferenceServices` execution dispatch for
options-menu side effects.
- `pp_app_core_app_startup_tests` covers startup run-counter increment
planning, optional auto-timelapse/license/VR-controller decisions, negative
and overflow run-counter rejection, stable full startup dispatch ordering,
split persistence/runtime dispatch, and malformed startup-plan rejection.
- `pp_platform_api_tests` covers service dispatch for clipboard read/write,
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
external file display, file sharing, and picker callbacks without platform

View File

@@ -63,6 +63,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`, but the bridge still calls legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::vr_start`, `App::vr_stop`, `NodeCanvas::set_density`, `NodeCanvas::set_cursor_visibility`, `App::rec_start`, `App::rec_stop`, and `Settings::save` directly | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters |
| DEBT-0046 | Open | Modernization | Startup preference/runtime execution now consumes pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `AppStartupServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, and message-box license warning execution directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, and startup UI/runtime side effects are owned by injected app/preferences/storage/recording/UI services with `App::init` acting only as orchestration |
## Closed Debt

View File

@@ -183,6 +183,12 @@ contracts. Options-menu preference execution now dispatches through
`AppPreferenceServices` and `src/legacy_app_preference_services.*` before
legacy widgets, settings persistence, recording toggles, and canvas cursor
updates continue.
It also owns tested startup plans for run-counter increments, preference-save
intent, auto-timelapse startup, stored VR-controller state, and license-warning
visibility. `App::init` now plans those decisions before heavy initialization,
executes run-counter persistence through `src/legacy_app_startup_services.*`
before asset/layout setup, and executes runtime startup side effects after the
UI layout and main render target exist.
It also owns tested app status/display plans for document title text,
resolution mapping/labels, DPI text, history-memory text, and recording-frame
status text, plus renderer diagnostic indicator labels for framebuffer fetch
@@ -1358,6 +1364,11 @@ Results:
- Focused preference CTest coverage passed for
`pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests
after the live bridge split, including VR mode failed-start status coverage.
- `PanoPainter`, `pp_app_core_app_startup_tests`, and `pano_cli` built after
startup preference/runtime execution moved behind app startup services.
- Focused startup CTest coverage passed for `pp_app_core_app_startup_tests`,
`pano_cli_plan_app_startup_smoke`, and
`pano_cli_plan_app_startup_rejects_negative_counter`.
- `pp_app_core_document_recording_tests` passed, covering recording start/stop,
clear, platform recorded-file cleanup, frame-count reset, export progress
totals, and oversized progress-total clamping.

View File

@@ -6,10 +6,12 @@
#include "node_progress_bar.h"
#include "mp4enc.h"
#include "app_core/app_status.h"
#include "app_core/app_startup.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_recording.h"
#include "app_core/document_route.h"
#include "app_core/document_session.h"
#include "legacy_app_startup_services.h"
#include "legacy_document_open_services.h"
#include "legacy_document_session_services.h"
#include "legacy_recording_services.h"
@@ -436,12 +438,20 @@ void App::init()
LOG("OpenGL startup state failed: %s", startup_state_status.message);
});
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1;
Settings::set("run_counter", Serializer::Integer(run_counter));
LOG("run_counter %d", run_counter);
if (!Settings::save())
LOG("save preferences failed");
const auto startup_plan = pp::app::plan_app_startup(
Settings::value<Serializer::Integer>("run_counter"),
Settings::value_or<Serializer::Boolean>("auto-timelapse", true),
Settings::value_or<Serializer::Boolean>("vr-controllers-enabled", vr_controllers_enabled),
check_license());
if (!startup_plan) {
LOG("App startup plan failed: %s", startup_plan.status().message);
} else {
const auto persistence_status = pp::panopainter::execute_legacy_app_startup_persistence_plan(
*this,
startup_plan.value());
if (!persistence_status.ok())
LOG("App startup persistence failed: %s", persistence_status.message);
}
initShaders();
initAssets();
@@ -450,13 +460,12 @@ void App::init()
uirtt.create(width, height, -1, rgba8_internal_format(), true);
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true))
rec_start();
Settings::value<Serializer::Boolean>("vr-controllers-enabled", vr_controllers_enabled);
if (!check_license())
{
message_box("License", "Could not validate this license, running in demo mode.");
if (startup_plan) {
const auto startup_status = pp::panopainter::execute_legacy_app_startup_runtime_plan(
*this,
startup_plan.value());
if (!startup_status.ok())
LOG("App startup runtime execution failed: %s", startup_status.message);
}
}

112
src/app_core/app_startup.h Normal file
View File

@@ -0,0 +1,112 @@
#pragma once
#include "foundation/result.h"
#include <limits>
namespace pp::app {
struct AppStartupPlan {
int previous_run_counter = 0;
int next_run_counter = 1;
bool save_preferences = true;
bool start_timelapse = false;
bool vr_controllers_enabled = true;
bool show_license_warning = false;
};
class AppStartupServices {
public:
virtual ~AppStartupServices() = default;
virtual void store_run_counter(int value) = 0;
virtual void save_preferences() = 0;
virtual void start_timelapse_recording() = 0;
virtual void apply_vr_controllers_enabled(bool enabled) = 0;
virtual void show_license_warning() = 0;
};
[[nodiscard]] inline pp::foundation::Result<AppStartupPlan> plan_app_startup(
int current_run_counter,
bool auto_timelapse_enabled,
bool stored_vr_controllers_enabled,
bool license_valid)
{
if (current_run_counter < 0) {
return pp::foundation::Result<AppStartupPlan>::failure(
pp::foundation::Status::invalid_argument("run counter must not be negative"));
}
if (current_run_counter == std::numeric_limits<int>::max()) {
return pp::foundation::Result<AppStartupPlan>::failure(
pp::foundation::Status::out_of_range("run counter would overflow"));
}
AppStartupPlan plan;
plan.previous_run_counter = current_run_counter;
plan.next_run_counter = current_run_counter + 1;
plan.start_timelapse = auto_timelapse_enabled;
plan.vr_controllers_enabled = stored_vr_controllers_enabled;
plan.show_license_warning = !license_valid;
return pp::foundation::Result<AppStartupPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
services.store_run_counter(plan.next_run_counter);
if (plan.save_preferences) {
services.save_preferences();
}
if (plan.start_timelapse) {
services.start_timelapse_recording();
}
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
if (plan.show_license_warning) {
services.show_license_warning();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_persistence_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
services.store_run_counter(plan.next_run_counter);
if (plan.save_preferences) {
services.save_preferences();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_runtime_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
if (plan.start_timelapse) {
services.start_timelapse_recording();
}
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
if (plan.show_license_warning) {
services.show_license_warning();
}
return pp::foundation::Status::success();
}
} // namespace pp::app

View File

@@ -0,0 +1,76 @@
#include "pch.h"
#include "legacy_app_startup_services.h"
#include "app.h"
#include "serializer.h"
#include "settings.h"
namespace pp::panopainter {
namespace {
class LegacyAppStartupServices final : public pp::app::AppStartupServices {
public:
explicit LegacyAppStartupServices(App& app) noexcept
: app_(app)
{
}
void store_run_counter(int value) override
{
Settings::set("run_counter", Serializer::Integer(value));
LOG("run_counter %d", value);
}
void save_preferences() override
{
if (!Settings::save())
LOG("save preferences failed");
}
void start_timelapse_recording() override
{
app_.rec_start();
}
void apply_vr_controllers_enabled(bool enabled) override
{
app_.vr_controllers_enabled = enabled;
}
void show_license_warning() override
{
app_.message_box("License", "Could not validate this license, running in demo mode.");
}
private:
App& app_;
};
} // namespace
pp::foundation::Status execute_legacy_app_startup_plan(
App& app,
const pp::app::AppStartupPlan& plan)
{
LegacyAppStartupServices services(app);
return pp::app::execute_app_startup_plan(plan, services);
}
pp::foundation::Status execute_legacy_app_startup_persistence_plan(
App& app,
const pp::app::AppStartupPlan& plan)
{
LegacyAppStartupServices services(app);
return pp::app::execute_app_startup_persistence_plan(plan, services);
}
pp::foundation::Status execute_legacy_app_startup_runtime_plan(
App& app,
const pp::app::AppStartupPlan& plan)
{
LegacyAppStartupServices services(app);
return pp::app::execute_app_startup_runtime_plan(plan, services);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,19 @@
#pragma once
#include "app_core/app_startup.h"
class App;
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_plan(
App& app,
const pp::app::AppStartupPlan& plan);
[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_persistence_plan(
App& app,
const pp::app::AppStartupPlan& plan);
[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_runtime_plan(
App& app,
const pp::app::AppStartupPlan& plan);
} // namespace pp::panopainter

View File

@@ -488,6 +488,16 @@ add_test(NAME pp_app_core_app_status_tests COMMAND pp_app_core_app_status_tests)
set_tests_properties(pp_app_core_app_status_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_app_startup_tests
app_core/app_startup_tests.cpp)
target_link_libraries(pp_app_core_app_startup_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_app_startup_tests COMMAND pp_app_core_app_startup_tests)
set_tests_properties(pp_app_core_app_startup_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
@@ -851,6 +861,22 @@ if(TARGET pano_cli)
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_app_startup_smoke
COMMAND pano_cli plan-app-startup
--run-counter 7
--vr-controllers-disabled
--license-invalid)
set_tests_properties(pano_cli_plan_app_startup_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup\".*\"runCounter\":7.*\"licenseValid\":false.*\"previousRunCounter\":7.*\"nextRunCounter\":8.*\"savePreferences\":true.*\"startTimelapse\":true.*\"vrControllersEnabled\":false.*\"showLicenseWarning\":true")
add_test(NAME pano_cli_plan_app_startup_rejects_negative_counter
COMMAND pano_cli plan-app-startup --run-counter -1)
set_tests_properties(pano_cli_plan_app_startup_rejects_negative_counter PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
WILL_FAIL TRUE
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup\".*\"message\":\"run counter must not be negative\"")
add_test(NAME pano_cli_plan_tools_menu_shortcuts_smoke
COMMAND pano_cli plan-tools-menu --command shortcuts)
set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES

View File

@@ -0,0 +1,164 @@
#include "app_core/app_startup.h"
#include "test_harness.h"
#include <limits>
#include <string>
namespace {
class FakeAppStartupServices final : public pp::app::AppStartupServices {
public:
void store_run_counter(int value) override
{
stored_run_counter = value;
call_order += "store-counter;";
}
void save_preferences() override
{
save_calls += 1;
call_order += "save;";
}
void start_timelapse_recording() override
{
timelapse_starts += 1;
call_order += "timelapse;";
}
void apply_vr_controllers_enabled(bool enabled) override
{
vr_controllers_enabled = enabled;
call_order += "vr-controllers;";
}
void show_license_warning() override
{
license_warnings += 1;
call_order += "license;";
}
int stored_run_counter = 0;
int save_calls = 0;
int timelapse_starts = 0;
bool vr_controllers_enabled = false;
int license_warnings = 0;
std::string call_order;
};
void startup_plan_increments_counter_and_enables_requested_side_effects(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_startup(7, true, false, false);
PP_EXPECT(harness, plan);
PP_EXPECT(harness, plan.value().previous_run_counter == 7);
PP_EXPECT(harness, plan.value().next_run_counter == 8);
PP_EXPECT(harness, plan.value().save_preferences);
PP_EXPECT(harness, plan.value().start_timelapse);
PP_EXPECT(harness, !plan.value().vr_controllers_enabled);
PP_EXPECT(harness, plan.value().show_license_warning);
}
void startup_plan_preserves_disabled_optional_work(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_startup(0, false, true, true);
PP_EXPECT(harness, plan);
PP_EXPECT(harness, plan.value().next_run_counter == 1);
PP_EXPECT(harness, !plan.value().start_timelapse);
PP_EXPECT(harness, plan.value().vr_controllers_enabled);
PP_EXPECT(harness, !plan.value().show_license_warning);
}
void startup_plan_rejects_invalid_counters(pp::tests::Harness& harness)
{
const auto negative = pp::app::plan_app_startup(-1, true, true, true);
const auto overflow = pp::app::plan_app_startup(std::numeric_limits<int>::max(), true, true, true);
PP_EXPECT(harness, !negative);
PP_EXPECT(harness, negative.status().code == pp::foundation::StatusCode::invalid_argument);
PP_EXPECT(harness, !overflow);
PP_EXPECT(harness, overflow.status().code == pp::foundation::StatusCode::out_of_range);
}
void startup_executor_dispatches_in_stable_order(pp::tests::Harness& harness)
{
FakeAppStartupServices services;
const auto plan = pp::app::plan_app_startup(2, true, true, false);
PP_EXPECT(harness, plan);
PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan.value(), services).ok());
PP_EXPECT(harness, services.stored_run_counter == 3);
PP_EXPECT(harness, services.save_calls == 1);
PP_EXPECT(harness, services.timelapse_starts == 1);
PP_EXPECT(harness, services.vr_controllers_enabled);
PP_EXPECT(harness, services.license_warnings == 1);
PP_EXPECT(harness, services.call_order == "store-counter;save;timelapse;vr-controllers;license;");
}
void startup_executor_preserves_no_op_side_effects(pp::tests::Harness& harness)
{
FakeAppStartupServices services;
auto plan = pp::app::plan_app_startup(4, false, false, true).value();
PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan, services).ok());
PP_EXPECT(harness, services.stored_run_counter == 5);
PP_EXPECT(harness, services.save_calls == 1);
PP_EXPECT(harness, services.timelapse_starts == 0);
PP_EXPECT(harness, !services.vr_controllers_enabled);
PP_EXPECT(harness, services.license_warnings == 0);
PP_EXPECT(harness, services.call_order == "store-counter;save;vr-controllers;");
}
void startup_split_executors_keep_persistence_and_runtime_separate(pp::tests::Harness& harness)
{
FakeAppStartupServices persistence_services;
FakeAppStartupServices runtime_services;
const auto plan = pp::app::plan_app_startup(5, true, false, false);
PP_EXPECT(harness, plan);
PP_EXPECT(harness, pp::app::execute_app_startup_persistence_plan(plan.value(), persistence_services).ok());
PP_EXPECT(harness, persistence_services.stored_run_counter == 6);
PP_EXPECT(harness, persistence_services.save_calls == 1);
PP_EXPECT(harness, persistence_services.timelapse_starts == 0);
PP_EXPECT(harness, persistence_services.license_warnings == 0);
PP_EXPECT(harness, persistence_services.call_order == "store-counter;save;");
PP_EXPECT(harness, pp::app::execute_app_startup_runtime_plan(plan.value(), runtime_services).ok());
PP_EXPECT(harness, runtime_services.stored_run_counter == 0);
PP_EXPECT(harness, runtime_services.save_calls == 0);
PP_EXPECT(harness, runtime_services.timelapse_starts == 1);
PP_EXPECT(harness, !runtime_services.vr_controllers_enabled);
PP_EXPECT(harness, runtime_services.license_warnings == 1);
PP_EXPECT(harness, runtime_services.call_order == "timelapse;vr-controllers;license;");
}
void startup_executor_rejects_malformed_counter_state(pp::tests::Harness& harness)
{
FakeAppStartupServices services;
pp::app::AppStartupPlan plan;
plan.previous_run_counter = 3;
plan.next_run_counter = 3;
PP_EXPECT(harness, !pp::app::execute_app_startup_plan(plan, services).ok());
PP_EXPECT(harness, services.call_order.empty());
}
}
int main()
{
pp::tests::Harness harness;
harness.run(
"startup plan increments counter and enables requested side effects",
startup_plan_increments_counter_and_enables_requested_side_effects);
harness.run("startup plan preserves disabled optional work", startup_plan_preserves_disabled_optional_work);
harness.run("startup plan rejects invalid counters", startup_plan_rejects_invalid_counters);
harness.run("startup executor dispatches in stable order", startup_executor_dispatches_in_stable_order);
harness.run("startup executor preserves no-op side effects", startup_executor_preserves_no_op_side_effects);
harness.run(
"startup split executors keep persistence and runtime separate",
startup_split_executors_keep_persistence_and_runtime_separate);
harness.run("startup executor rejects malformed counter state", startup_executor_rejects_malformed_counter_state);
return harness.finish();
}

View File

@@ -1,6 +1,7 @@
#include "app_core/about_menu.h"
#include "app_core/app_preferences.h"
#include "app_core/app_status.h"
#include "app_core/app_startup.h"
#include "app_core/brush_ui.h"
#include "app_core/canvas_hotkey.h"
#include "app_core/canvas_tool_ui.h"
@@ -227,6 +228,13 @@ struct PlanAppPreferencesArgs {
int cursor_mode = 0;
};
struct PlanAppStartupArgs {
int run_counter = 0;
bool auto_timelapse_enabled = true;
bool vr_controllers_enabled = true;
bool license_valid = true;
};
struct PlanAppStatusArgs {
std::string document_name = "no-name";
bool unsaved = false;
@@ -1852,6 +1860,7 @@ void print_help()
<< " 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-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n"
<< " plan-app-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N] [--framebuffer-fetch] [--float32] [--float32-linear] [--float16]\n"
<< " plan-tools-menu --command panels|options|clear-grids|reset-camera|shortcuts|sonarpen [--sonarpen-available]\n"
<< " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n"
@@ -3416,6 +3425,70 @@ int plan_app_preferences(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_plan_app_startup_args(
int argc,
char** argv,
PlanAppStartupArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--run-counter") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
args.run_counter = value.value();
} else if (key == "--auto-timelapse-disabled") {
args.auto_timelapse_enabled = false;
} else if (key == "--vr-controllers-disabled") {
args.vr_controllers_enabled = false;
} else if (key == "--license-invalid") {
args.license_valid = false;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_app_startup(int argc, char** argv)
{
PlanAppStartupArgs args;
const auto status = parse_plan_app_startup_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-app-startup", status.message);
return 2;
}
const auto plan = pp::app::plan_app_startup(
args.run_counter,
args.auto_timelapse_enabled,
args.vr_controllers_enabled,
args.license_valid);
if (!plan) {
print_error("plan-app-startup", plan.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-app-startup\""
<< ",\"state\":{\"runCounter\":" << args.run_counter
<< ",\"autoTimelapseEnabled\":" << json_bool(args.auto_timelapse_enabled)
<< ",\"vrControllersEnabled\":" << json_bool(args.vr_controllers_enabled)
<< ",\"licenseValid\":" << json_bool(args.license_valid)
<< "},\"plan\":{\"previousRunCounter\":" << plan.value().previous_run_counter
<< ",\"nextRunCounter\":" << plan.value().next_run_counter
<< ",\"savePreferences\":" << json_bool(plan.value().save_preferences)
<< ",\"startTimelapse\":" << json_bool(plan.value().start_timelapse)
<< ",\"vrControllersEnabled\":" << json_bool(plan.value().vr_controllers_enabled)
<< ",\"showLicenseWarning\":" << json_bool(plan.value().show_license_warning)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_tools_menu_args(
int argc,
char** argv,
@@ -8492,6 +8565,10 @@ int main(int argc, char** argv)
return plan_app_preferences(argc, argv);
}
if (command == "plan-app-startup") {
return plan_app_startup(argc, argv);
}
if (command == "plan-app-status") {
return plan_app_status(argc, argv);
}