diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index de0368d..736371a 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets. | Platform/Target | Current Entrypoint | Notes | | --- | --- | --- | -| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard/display/share service contracts owned by `pp_platform_api`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | +| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard/display/share/picker service contracts owned by `pp_platform_api`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | | Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging | | macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today | | iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today | @@ -446,9 +446,10 @@ 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, and file sharing; live app execution now reaches retained - platform bridges through the debt-tracked legacy adapter in - `app_events.cpp`. + file display, file sharing, and picker callbacks; live app execution now + reaches retained platform bridges through the debt-tracked legacy adapter in + `app_events.cpp`. The iOS/Web 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, new-document warning, publish prompt, and save-before-upload planning as JSON; the live cloud upload command consumes the same start contract before @@ -487,8 +488,8 @@ Known local toolchain state: planning before platform clipboard callbacks. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, - external file display, and file sharing without platform SDK headers or a - window. + external file display, file sharing, and picker callbacks without platform + SDK headers or a window. - `pp_app_core_document_cloud_tests` covers cloud upload no-canvas, new-document warning, clean publish prompt, and dirty save-before-upload decisions, plus cloud browse no-canvas/show-browser and selected-download diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 4a1cfa8..1044ddc 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -67,7 +67,7 @@ and validation command. | --- | --- | --- | --- | | Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback | | Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback | -| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share dispatch tests, picker callback tests, platform smoke or mocked service | +| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service | | Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke | | 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 | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 3d3c970..6db2983 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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-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 execution, picker callback execution, display-file platform execution, keyboard 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-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, `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-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-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 | @@ -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`, but live cursor execution still reaches retained Win32/macOS platform bridges 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 `pp_platform_*` services and live app code depends on an injected platform interface instead of direct singleton/platform calls | | 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`, but live clipboard execution still reaches retained Win32/Apple/Android platform bridges 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 `pp_platform_*` services and live app code depends on an injected platform interface instead of direct singleton/platform calls | -| 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`, and `App::share_file` now call the SDK-free `pp::platform::PlatformServices` interface, but the live implementation is still a legacy adapter in `app_events.cpp` that forwards to retained Win32/Apple/Android 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 the `app_events.cpp` legacy adapter with injected `pp_platform_*` service implementations owned by each 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`, and `App::pick_dir` now call the SDK-free `pp::platform::PlatformServices` interface, but the live implementation is still a legacy adapter in `app_events.cpp` that forwards to retained Win32/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 the `app_events.cpp` legacy adapter with injected `pp_platform_*` service implementations owned by each platform shell | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 95d0b62..043cde4 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -470,10 +470,12 @@ app-core clipboard text decisions used by live clipboard get/set requests before retained platform clipboard bridges continue. `pp_platform_api` now owns a headless `PlatformServices` interface for clipboard text, cursor visibility, virtual-keyboard visibility, external file -display, and file sharing. Live app clipboard/cursor/keyboard/display/share -execution routes through a debt-tracked legacy adapter in `app_events.cpp`, so -behavior is preserved while later platform shell implementations can replace -direct bridge calls. +display, file sharing, image/file/save-file pickers, and directory pickers. +Live app clipboard/cursor/keyboard/display/share/picker execution routes +through a debt-tracked legacy adapter in `app_events.cpp`, so behavior is +preserved while later platform shell implementations can replace direct bridge +calls. The iOS/Web save-with-writer overload remains separate because it writes +a temporary/exported file before handing control to the platform. `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, @@ -987,9 +989,9 @@ 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, and file sharing dispatch. The live app now consumes this - interface through the legacy platform adapter for - clipboard/cursor/keyboard/display/share execution. + display dispatch, file sharing dispatch, and picker callback dispatch. The + live app now consumes this interface through the legacy platform adapter for + clipboard/cursor/keyboard/display/share/picker execution. - `panopainter_validate_shaders` passed, validating 25 shader programs and 7 shader includes for stage markers and include graph integrity. - `pp_renderer_gl_capabilities_tests` passed on default MSVC, vcpkg-headless, diff --git a/src/app_events.cpp b/src/app_events.cpp index b9d887d..68e3630 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -63,6 +63,27 @@ void webgl_sync(); namespace { +std::string build_supported_files_filter(const std::vector& types) +{ + std::string filter = "Supported Files ("; + bool first_type = true; + for (const auto& t : types) + { + filter.append(std::string(first_type ? "" : " ,") + "*." + t); + first_type = false; + } + filter.append(")"); + filter.push_back(0); + first_type = true; + for (const auto& t : types) + { + filter.append(std::string(first_type ? "" : ";") + "*." + t); + first_type = false; + } + filter.push_back(0); + return filter; +} + class LegacyPlatformServices final : public pp::platform::PlatformServices { public: [[nodiscard]] std::string clipboard_text() override @@ -123,6 +144,108 @@ public: #endif } + void pick_image(pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + [App::I->ios_view pick_photo:callback]; + }); +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil]; + std::string path = [App::I->osx_view pick_file:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file(callback); +#elif _WIN32 + std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png"); + invoke_picked_path_if_selected(path, callback); +#elif __LINUX__ + if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) + invoke_picked_path_if_selected(p, callback); +#elif __WEB__ + webgl_pick_file(callback); +#else + (void)callback; +#endif + } + + void pick_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + [App::I->ios_view pick_file:fileTypes then:callback]; + }); +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + std::string path = [App::I->osx_view pick_file:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file(callback); +#elif _WIN32 + const std::string filter = build_supported_files_filter(file_types); + std::string path = win32_open_file(filter.c_str()); + invoke_picked_path_if_selected(path, callback); +#elif __LINUX__ + if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) + invoke_picked_path_if_selected(p, callback); +#elif __WEB__ + webgl_pick_file(callback); +#else + (void)file_types; + (void)callback; +#endif + } + + void pick_save_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { +#if __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()]; + for (const auto& t : file_types) + [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; + std::string path = [App::I->osx_view pick_file_save:fileTypes]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + android_pick_file_save(callback); +#elif _WIN32 + const std::string filter = build_supported_files_filter(file_types); + std::string path = win32_save_file(filter.c_str()); + invoke_picked_path_if_selected(path, callback); +#else + (void)file_types; + (void)callback; +#endif + } + + void pick_directory(pp::platform::PickedPathCallback callback) override + { +#ifdef __IOS__ + (void)callback; +#elif __OSX__ + dispatch_async(dispatch_get_main_queue(), ^{ + std::string path = [App::I->osx_view pick_dir]; + invoke_picked_path_if_selected(path, callback); + }); +#elif __ANDROID__ + (void)callback; +#elif _WIN32 + std::string path = win32_open_dir(); + invoke_picked_path_if_selected(path, callback); +#else + (void)callback; +#endif + } + void display_file(std::string_view path) override { const std::string value(path); @@ -271,74 +394,13 @@ void App::hideKeyboard() void App::pick_image(std::function callback) { redraw = true; -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - [ios_view pick_photo:callback]; - }); -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil]; - std::string path = [osx_view pick_file:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file(callback); -#elif _WIN32 - std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png"); - invoke_picked_path_if_selected(path, callback); -#elif __LINUX__ - if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) - invoke_picked_path_if_selected(p, callback); -#elif __WEB__ - webgl_pick_file(callback); -#endif + legacy_platform_services().pick_image(std::move(callback)); } void App::pick_file(std::vector types, std::function callback) { redraw = true; -#ifdef __IOS__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:types.size()]; - for (const auto& t : types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - [ios_view pick_file:fileTypes then:callback]; - }); -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:types.size()]; - for (const auto& t : types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - std::string path = [osx_view pick_file:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file(callback); -#elif _WIN32 - std::string filter = "Supported Files ("; - bool first_type = true; - for (auto& t : types) - { - filter.append(std::string(first_type ? "" : " ,") + "*." + t); - first_type = false; - } - filter.append(")"); - filter.push_back(0); - first_type = true; - for (auto& t : types) - { - filter.append(std::string(first_type ? "" : ";") + "*." + t); - first_type = false; - } - filter.push_back(0); - std::string path = win32_open_file(filter.c_str()); - invoke_picked_path_if_selected(path, callback); -#elif __LINUX__ - if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false)) - invoke_picked_path_if_selected(p, callback); -#elif __WEB__ - webgl_pick_file(callback); -#endif + legacy_platform_services().pick_file(std::move(types), std::move(callback)); } #if __IOS__ @@ -370,57 +432,14 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam void App::pick_file_save(std::vector types, std::function callback) { redraw = true; -#if __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - //NSArray* fileTypes = [NSArray arrayWithObjects:@"ppi", @"PPI", nil]; - NSMutableArray* fileTypes = [NSMutableArray arrayWithCapacity:types.size()]; - for (const auto& t : types) - [fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]]; - std::string path = [osx_view pick_file_save:fileTypes]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - android_pick_file_save(callback); -#elif _WIN32 - std::string filter = "Supported Files ("; - bool first_type = true; - for (auto& t : types) - { - filter.append(std::string(first_type ? "" : " ,") + "*." + t); - first_type = false; - } - filter.append(")"); - filter.push_back(0); - first_type = true; - for (auto& t : types) - { - filter.append(std::string(first_type ? "" : ";") + "*." + t); - first_type = false; - } - filter.push_back(0); - std::string path = win32_save_file(filter.c_str()); - invoke_picked_path_if_selected(path, callback); -#endif + legacy_platform_services().pick_save_file(std::move(types), std::move(callback)); } #endif void App::pick_dir(std::function callback) { redraw = true; -#ifdef __IOS__ - // NOT IMPLEMENTED -#elif __OSX__ - dispatch_async(dispatch_get_main_queue(), ^{ - std::string path = [osx_view pick_dir]; - invoke_picked_path_if_selected(path, callback); - }); -#elif __ANDROID__ - // NOT IMPLEMENTED -#elif _WIN32 - // TODO: to be implemented - std::string path = win32_open_dir(); - invoke_picked_path_if_selected(path, callback); -#endif + legacy_platform_services().pick_directory(std::move(callback)); } void App::display_file(std::string path) diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h index 3a642dc..3e41782 100644 --- a/src/platform_api/platform_services.h +++ b/src/platform_api/platform_services.h @@ -1,10 +1,14 @@ #pragma once +#include #include #include +#include namespace pp::platform { +using PickedPathCallback = std::function; + class PlatformServices { public: virtual ~PlatformServices() = default; @@ -15,6 +19,10 @@ public: virtual void set_virtual_keyboard_visible(bool visible) = 0; virtual void display_file(std::string_view path) = 0; virtual void share_file(std::string_view path) = 0; + virtual void pick_image(PickedPathCallback callback) = 0; + 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; }; } diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index f4fdb78..cd28e6d 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace { @@ -51,16 +52,51 @@ public: shared_path.assign(path); } + void pick_image(pp::platform::PickedPathCallback callback) override + { + ++pick_image_requests; + callback(picker_path); + } + + void pick_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { + ++pick_file_requests; + picked_file_types = std::move(file_types); + callback(picker_path); + } + + void pick_save_file(std::vector file_types, pp::platform::PickedPathCallback callback) override + { + ++pick_save_file_requests; + save_file_types = std::move(file_types); + callback(save_path); + } + + void pick_directory(pp::platform::PickedPathCallback callback) override + { + ++pick_directory_requests; + callback(directory_path); + } + int clipboard_reads = 0; int clipboard_writes = 0; int cursor_updates = 0; int keyboard_updates = 0; int display_file_requests = 0; int share_file_requests = 0; + int pick_image_requests = 0; + int pick_file_requests = 0; + int pick_save_file_requests = 0; + int pick_directory_requests = 0; bool cursor_visible = false; bool keyboard_visible = false; std::string displayed_path; std::string shared_path; + std::string picker_path = "D:/Paint/import.png"; + std::string save_path = "D:/Paint/export.ppi"; + std::string directory_path = "D:/Paint"; + std::vector picked_file_types; + std::vector save_file_types; private: std::string clipboard_value_; @@ -118,6 +154,32 @@ void platform_services_dispatch_file_actions(pp::tests::Harness& harness) PP_EXPECT(harness, fake.shared_path == "D:/Paint/demo.ppi"); } +void platform_services_dispatch_picker_callbacks(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + std::string image_path; + std::string file_path; + std::string save_path; + std::string directory_path; + + services.pick_image([&](std::string path) { image_path = std::move(path); }); + services.pick_file({ "ppi", "ppbr" }, [&](std::string path) { file_path = std::move(path); }); + services.pick_save_file({ "ppi" }, [&](std::string path) { save_path = std::move(path); }); + services.pick_directory([&](std::string path) { directory_path = std::move(path); }); + + PP_EXPECT(harness, fake.pick_image_requests == 1); + PP_EXPECT(harness, fake.pick_file_requests == 1); + PP_EXPECT(harness, fake.pick_save_file_requests == 1); + PP_EXPECT(harness, fake.pick_directory_requests == 1); + PP_EXPECT(harness, image_path == "D:/Paint/import.png"); + PP_EXPECT(harness, file_path == "D:/Paint/import.png"); + PP_EXPECT(harness, save_path == "D:/Paint/export.ppi"); + PP_EXPECT(harness, directory_path == "D:/Paint"); + PP_EXPECT(harness, fake.picked_file_types.size() == 2); + PP_EXPECT(harness, fake.save_file_types.size() == 1); +} + } int main() @@ -127,5 +189,6 @@ int main() harness.run("platform services preserve empty clipboard writes", platform_services_preserve_empty_clipboard_writes); 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); return harness.finish(); }