Plan display file actions in app core

This commit is contained in:
2026-06-03 03:38:14 +02:00
parent 712c28068d
commit 4af55a7d3f
9 changed files with 116 additions and 4 deletions

View File

@@ -430,6 +430,9 @@ Known local toolchain state:
JSON for empty and non-empty file picker results; live image/file/save/ JSON for empty and non-empty file picker results; live image/file/save/
directory picker branches consume the same contract before invoking retained directory picker branches consume the same contract before invoking retained
platform callbacks or legacy picker bridges. platform callbacks or legacy picker bridges.
- `pano_cli plan-display-file` exposes `pp_app_core` external file presentation
planning as JSON for empty and non-empty paths; the live display-file command
consumes the same contract before retained platform open-file bridges.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON; new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before the live cloud upload command consumes the same start contract before
@@ -461,7 +464,8 @@ Known local toolchain state:
- `pp_app_core_document_sharing_tests` covers saved-path gating before platform - `pp_app_core_document_sharing_tests` covers saved-path gating before platform
share execution. share execution.
- `pp_app_core_document_platform_io_tests` covers empty selected-path filtering - `pp_app_core_document_platform_io_tests` covers empty selected-path filtering
and non-empty picked-path callback planning before platform picker callbacks. and non-empty picked-path callback planning, plus empty/non-empty display-file
planning before platform picker/display callbacks.
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
new-document warning, clean publish prompt, and dirty save-before-upload new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download decisions, plus cloud browse no-canvas/show-browser and selected-download

View File

@@ -67,7 +67,7 @@ and validation command.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback | | Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback |
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback | | Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
| Clipboard/file picker/share | `App` platform methods | `pp_app_core`, `pp_platform_*` | Share saved-path and picked-path decision tests, platform smoke or mocked service | | Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_*` | Share saved-path, picked-path, and display-file decision tests, platform smoke or mocked service |
| Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke | | Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke |
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests | | OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate | | Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |

View File

@@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat.
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-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, 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-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/cloud contracts, but document creation/loading, brush import execution, saving, export execution, platform share execution, picker callback 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-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, 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-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/cloud contracts, but document creation/loading, brush import execution, saving, export execution, platform share execution, picker callback execution, display-file platform 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-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 |

View File

@@ -454,6 +454,9 @@ platform branches execute.
`pano_cli plan-picked-path` exposes the app-core selected-path decision used by `pano_cli plan-picked-path` exposes the app-core selected-path decision used by
live image, file, save-file, and directory picker branches before retained live image, file, save-file, and directory picker branches before retained
platform callbacks or legacy picker bridges continue. platform callbacks or legacy picker bridges continue.
`pano_cli plan-display-file` exposes the app-core external file presentation
decision used by live display-file requests before retained platform open-file
bridges continue.
`pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by
the live cloud upload command for missing-canvas, new-document warning, publish the live cloud upload command for missing-canvas, new-document warning, publish
prompt, and dirty-document save-before-upload states before legacy UI, canvas, prompt, and dirty-document save-before-upload states before legacy UI, canvas,
@@ -942,10 +945,14 @@ Results:
decisions as JSON. decisions as JSON.
- `pp_app_core_document_platform_io_tests` passed, covering empty selected-path - `pp_app_core_document_platform_io_tests` passed, covering empty selected-path
filtering and non-empty picked-path callback planning before platform picker filtering and non-empty picked-path callback planning before platform picker
callbacks. callbacks, plus empty/non-empty display-file planning before platform
display callbacks.
- `pano_cli_plan_picked_path_empty_smoke` and - `pano_cli_plan_picked_path_empty_smoke` and
`pano_cli_plan_picked_path_selected_smoke` passed and expose app-core picker `pano_cli_plan_picked_path_selected_smoke` passed and expose app-core picker
selected-path decisions as JSON. selected-path decisions as JSON.
- `pano_cli_plan_display_file_empty_smoke` and
`pano_cli_plan_display_file_selected_smoke` passed and expose app-core
display-file decisions as JSON.
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7 - `panopainter_validate_shaders` passed, validating 25 shader programs and 7
shader includes for stage markers and include graph integrity. shader includes for stage markers and include graph integrity.
- `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless,

View File

@@ -9,6 +9,11 @@ enum class PickedPathAction {
invoke_callback, invoke_callback,
}; };
enum class DisplayFileAction {
ignore_empty_path,
open_external_file,
};
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept [[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
{ {
return path.empty() return path.empty()
@@ -16,4 +21,11 @@ enum class PickedPathAction {
: PickedPathAction::invoke_callback; : PickedPathAction::invoke_callback;
} }
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
{
return path.empty()
? DisplayFileAction::ignore_empty_path
: DisplayFileAction::open_external_file;
}
} }

View File

@@ -311,6 +311,9 @@ void App::pick_dir(std::function<void(std::string path)> callback)
void App::display_file(std::string path) void App::display_file(std::string path)
{ {
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
return;
#ifdef __IOS__ #ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[ios_view display_file:path]; [ios_view display_file:path];

View File

@@ -615,6 +615,18 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-picked-path\".*\"path\":\"D:/Paint/demo.ppi\".*\"decision\":\"invoke-callback\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-picked-path\".*\"path\":\"D:/Paint/demo.ppi\".*\"decision\":\"invoke-callback\"")
add_test(NAME pano_cli_plan_display_file_empty_smoke
COMMAND pano_cli plan-display-file)
set_tests_properties(pano_cli_plan_display_file_empty_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-display-file\".*\"path\":\"\".*\"decision\":\"ignore-empty-path\"")
add_test(NAME pano_cli_plan_display_file_selected_smoke
COMMAND pano_cli plan-display-file --path D:/Paint/export.png)
set_tests_properties(pano_cli_plan_display_file_selected_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-display-file\".*\"path\":\"D:/Paint/export.png\".*\"decision\":\"open-external-file\"")
add_test(NAME pano_cli_simulate_app_session_clean_smoke add_test(NAME pano_cli_simulate_app_session_clean_smoke
COMMAND pano_cli simulate-app-session) COMMAND pano_cli simulate-app-session)
set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES

View File

@@ -17,6 +17,20 @@ void picked_path_invokes_callback_for_nonempty_path(pp::tests::Harness& harness)
pp::app::plan_picked_path("D:/Paint/demo.ppi") == pp::app::PickedPathAction::invoke_callback); pp::app::plan_picked_path("D:/Paint/demo.ppi") == pp::app::PickedPathAction::invoke_callback);
} }
void display_file_ignores_empty_path(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_display_file("") == pp::app::DisplayFileAction::ignore_empty_path);
}
void display_file_opens_nonempty_path(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_display_file("D:/Paint/export.png") == pp::app::DisplayFileAction::open_external_file);
}
} }
int main() int main()
@@ -24,5 +38,7 @@ int main()
pp::tests::Harness harness; pp::tests::Harness harness;
harness.run("picked path ignores empty path", picked_path_ignores_empty_path); harness.run("picked path ignores empty path", picked_path_ignores_empty_path);
harness.run("picked path invokes callback for nonempty path", picked_path_invokes_callback_for_nonempty_path); harness.run("picked path invokes callback for nonempty path", picked_path_invokes_callback_for_nonempty_path);
harness.run("display file ignores empty path", display_file_ignores_empty_path);
harness.run("display file opens nonempty path", display_file_opens_nonempty_path);
return harness.finish(); return harness.finish();
} }

View File

@@ -170,6 +170,10 @@ struct PlanPickedPathArgs {
std::string path; std::string path;
}; };
struct PlanDisplayFileArgs {
std::string path;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -515,6 +519,18 @@ const char* picked_path_action_name(pp::app::PickedPathAction action) noexcept
return "ignore-empty-path"; return "ignore-empty-path";
} }
const char* display_file_action_name(pp::app::DisplayFileAction action) noexcept
{
switch (action) {
case pp::app::DisplayFileAction::ignore_empty_path:
return "ignore-empty-path";
case pp::app::DisplayFileAction::open_external_file:
return "open-external-file";
}
return "ignore-empty-path";
}
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;
@@ -557,6 +573,7 @@ void print_help()
<< " 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-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"
<< " load-project --path FILE\n" << " load-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
<< " record-render [--width N] [--height N] [--exercise-clear]\n" << " record-render [--width N] [--height N] [--exercise-clear]\n"
@@ -1964,6 +1981,43 @@ int plan_picked_path(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_display_file_args(
int argc,
char** argv,
PlanDisplayFileArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_display_file(int argc, char** argv)
{
PlanDisplayFileArgs args;
const auto status = parse_plan_display_file_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-display-file", status.message);
return 2;
}
const auto decision = pp::app::plan_display_file(args.path);
std::cout << "{\"ok\":true,\"command\":\"plan-display-file\""
<< ",\"state\":{\"path\":\"" << json_escape(args.path)
<< "\"},\"decision\":\"" << display_file_action_name(decision)
<< "\"}\n";
return 0;
}
pp::foundation::Status parse_plan_export_target_args( pp::foundation::Status parse_plan_export_target_args(
int argc, int argc,
char** argv, char** argv,
@@ -4139,6 +4193,10 @@ int main(int argc, char** argv)
return plan_picked_path(argc, argv); return plan_picked_path(argc, argv);
} }
if (command == "plan-display-file") {
return plan_display_file(argc, argv);
}
if (command == "load-project") { if (command == "load-project") {
return load_project(argc, argv); return load_project(argc, argv);
} }