diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 7360f83..24a2881 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -837,7 +837,7 @@ Known local toolchain state: options-menu side effects. - `pp_app_core_app_dialog_tests` covers app-level progress/message/input dialog metadata planning, progress initialization, negative progress-total clamping, - message cancel-button policy, input OK-caption propagation, and malformed + message cancel-button/caption policy, input OK-caption propagation, and malformed empty OK-caption rejection without requiring legacy `Node*` dialogs. - `pp_app_core_app_startup_tests` covers startup run-counter increment planning, optional auto-timelapse/license/VR-controller decisions, negative @@ -881,7 +881,9 @@ Known local toolchain state: - `pp_app_core_document_session_tests` covers clean and dirty app session, document-open action planning and executor dispatch/rejection, save-request, close-request executor dispatch/no-op preservation, document-save executor - dispatch/no-op preservation, save-before-workflow executor dispatch, + dispatch/no-op preservation, document-session prompt metadata for close, + save-before-workflow, overwrite, and save-error dialogs, + save-before-workflow executor dispatch, new-document target/resolution/overwrite planning and executor dispatch, document file target, combined save-file overwrite planning and executor dispatch, plus save-version target decisions and executor validation without @@ -903,9 +905,13 @@ Known local toolchain state: - `src/legacy_document_session_services.*` is the current app-shell bridge between `pp_app_core` document-session decisions and live close prompts, save dialogs, save-version routing, existing-project saves, and - save-before-workflow prompts. It also bridges accepted new-document plans to - legacy canvas resize/layer setup, overwrite prompts, title updates, and - keyboard/dialog cleanup. Accepted Save As and Save Version plans now also + save-before-workflow prompts. Close, save-before-workflow, new-document + overwrite, Save As overwrite, and save-error prompt text/captions now come + from the pure document-session prompt catalog exposed through + `pano_cli plan-document-session-prompt` before retained `NodeMessageBox` + creation. It also bridges accepted new-document plans to legacy canvas + resize/layer setup, overwrite prompts, title updates, and keyboard/dialog + cleanup. Accepted Save As and Save Version plans now also route through this bridge before reaching legacy project-save execution, overwrite prompts, document field updates, title updates, and keyboard/dialog cleanup. Retained legacy UI/canvas execution remains tracked by `DEBT-0040`, diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 6c270b0..616995e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -99,6 +99,14 @@ agent or engineer to remove them without reconstructing context from chat. the retained OpenGL startup task in place, then delegates startup resources and runtime side effects through the startup bridge. `pano_cli plan-app-startup-resources` exposes the resource path for automation. +- 2026-06-05: DEBT-0040/0041/0042 were narrowed. Close-unsaved, + save-before-workflow, new-document overwrite, Save As overwrite, and + save-error prompt metadata now comes from a tested pure document-session + prompt catalog consumed by `src/legacy_document_session_services.*`, and + `pano_cli plan-document-session-prompt` exposes the titles, messages, + captions, and cancel visibility for automation. Retained `NodeMessageBox` + creation, callback wiring, project-save execution, and canvas/document + mutation remain open under the same debts. - 2026-06-05: DEBT-0003 was narrowed. Initial surface sizing, redraw/animation update gating, layout tick selection, resize render-target recreation, canvas-stroke draw eligibility, VR UI pass selection, main UI pass selection, @@ -211,9 +219,9 @@ 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.*`, and retained `PBO` allocation/readback/map/unmap/delete operations now route through tested `pp_renderer_gl` dispatch, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, retained `App::rec_loop` readback call sites, 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`; `pp_renderer_gl_capabilities_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 scheduling, 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` CURL helpers now consume `pp_app_core` cloud transfer request/progress planning and the platform TLS-verification bypass policy before retained CURL setup, `pano_cli plan-cloud-transfer` exposes the same missing endpoint, TLS policy, progress-callback, and progress fraction guards, 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, upload form construction, response/error handling, 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`; `pano_cli plan-cloud-transfer --direction download --progress --disable-tls-verification`; `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.*`; 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-0040 | Open | Modernization | Close request, document save, save-before-workflow planning/execution dispatch, and close/save-before/save-error prompt metadata 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`, `pano_cli plan-document-session-prompt`, `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 retained `NodeMessageBox`/save dialogs, wires callbacks directly, 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 plan-document-session-prompt --kind close-unsaved`; `pano_cli plan-document-session-prompt --kind save-before-workflow`; `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 and new-document overwrite prompt metadata now consume pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `pano_cli plan-document-session-prompt`, `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, creates retained `NodeMessageBox` overwrite prompts, 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 plan-document-session-prompt --kind new-document-overwrite`; `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 plus Save As overwrite prompt metadata now consumes pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-document-session-prompt`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`, but the bridge still creates retained `NodeMessageBox` 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-session-prompt --kind file-overwrite --name demo`; `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 | | DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and `Settings::save` directly; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | @@ -229,7 +237,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0055 | Open | Modernization | `src/app.h` now forward-declares retained iOS/macOS/Android/Linux/Web platform handles instead of including platform SDK headers, and full SDK includes are isolated in `src/platform_legacy/legacy_platform_services.cpp`, but the `App` singleton still stores those platform handles directly | Reduce central header platform coupling incrementally without rewriting non-Windows platform entrypoints before Phase 6 | Windows app build; Apple/Android/Linux/Web package smoke once platform root builds are active | Platform handles are owned by injected `pp_platform_*` shell state or services, and `App` has no platform SDK handle fields or platform conditional members | | DEBT-0056 | Open | Modernization | `src/asset.h` now forward-declares Android asset-manager types and uses `Asset::set_android_asset_manager` instead of public mutable manager state, but retained `Asset` still stores Android asset handles and `src/asset.cpp` still performs Android `AAssetManager` reads directly; the current `android-arm64` root preset is headless and does not expose `pp_legacy_assets_io` | Reduce legacy asset I/O header coupling without rewriting Android asset loading before the asset manager/storage boundary exists | Windows app build; `cmake --build --preset android-arm64 --target pp_assets`; Android package smoke once package builds consume shared targets | Android asset loading is owned by injected asset storage/platform services or `pp_assets` file providers, with no static Android asset manager on `Asset` | | DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`, but WebGL's retained 512 default still lives in `src/platform_legacy/legacy_platform_services.cpp` until the Web shell owns injected services | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback | -| DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`, but live execution still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances directly in `src/app_dialogs.cpp` and inserts them into the legacy layout tree | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters | +| DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata, including message-dialog OK/cancel captions, now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`, but live execution still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances directly in `src/app_dialogs.cpp` and inserts them into the legacy layout tree | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 850e552..e484ac7 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -992,6 +992,10 @@ app-core document-session executors and `src/legacy_document_session_services.*` preserving close prompts, save dialogs, save-version routing, existing-project save execution, and dirty-workflow save-before-continue prompts while retained legacy UI/canvas behavior remains tracked under `DEBT-0040`. +The retained document-session prompt boxes now consume a pure prompt catalog for +close-unsaved, save-before-workflow, new-document overwrite, Save As overwrite, +and save-error metadata; `pano_cli plan-document-session-prompt` exposes the +same titles, messages, button captions, and cancel visibility for automation. `App::dialog_newdoc` now routes accepted new-document plans through the app-core new-document executor and `src/legacy_document_session_services.*`, preserving target overwrite prompts, legacy canvas resize/layer setup, history @@ -1682,6 +1686,20 @@ Results: `pp_app_core_document_session_tests`, `pano_cli_plan_document_file_*`, `pano_cli_plan_document_version_*`, and `pano_cli_simulate_app_session_*` smoke tests after the live bridge split. +- `PanoPainter`, `pano_cli`, `pp_app_core_app_dialog_tests`, and + `pp_app_core_document_session_tests` built after close/save/workflow, + new-document overwrite, Save As overwrite, and save-error prompt metadata + moved into the pure document-session prompt catalog. A clean rebuild was + required once because MSVC reported the known Debug PDB `LNK1103` + corruption, after which the build passed. +- Focused document-session prompt CTest coverage passed for + `pp_app_core_app_dialog_tests`, `pp_app_core_document_session_tests`, + `pano_cli_plan_document_session_prompt_*`, + `pano_cli_plan_document_file_*`, `pano_cli_plan_new_document_*`, and + representative `pano_cli_simulate_app_session_*` smoke tests. +- Android arm64 headless `pp_app_core`, `pano_cli`, + `pp_app_core_app_dialog_tests`, and `pp_app_core_document_session_tests` + built after the same prompt-catalog change. - `PanoPainter`, `pp_app_core_document_export_tests`, and `pano_cli` built after equirectangular, layers, animation-frame, depth, and cube-face export execution moved behind document export services. A clean rebuild was required diff --git a/src/app_core/app_dialog.h b/src/app_core/app_dialog.h index 9b63974..aa3eaf0 100644 --- a/src/app_core/app_dialog.h +++ b/src/app_core/app_dialog.h @@ -24,6 +24,7 @@ struct AppMessageDialogPlan { std::string title; std::string message; std::string ok_caption = "Ok"; + std::string cancel_caption = "Cancel"; bool show_cancel = false; }; @@ -48,12 +49,15 @@ struct AppInputDialogPlan { [[nodiscard]] inline AppMessageDialogPlan plan_app_message_dialog( std::string_view title, std::string_view message, - bool show_cancel) + bool show_cancel, + std::string_view ok_caption = "Ok", + std::string_view cancel_caption = "Cancel") { return { std::string(title), std::string(message), - "Ok", + std::string(ok_caption), + std::string(cancel_caption), show_cancel, }; } diff --git a/src/app_core/document_session.h b/src/app_core/document_session.h index 5858b3b..91365f2 100644 --- a/src/app_core/document_session.h +++ b/src/app_core/document_session.h @@ -1,5 +1,6 @@ #pragma once +#include "app_core/app_dialog.h" #include "app_core/document_route.h" #include "foundation/result.h" @@ -55,6 +56,14 @@ enum class DocumentOpenPlanAction { prompt_import_ppbr, }; +enum class DocumentSessionPromptKind { + close_unsaved_document, + save_before_workflow_continue, + new_document_overwrite, + document_file_overwrite, + document_save_error, +}; + class DocumentOpenServices { public: virtual ~DocumentOpenServices() = default; @@ -135,6 +144,47 @@ public: virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0; }; +[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt( + DocumentSessionPromptKind kind, + std::string_view document_name = {}) +{ + switch (kind) { + case DocumentSessionPromptKind::close_unsaved_document: + return plan_app_message_dialog( + "Unsaved document", + "Do you want to close without saving?", + true, + "Yes", + "No"); + case DocumentSessionPromptKind::save_before_workflow_continue: + return plan_app_message_dialog( + "Unsaved document", + "Would you like to save this document before closing?", + true, + "Yes", + "No"); + case DocumentSessionPromptKind::new_document_overwrite: + return plan_app_message_dialog( + "Warning", + "A document with this name already exists, continue?", + true); + case DocumentSessionPromptKind::document_file_overwrite: + { + std::string message = "Are you sure you want to overwrite "; + message += document_name; + message += "?"; + return plan_app_message_dialog("Warning", message, true); + } + case DocumentSessionPromptKind::document_save_error: + return plan_app_message_dialog( + "Saving Error", + "There was a problem saving the document", + false); + } + + return plan_app_message_dialog("Warning", "", false); +} + [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept { return has_unsaved_changes diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 8ba1799..aff990d 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -134,7 +134,9 @@ std::shared_ptr App::message_box(const std::string &title, const m->m_title->set_text(plan.title.c_str()); m->m_message->set_text(plan.message.c_str()); m->btn_ok->m_text->set_text(plan.ok_caption.c_str()); - if (!plan.show_cancel) + if (plan.show_cancel) + m->btn_cancel->m_text->set_text(plan.cancel_caption.c_str()); + else m->btn_cancel->destroy(); layout[main_id]->add_child(m); return m; diff --git a/src/legacy_document_session_services.cpp b/src/legacy_document_session_services.cpp index 16ad27a..f6f6dcd 100644 --- a/src/legacy_document_session_services.cpp +++ b/src/legacy_document_session_services.cpp @@ -12,6 +12,20 @@ namespace pp::panopainter { namespace { +void apply_legacy_message_box_plan( + NodeMessageBox& msgbox, + const pp::app::AppMessageDialogPlan& plan) +{ + msgbox.m_title->set_text(plan.title.c_str()); + msgbox.m_message->set_text(plan.message.c_str()); + msgbox.btn_ok->m_text->set_text(plan.ok_caption.c_str()); + if (plan.show_cancel) { + msgbox.btn_cancel->m_text->set_text(plan.cancel_caption.c_str()); + } else { + msgbox.btn_cancel->destroy(); + } +} + void create_legacy_new_document( App& app, const pp::app::NewDocumentPlan& plan, @@ -58,8 +72,10 @@ public: auto msgbox = new NodeMessageBox(); msgbox->set_manager(&app_.layout); msgbox->init(); - msgbox->m_title->set_text("Warning"); - msgbox->m_message->set_text("A document with this name already exists, continue?"); + apply_legacy_message_box_plan( + *msgbox, + pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::new_document_overwrite)); auto* app = &app_; auto dialog = dialog_; msgbox->btn_ok->on_click = [app, msgbox, dialog, plan](Node*) { @@ -106,8 +122,11 @@ public: auto msgbox = new NodeMessageBox(); msgbox->set_manager(&app_.layout); msgbox->init(); - msgbox->m_title->set_text("Warning"); - msgbox->m_message->set_text(("Are you sure you want to overwrite " + plan.target.name + "?").c_str()); + apply_legacy_message_box_plan( + *msgbox, + pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::document_file_overwrite, + plan.target.name)); auto* app = &app_; auto dialog = dialog_; msgbox->btn_ok->on_click = [app, msgbox, dialog, plan](Node*) { @@ -159,14 +178,14 @@ public: auto* app = &app_; auto* dialog_already_opened = &dialog_already_opened_; auto* m = app_.layout[app_.main_id]->add_child(); - m->m_title->set_text("Unsaved document"); - m->m_message->set_text("Do you want to close without saving?"); - m->btn_ok->m_text->set_text("Yes"); + apply_legacy_message_box_plan( + *m, + pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::close_unsaved_document)); m->btn_ok->on_click = [app](Node*) { app->request_app_close(); Canvas::I->m_unsaved = false; }; - m->btn_cancel->m_text->set_text("No"); m->btn_cancel->on_click = [dialog_already_opened, m](Node*) { m->destroy(); *dialog_already_opened = false; @@ -221,18 +240,21 @@ public: void prompt_save_before_continue() override { auto m = app_.layout[app_.main_id]->add_child(); - m->m_title->set_text("Unsaved document"); - m->m_message->set_text("Would you like to save this document before closing?"); - m->btn_ok->m_text->set_text("Yes"); - m->btn_cancel->m_text->set_text("No"); + apply_legacy_message_box_plan( + *m, + pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::save_before_workflow_continue)); auto* app = &app_; auto action = action_; m->btn_ok->on_click = [app, m, action](Node*) { Canvas::I->project_save([app, m, action](bool success) { if (success) action(); - else - app->message_box("Saving Error", "There was a problem saving the document"); + else { + const auto plan = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::document_save_error); + app->message_box(plan.title, plan.message, plan.show_cancel); + } }); m->destroy(); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 89e4b60..725c513 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -820,6 +820,25 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-version\".*\"documentName\":\"demo.01\".*\"existingPaths\":1.*\"name\":\"demo.03\".*\"path\":\"D:/Paint/demo.03.ppi\"") + add_test(NAME pano_cli_plan_document_session_prompt_close_smoke + COMMAND pano_cli plan-document-session-prompt --kind close-unsaved) + set_tests_properties(pano_cli_plan_document_session_prompt_close_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-session-prompt\".*\"kind\":\"close-unsaved\".*\"title\":\"Unsaved document\".*\"message\":\"Do you want to close without saving\\?\".*\"okCaption\":\"Yes\".*\"cancelCaption\":\"No\".*\"showCancel\":true") + + add_test(NAME pano_cli_plan_document_session_prompt_overwrite_smoke + COMMAND pano_cli plan-document-session-prompt --kind file-overwrite --name demo) + set_tests_properties(pano_cli_plan_document_session_prompt_overwrite_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-session-prompt\".*\"kind\":\"file-overwrite\".*\"message\":\"Are you sure you want to overwrite demo\\?\".*\"showCancel\":true") + + add_test(NAME pano_cli_plan_document_session_prompt_rejects_unknown + COMMAND pano_cli plan-document-session-prompt --kind nope) + set_tests_properties(pano_cli_plan_document_session_prompt_rejects_unknown PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-session-prompt\".*\"message\":\"unknown document session prompt kind\"") + add_test(NAME pano_cli_plan_export_start_allowed_smoke COMMAND pano_cli plan-export-start --requires-license) set_tests_properties(pano_cli_plan_export_start_allowed_smoke PROPERTIES diff --git a/tests/app_core/app_dialog_tests.cpp b/tests/app_core/app_dialog_tests.cpp index 057a9a0..e6a57aa 100644 --- a/tests/app_core/app_dialog_tests.cpp +++ b/tests/app_core/app_dialog_tests.cpp @@ -26,6 +26,7 @@ void message_dialog_preserves_cancel_policy(pp::tests::Harness& harness) PP_EXPECT(harness, plan.title == "Import"); PP_EXPECT(harness, plan.message == "Import brushes?"); PP_EXPECT(harness, plan.ok_caption == "Ok"); + PP_EXPECT(harness, plan.cancel_caption == "Cancel"); PP_EXPECT(harness, plan.show_cancel); } @@ -37,6 +38,14 @@ void message_dialog_defaults_to_no_cancel(pp::tests::Harness& harness) PP_EXPECT(harness, !plan.show_cancel); } +void message_dialog_allows_custom_button_captions(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_message_dialog("Unsaved", "Save first?", true, "Yes", "No"); + PP_EXPECT(harness, plan.ok_caption == "Yes"); + PP_EXPECT(harness, plan.cancel_caption == "No"); + PP_EXPECT(harness, plan.show_cancel); +} + void input_dialog_preserves_ok_caption(pp::tests::Harness& harness) { const auto plan = pp::app::plan_app_input_dialog("Rename", "Layer", "Save"); @@ -62,6 +71,7 @@ int main() harness.run("progress dialog clamps negative total", progress_dialog_clamps_negative_total); harness.run("message dialog preserves cancel policy", message_dialog_preserves_cancel_policy); harness.run("message dialog defaults to no cancel", message_dialog_defaults_to_no_cancel); + harness.run("message dialog allows custom button captions", message_dialog_allows_custom_button_captions); harness.run("input dialog preserves ok caption", input_dialog_preserves_ok_caption); harness.run("input dialog rejects empty ok caption", input_dialog_rejects_empty_ok_caption); return harness.finish(); diff --git a/tests/app_core/document_session_tests.cpp b/tests/app_core/document_session_tests.cpp index 3b28f28..d96be60 100644 --- a/tests/app_core/document_session_tests.cpp +++ b/tests/app_core/document_session_tests.cpp @@ -358,6 +358,43 @@ void close_request_executor_dispatches_and_preserves_wait(pp::tests::Harness& ha PP_EXPECT(harness, services.call_order == "close;prompt;"); } +void document_session_prompt_catalog_preserves_legacy_close_and_workflow_text(pp::tests::Harness& harness) +{ + const auto close = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::close_unsaved_document); + const auto workflow = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::save_before_workflow_continue); + + PP_EXPECT(harness, close.title == "Unsaved document"); + PP_EXPECT(harness, close.message == "Do you want to close without saving?"); + PP_EXPECT(harness, close.ok_caption == "Yes"); + PP_EXPECT(harness, close.cancel_caption == "No"); + PP_EXPECT(harness, close.show_cancel); + PP_EXPECT(harness, workflow.message == "Would you like to save this document before closing?"); + PP_EXPECT(harness, workflow.ok_caption == "Yes"); + PP_EXPECT(harness, workflow.cancel_caption == "No"); +} + +void document_session_prompt_catalog_preserves_overwrite_and_error_text(pp::tests::Harness& harness) +{ + const auto new_doc = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::new_document_overwrite); + const auto overwrite = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::document_file_overwrite, + "demo"); + const auto save_error = pp::app::plan_document_session_prompt( + pp::app::DocumentSessionPromptKind::document_save_error); + + PP_EXPECT(harness, new_doc.title == "Warning"); + PP_EXPECT(harness, new_doc.message == "A document with this name already exists, continue?"); + PP_EXPECT(harness, new_doc.show_cancel); + PP_EXPECT(harness, overwrite.message == "Are you sure you want to overwrite demo?"); + PP_EXPECT(harness, overwrite.show_cancel); + PP_EXPECT(harness, save_error.title == "Saving Error"); + PP_EXPECT(harness, save_error.message == "There was a problem saving the document"); + PP_EXPECT(harness, !save_error.show_cancel); +} + void save_clean_existing_document_is_no_op(pp::tests::Harness& harness) { PP_EXPECT( @@ -757,6 +794,12 @@ int main() harness.run("close clean document executes immediately", close_clean_document_executes_immediately); harness.run("close dirty document opens one prompt", close_dirty_document_opens_one_prompt); harness.run("close request executor dispatches and preserves wait", close_request_executor_dispatches_and_preserves_wait); + harness.run( + "document session prompt catalog preserves legacy close and workflow text", + document_session_prompt_catalog_preserves_legacy_close_and_workflow_text); + harness.run( + "document session prompt catalog preserves overwrite and error text", + document_session_prompt_catalog_preserves_overwrite_and_error_text); harness.run("save clean existing document is no op", save_clean_existing_document_is_no_op); harness.run("save executor dispatches visible work and no ops cleanly", save_executor_dispatches_visible_work_and_no_ops_cleanly); harness.run("save new or dirty document has user visible work", save_new_or_dirty_document_has_user_visible_work); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index f9f034e..0a5f3f0 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -154,6 +154,11 @@ struct PlanDocumentVersionArgs { std::vector existing_paths; }; +struct PlanDocumentSessionPromptArgs { + pp::app::DocumentSessionPromptKind kind = pp::app::DocumentSessionPromptKind::close_unsaved_document; + std::string name = "demo"; +}; + struct PlanExportTargetArgs { std::string kind; std::string work_directory; @@ -915,6 +920,52 @@ const char* document_workflow_decision_name(pp::app::DocumentWorkflowDecision de return "unavailable"; } +const char* document_session_prompt_kind_name(pp::app::DocumentSessionPromptKind kind) noexcept +{ + switch (kind) { + case pp::app::DocumentSessionPromptKind::close_unsaved_document: + return "close-unsaved"; + case pp::app::DocumentSessionPromptKind::save_before_workflow_continue: + return "save-before-workflow"; + case pp::app::DocumentSessionPromptKind::new_document_overwrite: + return "new-document-overwrite"; + case pp::app::DocumentSessionPromptKind::document_file_overwrite: + return "file-overwrite"; + case pp::app::DocumentSessionPromptKind::document_save_error: + return "save-error"; + } + + return "close-unsaved"; +} + +pp::foundation::Result parse_document_session_prompt_kind( + std::string_view kind) +{ + if (kind == "close-unsaved") { + return pp::foundation::Result::success( + pp::app::DocumentSessionPromptKind::close_unsaved_document); + } + if (kind == "save-before-workflow") { + return pp::foundation::Result::success( + pp::app::DocumentSessionPromptKind::save_before_workflow_continue); + } + if (kind == "new-document-overwrite") { + return pp::foundation::Result::success( + pp::app::DocumentSessionPromptKind::new_document_overwrite); + } + if (kind == "file-overwrite") { + return pp::foundation::Result::success( + pp::app::DocumentSessionPromptKind::document_file_overwrite); + } + if (kind == "save-error") { + return pp::foundation::Result::success( + pp::app::DocumentSessionPromptKind::document_save_error); + } + + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("unknown document session prompt kind")); +} + const char* file_menu_command_name(pp::app::FileMenuCommand command) noexcept { switch (command) { @@ -2179,6 +2230,7 @@ void print_help() << " plan-new-document --work-dir DIR --name NAME [--resolution-index N] [--target-exists]\n" << " plan-document-file --work-dir DIR --name NAME [--target-exists]\n" << " plan-document-version --directory DIR --doc-name NAME [--existing-path FILE]\n" + << " plan-document-session-prompt --kind close-unsaved|save-before-workflow|new-document-overwrite|file-overwrite|save-error [--name NAME]\n" << " plan-export-start [--requires-license] [--demo] [--no-canvas]\n" << " plan-export-menu --kind jpeg|png|layers|cube-faces|depth|animation-frames|animation-mp4|timelapse [--demo] [--no-canvas]\n" << " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n" @@ -3388,6 +3440,57 @@ int plan_document_version(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_document_session_prompt_args( + int argc, + char** argv, + PlanDocumentSessionPromptArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto kind = parse_document_session_prompt_kind(argv[++i]); + if (!kind) { + return kind.status(); + } + args.kind = kind.value(); + } else if (key == "--name") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.name = argv[++i]; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_document_session_prompt(int argc, char** argv) +{ + PlanDocumentSessionPromptArgs args; + const auto status = parse_plan_document_session_prompt_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-document-session-prompt", status.message); + return 2; + } + + const auto plan = pp::app::plan_document_session_prompt(args.kind, args.name); + std::cout << "{\"ok\":true,\"command\":\"plan-document-session-prompt\"" + << ",\"kind\":\"" << document_session_prompt_kind_name(args.kind) + << "\",\"name\":\"" << json_escape(args.name) + << "\",\"plan\":{\"title\":\"" << json_escape(plan.title) + << "\",\"message\":\"" << json_escape(plan.message) + << "\",\"okCaption\":\"" << json_escape(plan.ok_caption) + << "\",\"cancelCaption\":\"" << json_escape(plan.cancel_caption) + << "\",\"showCancel\":" << json_bool(plan.show_cancel) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_export_start_args( int argc, char** argv, @@ -3806,6 +3909,7 @@ int plan_app_dialog(int argc, char** argv) << "\",\"plan\":{\"title\":\"" << json_escape(plan.title) << "\",\"message\":\"" << json_escape(plan.message) << "\",\"okCaption\":\"" << json_escape(plan.ok_caption) + << "\",\"cancelCaption\":\"" << json_escape(plan.cancel_caption) << "\",\"showCancel\":" << json_bool(plan.show_cancel) << "}}\n"; return 0; @@ -11035,6 +11139,10 @@ int main(int argc, char** argv) return plan_document_version(argc, argv); } + if (command == "plan-document-session-prompt") { + return plan_document_session_prompt(argc, argv); + } + if (command == "plan-export-start") { return plan_export_start(argc, argv); }