diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 2c0c04a..dc55bfc 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -552,8 +552,9 @@ Known local toolchain state: render-target binding hooks, render platform hint hooks, render-capture frame hooks, render debug callback hooks, per-frame platform hooks, picker callbacks, recording cleanup, exported-image publishing, persistent storage - flushing, document browse roots, native UI/window state saving, live - asset/layout reload policy, diagnostic stacktrace/crash hooks, + flushing, document browse roots, working-directory picker policy and + display-path formatting, native UI/window state saving, live asset/layout + reload policy, diagnostic stacktrace/crash hooks, SonarPen availability/startup, PPBR export data-directory policy, prepared-file writable target selection, network TLS verification policy, and prepared-file save/download handoff; PPBR @@ -613,6 +614,10 @@ Known local toolchain state: app-close, save, save-as, save-version, and save-before-workflow decisions as JSON and is covered for clean, dirty, already-prompting, missing-canvas, new-document, save-as, save-version, and dirty-save-version states. +- Save, New Document, and Browse dialogs now use `PlatformServices` for + working-directory picker availability and displayed absolute-path formatting, + so `src/node_dialog_open.cpp` and `src/node_dialog_browse.cpp` no longer own + desktop-only path picker branches. - `pp_app_core_document_route_tests` covers the app document-open route contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, and malformed paths before the live `App::open_document` performs UI or diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 551c51b..09925ae 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`, `App::show_cursor`/`App::hide_cursor` dispatch through `PlatformServices` without platform guards, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter | 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 | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, and SonarPen availability/startup 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, including the retained iOS SonarPen bridge and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | 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_export_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 | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, working-directory picker/display-path policy, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, and SonarPen availability/startup 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, including the retained macOS directory picker/display-path behavior, retained iOS SonarPen bridge, and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | 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_export_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-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings | | DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and clears legacy `ActionManager` history through the history bridge | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or direct `ActionManager` history clearing | | DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch and layer panel operation planning/execution dispatch now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`, but that shared bridge still mutates legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and `ActionManager` undo entries | Preserve existing UI/canvas behavior while document layer commands and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution is owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely | @@ -57,8 +57,8 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters | | DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, the app-owned `upload`/`download`/license curl helpers now ask `PlatformServices` for the Android TLS-verification bypass policy, and retained `Asset::open_url`, `LogRemote::net_init`, and `NodeDialogCloud::load_thumbs_thread` curl sites consume the `pp_platform_api` default TLS policy helper instead of spelling Android branches locally, but the bridge still uses legacy save-before-upload, app-owned curl helpers instead of an injected network service, progress/message UI, OpenGL context guarding, `NodeDialogCloud`, `Canvas` project open, layer refresh, and `ActionManager` reset | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pp_platform_api_tests`; `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`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, TLS policy, save-before-upload, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters | | DEBT-0039 | Open | Modernization | Document-open planning and execution dispatch now consume pure `pp_app_core` through `App::open_document`, `pano_cli plan-open-route`, `DocumentOpenServices`, and `src/legacy_document_open_services.*`, but the bridge still opens ABR/PPBR import prompts before delegating import execution to `src/legacy_brush_package_import_services.*`, applies unsaved-project discard prompts, calls legacy project-open execution, refreshes layer UI, updates the app title, and clears legacy history directly | Preserve current file-open/import behavior while document loading and brush import move toward app/document/asset/UI services | `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli plan-open-route --path D:/Paint/Scenes/demo.ppi --unsaved`; `pano_cli plan-open-route --path D:/Paint/Brushes/clouds.ABR --unsaved`; `ctest --preset desktop-fast --build-config Debug` | Brush import prompting, project-open execution, unsaved-project discard prompting, layer refresh, title updates, and history clearing are owned by injected app/document/asset/UI services with `App::open_document` acting only as an adapter | -| DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy message boxes/save dialogs, calls `Canvas::I->project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `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`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters | -| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `NewDocumentServices`, and `src/legacy_document_session_services.*`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter | +| DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still opens legacy message boxes/save dialogs, calls `Canvas::I->project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `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`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters | +| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter | | DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy overwrite prompts, calls legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `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 simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters | | DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | @@ -68,7 +68,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | | DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly | | DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through `PlatformServices`, but non-Windows execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch | -| DEBT-0051 | Open | Modernization | Document browser search roots now dispatch through `PlatformServices`, but iOS `Inbox` inclusion still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve current iOS document import/browse behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | +| DEBT-0051 | Open | Modernization | Document browser search roots and Browse dialog working-directory picker visibility/path formatting now dispatch through `PlatformServices`, but iOS `Inbox` inclusion and macOS directory picker/display-path behavior still live in `src/platform_legacy/legacy_platform_services.*` | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | | DEBT-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge | Preserve current Windows/macOS UI persistence while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple package smoke once root Apple builds exist | UI/window state persistence is owned by injected platform services with no legacy adapter branch | | DEBT-0053 | Open | Modernization | Prepared-file writable target selection and prepared-file export-dialog policy now dispatch through `PlatformServices`, but iOS/Web target selection still lives in `src/platform_legacy/legacy_platform_services.*` | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection, export-dialog policy, and save/download handoff are owned by injected platform services with no legacy adapter branch | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 377dfcf..7931864 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -680,6 +680,10 @@ removing those direct platform calls from `Canvas` and brush preset storage. Document-browser search root selection now dispatches through `PlatformServices`, preserving the iOS `Inbox` root in the legacy adapter while removing the iOS-specific branch from `App::dialog_browse`. +Save, New Document, and Browse dialog working-directory picker availability and +display-path formatting now also dispatch through `PlatformServices`, removing +desktop-only branches and Win32/macOS path formatting from those UI nodes while +preserving Windows and macOS picker behavior in platform adapters. Native UI/window state saving now dispatches through `PlatformServices`, preserving Windows window placement persistence in `WindowsPlatformServices` and macOS UI state persistence in the legacy adapter while removing platform @@ -1736,6 +1740,7 @@ Results: dispatch, render debug callback dispatch, render-capture frame hook dispatch, recording cleanup dispatch, exported-image publish dispatch, persistent storage flush dispatch, document browse-root dispatch, + working-directory picker policy and display-path formatting dispatch, native UI/window state save dispatch, prepared-file writable target dispatch, prepared-file export-dialog policy dispatch, work-directory document export collection policy dispatch, network TLS verification policy dispatch, diff --git a/src/app.h b/src/app.h index ab84d41..f9fedd6 100644 --- a/src/app.h +++ b/src/app.h @@ -184,6 +184,8 @@ public: void pick_file_save(const std::string& type, const std::string& default_name, std::function writer, std::function callback); void pick_file_save(std::vector types, std::function callback); + [[nodiscard]] bool supports_working_directory_picker() const; + [[nodiscard]] std::string format_working_directory_path(std::string_view path) const; [[nodiscard]] bool uses_prepared_file_writes() const; [[nodiscard]] bool uses_work_directory_document_export_collections() const; [[nodiscard]] bool disables_network_tls_verification() const; diff --git a/src/app_events.cpp b/src/app_events.cpp index 88030b6..50c2eec 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -217,6 +217,16 @@ void App::pick_dir(std::function callback) active_platform_services().pick_directory(std::move(callback)); } +bool App::supports_working_directory_picker() const +{ + return active_platform_services().supports_working_directory_picker(); +} + +std::string App::format_working_directory_path(std::string_view path) const +{ + return active_platform_services().format_working_directory_path(path); +} + void App::display_file(std::string path) { if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path) diff --git a/src/node_dialog_browse.cpp b/src/node_dialog_browse.cpp index 309943c..eb864c3 100644 --- a/src/node_dialog_browse.cpp +++ b/src/node_dialog_browse.cpp @@ -69,32 +69,28 @@ void NodeDialogBrowse::init_controls() }; container = find("files-list"); init_list(); -#if defined(_WIN32) || defined(__OSX__) - static char path_buffer[256]; - btn_path = find("btn-path"); - btn_path->on_click = [this](Node*){ - App::I->pick_dir([this](std::string path){ - LOG("change working path to %s", path.c_str()); - App::I->work_path = path; -#ifdef _WIN32 - GetFullPathNameA(path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(path.c_str(), path_buffer); -#endif - working_path->set_text_format("Destination dir: %s", path_buffer); - search_paths = {path}; - clear_list(); - init_list(); - }); - }; - working_path = find("path"); -#ifdef _WIN32 - GetFullPathNameA(App::I->work_path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(App::I->work_path.c_str(), path_buffer); -#endif - working_path->set_text_format("Working dir: %s", path_buffer); -#endif + if (App::I->supports_working_directory_picker()) + { + btn_path = find("btn-path"); + btn_path->on_click = [this](Node*) { + App::I->pick_dir([this](std::string path) { + LOG("change working path to %s", path.c_str()); + App::I->work_path = path; + const auto display_path = App::I->format_working_directory_path(path); + working_path->set_text_format( + "Destination dir: %s", + display_path.c_str()); + search_paths = { path }; + clear_list(); + init_list(); + }); + }; + working_path = find("path"); + const auto display_path = App::I->format_working_directory_path(App::I->work_path); + working_path->set_text_format( + "Working dir: %s", + display_path.c_str()); + } // if (auto* first = (NodeDialogBrowseItem*)container->get_child_at(0)) // { // first->on_selected(first); diff --git a/src/node_dialog_open.cpp b/src/node_dialog_open.cpp index 16e83f0..faea6e6 100644 --- a/src/node_dialog_open.cpp +++ b/src/node_dialog_open.cpp @@ -188,29 +188,25 @@ void NodeDialogSave::init_controls() if (btn_ok->on_click) btn_ok->on_click(btn_ok); }; -#if defined(_WIN32) || defined(__OSX__) - static char path_buffer[256]; - btn_path = find("btn-path"); - btn_path->on_click = [this](Node*){ - App::I->pick_dir([this](std::string path){ - LOG("change working path to %s", path.c_str()); - App::I->work_path = path; -#ifdef _WIN32 - GetFullPathNameA(path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(path.c_str(), path_buffer); -#endif - working_path->set_text_format("Working dir: %s", path_buffer); - }); - }; - working_path = find("path"); -#ifdef _WIN32 - GetFullPathNameA(App::I->work_path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(App::I->work_path.c_str(), path_buffer); -#endif - working_path->set_text_format("Working dir: %s", path_buffer); -#endif + if (App::I->supports_working_directory_picker()) + { + btn_path = find("btn-path"); + btn_path->on_click = [this](Node*) { + App::I->pick_dir([this](std::string path) { + LOG("change working path to %s", path.c_str()); + App::I->work_path = path; + const auto display_path = App::I->format_working_directory_path(path); + working_path->set_text_format( + "Working dir: %s", + display_path.c_str()); + }); + }; + working_path = find("path"); + const auto display_path = App::I->format_working_directory_path(App::I->work_path); + working_path->set_text_format( + "Working dir: %s", + display_path.c_str()); + } } void NodeDialogSave::loaded() { @@ -253,29 +249,25 @@ void NodeDialogNewDoc::init_controls() if (btn_ok->on_click) btn_ok->on_click(btn_ok); }; -#if defined(_WIN32) || defined(__OSX__) - static char path_buffer[256]; - btn_path = find("btn-path"); - btn_path->on_click = [this](Node*){ - App::I->pick_dir([this](std::string path){ - LOG("change working path to %s", path.c_str()); - App::I->work_path = path; -#ifdef _WIN32 - GetFullPathNameA(path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(path.c_str(), path_buffer); -#endif - working_path->set_text_format("Working dir: %s", path_buffer); - }); - }; - working_path = find("path"); -#ifdef _WIN32 - GetFullPathNameA(App::I->work_path.c_str(), sizeof(path_buffer), path_buffer, nullptr); -#else - realpath(App::I->work_path.c_str(), path_buffer); -#endif - working_path->set_text_format("Working dir: %s", path_buffer); -#endif + if (App::I->supports_working_directory_picker()) + { + btn_path = find("btn-path"); + btn_path->on_click = [this](Node*) { + App::I->pick_dir([this](std::string path) { + LOG("change working path to %s", path.c_str()); + App::I->work_path = path; + const auto display_path = App::I->format_working_directory_path(path); + working_path->set_text_format( + "Working dir: %s", + display_path.c_str()); + }); + }; + working_path = find("path"); + const auto display_path = App::I->format_working_directory_path(App::I->work_path); + working_path->set_text_format( + "Working dir: %s", + display_path.c_str()); + } } void NodeDialogNewDoc::loaded() { diff --git a/src/platform_api/platform_services.h b/src/platform_api/platform_services.h index caaf624..da5d054 100644 --- a/src/platform_api/platform_services.h +++ b/src/platform_api/platform_services.h @@ -63,6 +63,8 @@ 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; + [[nodiscard]] virtual bool supports_working_directory_picker() = 0; + [[nodiscard]] virtual std::string format_working_directory_path(std::string_view path) = 0; [[nodiscard]] virtual bool uses_prepared_file_writes() = 0; [[nodiscard]] virtual bool uses_work_directory_document_export_collections() = 0; [[nodiscard]] virtual bool disables_network_tls_verification() = 0; diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index 755057a..ff8fcc4 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -432,6 +432,25 @@ public: #endif } + [[nodiscard]] bool supports_working_directory_picker() override + { +#if defined(__OSX__) + return true; +#else + return false; +#endif + } + + [[nodiscard]] std::string format_working_directory_path(std::string_view path) override + { +#if defined(__OSX__) + char path_buffer[4096] = {}; + if (realpath(std::string(path).c_str(), path_buffer)) + return path_buffer; +#endif + return std::string(path); + } + [[nodiscard]] bool uses_prepared_file_writes() override { #if __IOS__ || __WEB__ diff --git a/src/platform_windows/windows_platform_services.cpp b/src/platform_windows/windows_platform_services.cpp index f87d47d..18395f2 100644 --- a/src/platform_windows/windows_platform_services.cpp +++ b/src/platform_windows/windows_platform_services.cpp @@ -449,6 +449,24 @@ public: invoke_selected_path(path, callback); } + [[nodiscard]] bool supports_working_directory_picker() override + { + return true; + } + + [[nodiscard]] std::string format_working_directory_path(std::string_view path) override + { + char path_buffer[MAX_PATH] = {}; + const auto length = GetFullPathNameA( + std::string(path).c_str(), + static_cast(sizeof(path_buffer)), + path_buffer, + nullptr); + if (length > 0 && length < sizeof(path_buffer)) + return path_buffer; + return std::string(path); + } + [[nodiscard]] bool uses_prepared_file_writes() override { return false; diff --git a/tests/platform_api/platform_services_tests.cpp b/tests/platform_api/platform_services_tests.cpp index a3c65d8..80dbeba 100644 --- a/tests/platform_api/platform_services_tests.cpp +++ b/tests/platform_api/platform_services_tests.cpp @@ -212,6 +212,19 @@ public: callback(directory_path); } + [[nodiscard]] bool supports_working_directory_picker() override + { + ++working_directory_picker_support_checks; + return working_directory_picker_supported; + } + + [[nodiscard]] std::string format_working_directory_path(std::string_view path) override + { + ++working_directory_path_format_requests; + last_working_directory_path.assign(path); + return formatted_working_directory_path; + } + [[nodiscard]] bool uses_prepared_file_writes() override { ++prepared_file_write_policy_checks; @@ -306,6 +319,8 @@ public: int pick_file_requests = 0; int pick_save_file_requests = 0; int pick_directory_requests = 0; + int working_directory_picker_support_checks = 0; + int working_directory_path_format_requests = 0; int prepared_file_write_policy_checks = 0; int document_export_collection_policy_checks = 0; int network_tls_policy_checks = 0; @@ -318,6 +333,7 @@ public: bool keyboard_visible = false; bool prepared_file_saved = true; bool prepared_file_writes = true; + bool working_directory_picker_supported = false; bool work_directory_document_export_collections = false; bool network_tls_verification_disabled = false; bool ppbr_export_data_directory_override = false; @@ -341,6 +357,8 @@ public: std::string picker_path = "D:/Paint/import.png"; std::string save_path = "D:/Paint/export.ppi"; std::string directory_path = "D:/Paint"; + std::string last_working_directory_path; + std::string formatted_working_directory_path = "D:/Paint/Absolute"; pp::platform::PlatformStoragePaths storage_paths{ "D:/Paint", "D:/Paint/work", @@ -592,6 +610,22 @@ void platform_services_dispatch_picker_callbacks(pp::tests::Harness& harness) PP_EXPECT(harness, fake.save_file_types.size() == 1); } +void platform_services_dispatch_working_directory_picker_policy(pp::tests::Harness& harness) +{ + FakePlatformServices fake("unused"); + pp::platform::PlatformServices& services = fake; + + PP_EXPECT(harness, !services.supports_working_directory_picker()); + fake.working_directory_picker_supported = true; + PP_EXPECT(harness, services.supports_working_directory_picker()); + + const auto formatted = services.format_working_directory_path("D:/Paint"); + PP_EXPECT(harness, formatted == "D:/Paint/Absolute"); + PP_EXPECT(harness, fake.last_working_directory_path == "D:/Paint"); + PP_EXPECT(harness, fake.working_directory_picker_support_checks == 2); + PP_EXPECT(harness, fake.working_directory_path_format_requests == 1); +} + void platform_services_dispatch_prepared_file_save(pp::tests::Harness& harness) { FakePlatformServices fake("unused"); @@ -714,6 +748,9 @@ int main() harness.run("platform services dispatch frame hooks", platform_services_dispatch_frame_hooks); 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 working directory picker policy", + platform_services_dispatch_working_directory_picker_policy); harness.run("platform services dispatch prepared file save", platform_services_dispatch_prepared_file_save); harness.run("platform services dispatch writable file target", platform_services_dispatch_writable_file_target); harness.run(