Route prepared file saves through platform services

This commit is contained in:
2026-06-03 04:29:58 +02:00
parent e10e16f491
commit 2ea850cbcc
10 changed files with 109 additions and 27 deletions

View File

@@ -446,14 +446,13 @@ Known local toolchain state:
platform clipboard bridges. platform clipboard bridges.
- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for - `pp_platform_api` exposes the SDK-free `PlatformServices` interface for
clipboard text, cursor visibility, virtual-keyboard visibility, external clipboard text, cursor visibility, virtual-keyboard visibility, external
file display, file sharing, and picker callbacks; Windows live app execution file display, file sharing, picker callbacks, and prepared-file
now uses injected `WindowsPlatformServices` from save/download handoff; Windows live app execution now uses injected
`WindowsPlatformServices` from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`, `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`,
while non-Windows platforms still reach retained platform bridges through while non-Windows platforms still reach retained platform bridges through
the debt-tracked adapter isolated in the debt-tracked adapter isolated in
`src/platform_legacy/legacy_platform_services.*`. The iOS/Web `src/platform_legacy/legacy_platform_services.*`.
save-with-writer overload remains a separate app method until export handoff
is isolated.
- `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

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -475,9 +475,10 @@ Windows installs an injected `WindowsPlatformServices` implementation from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`; `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`;
other platforms still route through the debt-tracked legacy fallback adapter other platforms still route through the debt-tracked legacy fallback adapter
now isolated in `src/platform_legacy/legacy_platform_services.*`, so behavior now isolated in `src/platform_legacy/legacy_platform_services.*`, so behavior
is preserved while their platform shell implementations are extracted. The iOS/Web is preserved while their platform shell implementations are extracted.
save-with-writer overload remains separate because it writes a Prepared-file save/download handoff is now also part of the service contract,
temporary/exported file before handing control to the platform. 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 `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,
@@ -991,11 +992,11 @@ Results:
- `pp_platform_api_tests` passed, covering the SDK-free `PlatformServices` - `pp_platform_api_tests` passed, covering the SDK-free `PlatformServices`
interface for clipboard read/write, empty clipboard writes, cursor interface for clipboard read/write, empty clipboard writes, cursor
visibility dispatch, virtual-keyboard visibility dispatch, external file visibility dispatch, virtual-keyboard visibility dispatch, external file
display dispatch, file sharing dispatch, and picker callback dispatch. The display dispatch, file sharing dispatch, picker callback dispatch, and
live Windows app now consumes this interface through an injected prepared-file save/download callback dispatch. The live Windows app now
`WindowsPlatformServices` instance isolated in consumes this interface through an injected `WindowsPlatformServices`
`src/platform_windows/windows_platform_services.*`; other platforms still instance isolated in `src/platform_windows/windows_platform_services.*`;
use the legacy fallback adapter, now isolated in other platforms still use the legacy fallback adapter, now isolated in
`src/platform_legacy/legacy_platform_services.*` instead of being owned by `src/platform_legacy/legacy_platform_services.*` instead of being owned by
`app_events.cpp`. `app_events.cpp`.
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7 - `panopainter_validate_shaders` passed, validating 25 shader programs and 7

View File

@@ -189,6 +189,10 @@ public:
void pick_dir(std::function<void(std::string path)> callback); void pick_dir(std::function<void(std::string path)> callback);
void display_file(std::string path); void display_file(std::string path);
void share_file(std::string path); void share_file(std::string path);
void save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> callback);
void set_platform_services(pp::platform::PlatformServices* services) noexcept; void set_platform_services(pp::platform::PlatformServices* services) noexcept;
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept; [[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
void showKeyboard(); void showKeyboard();

View File

@@ -23,8 +23,6 @@
#include "oculus_vr.h" #include "oculus_vr.h"
#elif __WEB__ #elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback); void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_pick_file_save(const std::string& path,
const std::string& name, std::function<void(bool)> callback);
void webgl_sync(); void webgl_sync();
#endif #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); //auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
#elif __WEB__ #elif __WEB__
ui_task([=]{ 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 #endif
}); });

View File

@@ -31,11 +31,6 @@ namespace {
} }
#ifdef __WEB__
void webgl_pick_file_save(const std::string& path,
const std::string& name, std::function<void(bool)> callback);
#endif
namespace { namespace {
[[nodiscard]] pp::platform::PlatformServices& active_platform_services() [[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::string path = tmp_path + "/" + default_name + ext;
std::thread([=]{ std::thread([=]{
writer(path); writer(path);
dispatch_async(dispatch_get_main_queue(), ^{ save_prepared_file(path, default_name + ext, callback);
[ios_view pick_file_save:path];
});
callback(path, true);
}).detach(); }).detach();
} }
#elif __WEB__ #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; auto path = data_path + "/" + default_name + "." + type;
LOG("App::pick_file_save %s", path.c_str()); LOG("App::pick_file_save %s", path.c_str());
writer(path); writer(path);
webgl_pick_file_save(path, default_name + "." + type, callback); save_prepared_file(path, default_name + "." + type, std::move(callback));
} }
#else #else
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback) void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
@@ -236,6 +228,19 @@ void App::share_file(std::string path)
active_platform_services().share_file(path); active_platform_services().share_file(path);
} }
void App::save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> 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) bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
{ {
redraw = true; redraw = true;

View File

@@ -8,6 +8,7 @@
namespace pp::platform { namespace pp::platform {
using PickedPathCallback = std::function<void(std::string path)>; using PickedPathCallback = std::function<void(std::string path)>;
using PreparedFileCallback = std::function<void(std::string path, bool saved)>;
class PlatformServices { class PlatformServices {
public: public:
@@ -23,6 +24,10 @@ public:
virtual void pick_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0; virtual void pick_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
virtual void pick_save_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0; virtual void pick_save_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
virtual void pick_directory(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;
}; };
} }

View File

@@ -202,6 +202,29 @@ public:
}); });
#else #else
(void)value; (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 #endif
} }
}; };

View File

@@ -201,6 +201,15 @@ public:
const std::string path = open_directory(); const std::string path = open_directory();
invoke_selected_path(path, callback); 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);
}
}; };
} }

View File

@@ -78,6 +78,17 @@ public:
callback(directory_path); 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_reads = 0;
int clipboard_writes = 0; int clipboard_writes = 0;
int cursor_updates = 0; int cursor_updates = 0;
@@ -88,10 +99,14 @@ public:
int pick_file_requests = 0; int pick_file_requests = 0;
int pick_save_file_requests = 0; int pick_save_file_requests = 0;
int pick_directory_requests = 0; int pick_directory_requests = 0;
int save_prepared_file_requests = 0;
bool cursor_visible = false; bool cursor_visible = false;
bool keyboard_visible = false; bool keyboard_visible = false;
bool prepared_file_saved = true;
std::string displayed_path; std::string displayed_path;
std::string shared_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 picker_path = "D:/Paint/import.png";
std::string save_path = "D:/Paint/export.ppi"; std::string save_path = "D:/Paint/export.ppi";
std::string directory_path = "D:/Paint"; 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); 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() 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 visibility updates", platform_services_dispatch_visibility_updates);
harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions); 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 picker callbacks", platform_services_dispatch_picker_callbacks);
harness.run("platform services dispatch prepared file save", platform_services_dispatch_prepared_file_save);
return harness.finish(); return harness.finish();
} }