diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 474c1ea..9d2e9dd 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -446,14 +446,13 @@ Known local toolchain state: platform clipboard bridges. - `pp_platform_api` exposes the SDK-free `PlatformServices` interface for clipboard text, cursor visibility, virtual-keyboard visibility, external - file display, file sharing, and picker callbacks; Windows live app execution - now uses injected `WindowsPlatformServices` from + file display, file sharing, picker callbacks, and prepared-file + save/download handoff; Windows live app execution now uses injected + `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`, while non-Windows platforms still reach retained platform bridges through the debt-tracked adapter isolated in - `src/platform_legacy/legacy_platform_services.*`. The iOS/Web - save-with-writer overload remains a separate app method until export handoff - is isolated. + `src/platform_legacy/legacy_platform_services.*`. - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, new-document warning, publish prompt, and save-before-upload planning as JSON; the live cloud upload command consumes the same start contract before diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index c60375d..5eaaef8 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -35,7 +35,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | | DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter from `App::show_cursor` and `App::hide_cursor` | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by injected `pp_platform_*` services for every supported platform | | DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`, and Windows live execution uses injected `WindowsPlatformServices`, but Apple/Android clipboard execution still reaches retained fallback adapter branches from `App::clipboard_get_text` and `App::clipboard_set_text` | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard execution is owned by injected `pp_platform_*` services for every supported platform | -| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, and `App::pick_dir` now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches; the iOS/Web save-with-writer overload remains separate until export handoff is isolated | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell | +| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, and prepared-file save/download handoff now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 62e3ed7..0b7c5fc 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -475,9 +475,10 @@ Windows installs an injected `WindowsPlatformServices` implementation from `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`; other platforms still route through the debt-tracked legacy fallback adapter now isolated in `src/platform_legacy/legacy_platform_services.*`, so behavior -is preserved while their platform shell implementations are extracted. The iOS/Web -save-with-writer overload remains separate because it writes a -temporary/exported file before handing control to the platform. +is preserved while their platform shell implementations are extracted. +Prepared-file save/download handoff is now also part of the service contract, +so iOS/Web export completion routes through `PlatformServices` after the app +writes the temporary/exported payload. `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 prompt, and dirty-document save-before-upload states before legacy UI, canvas, @@ -991,11 +992,11 @@ Results: - `pp_platform_api_tests` passed, covering the SDK-free `PlatformServices` interface for clipboard read/write, empty clipboard writes, cursor visibility dispatch, virtual-keyboard visibility dispatch, external file - display dispatch, file sharing dispatch, and picker callback dispatch. The - live Windows app now consumes this interface through an injected - `WindowsPlatformServices` instance isolated in - `src/platform_windows/windows_platform_services.*`; other platforms still - use the legacy fallback adapter, now isolated in + display dispatch, file sharing dispatch, picker callback dispatch, and + prepared-file save/download callback dispatch. The live Windows app now + consumes this interface through an injected `WindowsPlatformServices` + instance isolated in `src/platform_windows/windows_platform_services.*`; + other platforms still use the legacy fallback adapter, now isolated in `src/platform_legacy/legacy_platform_services.*` instead of being owned by `app_events.cpp`. - `panopainter_validate_shaders` passed, validating 25 shader programs and 7 diff --git a/src/app.h b/src/app.h index 51584bc..3b54367 100644 --- a/src/app.h +++ b/src/app.h @@ -189,6 +189,10 @@ public: void pick_dir(std::function callback); void display_file(std::string path); void share_file(std::string path); + void save_prepared_file( + std::string path, + std::string suggested_name, + std::function callback); void set_platform_services(pp::platform::PlatformServices* services) noexcept; [[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept; void showKeyboard(); diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index cf985f7..7a14de6 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -23,8 +23,6 @@ #include "oculus_vr.h" #elif __WEB__ void webgl_pick_file(std::function callback); -void webgl_pick_file_save(const std::string& path, - const std::string& name, std::function callback); void webgl_sync(); #endif @@ -459,7 +457,7 @@ void App::dialog_export(std::string ext) //auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo); #elif __WEB__ ui_task([=]{ - webgl_pick_file_save(target.path, target.suggested_name, [](bool success){ }); + save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { }); }); #endif }); diff --git a/src/app_events.cpp b/src/app_events.cpp index 6fcf516..89ea981 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -31,11 +31,6 @@ namespace { } -#ifdef __WEB__ -void webgl_pick_file_save(const std::string& path, - const std::string& name, std::function callback); -#endif - namespace { [[nodiscard]] pp::platform::PlatformServices& active_platform_services() @@ -187,10 +182,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam std::string path = tmp_path + "/" + default_name + ext; std::thread([=]{ writer(path); - dispatch_async(dispatch_get_main_queue(), ^{ - [ios_view pick_file_save:path]; - }); - callback(path, true); + save_prepared_file(path, default_name + ext, callback); }).detach(); } #elif __WEB__ @@ -201,7 +193,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam auto path = data_path + "/" + default_name + "." + type; LOG("App::pick_file_save %s", path.c_str()); writer(path); - webgl_pick_file_save(path, default_name + "." + type, callback); + save_prepared_file(path, default_name + "." + type, std::move(callback)); } #else void App::pick_file_save(std::vector types, std::function callback) @@ -236,6 +228,19 @@ void App::share_file(std::string path) active_platform_services().share_file(path); } +void App::save_prepared_file( + std::string path, + std::string suggested_name, + std::function callback) +{ + active_platform_services().save_prepared_file( + path, + suggested_name, + [callback = std::move(callback)](std::string saved_path, bool saved) { + callback(saved_path, saved); + }); +} + bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser) { redraw = true; diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h index 3e41782..fc1c9d5 100644 --- a/src/platform_api/platform_services.h +++ b/src/platform_api/platform_services.h @@ -8,6 +8,7 @@ namespace pp::platform { using PickedPathCallback = std::function; +using PreparedFileCallback = std::function; class PlatformServices { public: @@ -23,6 +24,10 @@ public: virtual void pick_file(std::vector file_types, PickedPathCallback callback) = 0; virtual void pick_save_file(std::vector file_types, PickedPathCallback callback) = 0; virtual void pick_directory(PickedPathCallback callback) = 0; + virtual void save_prepared_file( + std::string_view path, + std::string_view suggested_name, + PreparedFileCallback callback) = 0; }; } diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index 424dbba..8fa4990 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -202,6 +202,29 @@ public: }); #else (void)value; +#endif + } + + void save_prepared_file( + std::string_view path, + std::string_view suggested_name, + pp::platform::PreparedFileCallback callback) override + { + const std::string value(path); + const std::string name(suggested_name); +#ifdef __IOS__ + (void)name; + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view pick_file_save:value]; + }); + callback(value, true); +#elif __WEB__ + webgl_pick_file_save(value, name, [callback = std::move(callback), value](bool success) { + callback(value, success); + }); +#else + (void)name; + callback(value, false); #endif } }; diff --git a/src/platform_windows/windows_platform_services.cpp b/src/platform_windows/windows_platform_services.cpp index 0df5a29..9b8a219 100644 --- a/src/platform_windows/windows_platform_services.cpp +++ b/src/platform_windows/windows_platform_services.cpp @@ -201,6 +201,15 @@ public: const std::string path = open_directory(); invoke_selected_path(path, callback); } + + void save_prepared_file( + std::string_view path, + std::string_view suggested_name, + pp::platform::PreparedFileCallback callback) override + { + (void)suggested_name; + callback(std::string(path), false); + } }; } diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index cd28e6d..2aa69df 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -78,6 +78,17 @@ public: callback(directory_path); } + void save_prepared_file( + std::string_view path, + std::string_view suggested_name, + pp::platform::PreparedFileCallback callback) override + { + ++save_prepared_file_requests; + prepared_file_path.assign(path); + prepared_file_name.assign(suggested_name); + callback(prepared_file_path, prepared_file_saved); + } + int clipboard_reads = 0; int clipboard_writes = 0; int cursor_updates = 0; @@ -88,10 +99,14 @@ public: int pick_file_requests = 0; int pick_save_file_requests = 0; int pick_directory_requests = 0; + int save_prepared_file_requests = 0; bool cursor_visible = false; bool keyboard_visible = false; + bool prepared_file_saved = true; std::string displayed_path; std::string shared_path; + std::string prepared_file_path; + std::string prepared_file_name; std::string picker_path = "D:/Paint/import.png"; std::string save_path = "D:/Paint/export.ppi"; std::string directory_path = "D:/Paint"; @@ -180,6 +195,28 @@ void platform_services_dispatch_picker_callbacks(pp::tests::Harness& harness) PP_EXPECT(harness, fake.save_file_types.size() == 1); } +void platform_services_dispatch_prepared_file_save(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + std::string saved_path; + bool saved = false; + + services.save_prepared_file( + "D:/Paint/export.mp4", + "export.mp4", + [&](std::string path, bool success) { + saved_path = std::move(path); + saved = success; + }); + + PP_EXPECT(harness, fake.save_prepared_file_requests == 1); + PP_EXPECT(harness, fake.prepared_file_path == "D:/Paint/export.mp4"); + PP_EXPECT(harness, fake.prepared_file_name == "export.mp4"); + PP_EXPECT(harness, saved_path == "D:/Paint/export.mp4"); + PP_EXPECT(harness, saved); +} + } int main() @@ -190,5 +227,6 @@ int main() harness.run("platform services dispatch visibility updates", platform_services_dispatch_visibility_updates); harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions); harness.run("platform services dispatch picker callbacks", platform_services_dispatch_picker_callbacks); + harness.run("platform services dispatch prepared file save", platform_services_dispatch_prepared_file_save); return harness.finish(); }