Plan document session prompts

This commit is contained in:
2026-06-05 08:07:54 +02:00
parent 5def47cdcc
commit e5526c6d0a
11 changed files with 316 additions and 26 deletions

View File

@@ -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`,

View File

@@ -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

View File

@@ -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

View File

@@ -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,
};
}

View File

@@ -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

View File

@@ -134,7 +134,9 @@ std::shared_ptr<NodeMessageBox> 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;

View File

@@ -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<NodeMessageBox>();
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<NodeMessageBox>();
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();
};

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -154,6 +154,11 @@ struct PlanDocumentVersionArgs {
std::vector<std::string> 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<pp::app::DocumentSessionPromptKind> parse_document_session_prompt_kind(
std::string_view kind)
{
if (kind == "close-unsaved") {
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::success(
pp::app::DocumentSessionPromptKind::close_unsaved_document);
}
if (kind == "save-before-workflow") {
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::success(
pp::app::DocumentSessionPromptKind::save_before_workflow_continue);
}
if (kind == "new-document-overwrite") {
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::success(
pp::app::DocumentSessionPromptKind::new_document_overwrite);
}
if (kind == "file-overwrite") {
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::success(
pp::app::DocumentSessionPromptKind::document_file_overwrite);
}
if (kind == "save-error") {
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::success(
pp::app::DocumentSessionPromptKind::document_save_error);
}
return pp::foundation::Result<pp::app::DocumentSessionPromptKind>::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);
}