diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index d8c6069..404b28f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets. | Platform/Target | Current Entrypoint | Notes | | --- | --- | --- | -| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing and unsaved-document session decisions owned by `pp_app_core`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | +| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` | | Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging | | macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today | | iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today | @@ -394,15 +394,16 @@ Known local toolchain state: - `pano_cli classify-open` exposes the `pp_app_core` document-open route contract as JSON and is covered for project files, ABR imports, PPBR imports, and malformed path rejection. -- `pano_cli simulate-app-session` exposes `pp_app_core` unsaved-document - decisions for project-open and app-close flows as JSON and is covered for - clean, dirty, and already-prompting states. +- `pano_cli simulate-app-session` exposes `pp_app_core` project-open, + app-close, save, save-as, and save-version decisions as JSON and is covered + for clean, dirty, already-prompting, new-document, save-as, save-version, and + dirty-save-version states. - `pp_app_core_document_route_tests` covers the app document-open route contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, and malformed paths before the live `App::open_document` performs UI or legacy canvas work. -- `pp_app_core_document_session_tests` covers clean and dirty app session - decisions without requiring a window, canvas, or message box. +- `pp_app_core_document_session_tests` covers clean and dirty app session plus + save-request decisions without requiring a window, canvas, or message box. - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` through the vcpkg preset; default and Android validation still use the retained vendored fallback tracked by DEBT-0012. diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 1cace97..68aae9a 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -13,10 +13,10 @@ and validation command. | --- | --- | --- | --- | | PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture | | Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, CLI route smoke, app open smoke | -| Unsaved document decisions | `App::open_document`, `App::request_close`, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open decision tests, CLI session smoke, app close/open smoke | +| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version decision tests, CLI session smoke, app close/open/save smoke | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input | -| Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke | +| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke | ## Image And Export diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 97bd271..21ebb5f 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `pano_cli classify-open`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session contracts, but document loading still reaches legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli simulate-app-session --unsaved`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, `pano_cli classify-open`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session contracts, but document loading and saving still reach legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index d9cb1c8..69f0823 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -168,12 +168,13 @@ targets. `pp_app_core` now owns tested app-level document-open routing for project files, ABR imports, and PPBR imports without UI, filesystem, platform, or renderer dependencies; `App::open_document` and `pano_cli classify-open` -consume this route contract. It also owns tested unsaved-document decisions for -project-open and app-close flows; `App::open_document`, `App::request_close`, -and `pano_cli simulate-app-session` consume those contracts while legacy -canvas/project loading remains in place. `panopainter_app` is now a real -static target that owns app orchestration sources, app version metadata, and -version-header generation. +consume this route contract. It also owns tested session decisions for +project-open, app-close, save, save-as, and save-version flows; +`App::open_document`, `App::request_close`, file-menu save actions, +`NodeCanvas` save hotkeys, and `pano_cli simulate-app-session` consume those +contracts while legacy canvas/project loading remains in place. +`panopainter_app` is now a real static target that owns app orchestration +sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, viewport, color-picker, stroke-preview, and tool UI workflow nodes outside `pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy @@ -287,10 +288,10 @@ CTest presets run headlessly, and PowerShell/bash wrappers exist for configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists with JSON automation commands for app document-open routing, app session -dirty-state decisions, creating a `pp_document` model, metadata-only PPI -project loading, and inspecting image signatures, PPI headers, and layout XML; -full document/app integration is debt-tracked as DEBT-0010 and full PPI body -parsing is debt-tracked as DEBT-0013. +dirty-state and save decisions, creating a `pp_document` model, metadata-only +PPI project loading, and inspecting image signatures, PPI headers, and layout +XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI +body parsing is debt-tracked as DEBT-0013. Implementation tasks: @@ -423,7 +424,8 @@ rejection smoke test for unsafe tiny canvas dimensions. `pano_cli classify-open` exposes the pure `pp_app_core` document-open route contract for project files, ABR imports, PPBR imports, and malformed path rejection. `pano_cli simulate-app-session` exposes the pure `pp_app_core` -unsaved-document decisions used by project-open and app-close flows. +session decisions used by project-open, app-close, save, save-as, and +save-version flows. `pano_cli parse-layout` exercises the XML layout path. Continue expanding document behavior toward legacy Canvas parity and then port OpenGL classes behind the renderer boundary. diff --git a/src/app.h b/src/app.h index 11624aa..30c862d 100644 --- a/src/app.h +++ b/src/app.h @@ -24,6 +24,7 @@ #include "node_input_box.h" #include "node_panel_animation.h" #include "layout.h" +#include "app_core/document_session.h" #if defined(__OBJC__) && defined(__IOS__) #import @@ -248,6 +249,7 @@ public: void dialog_usermanual(); void dialog_changelog(); void dialog_about(); + void save_document(pp::app::DocumentSaveIntent intent); void dialog_newdoc(); void dialog_save(); void dialog_save_ver(); diff --git a/src/app_core/document_session.h b/src/app_core/document_session.h index 1c1ae38..8e54ffa 100644 --- a/src/app_core/document_session.h +++ b/src/app_core/document_session.h @@ -13,6 +13,20 @@ enum class CloseRequestDecision { wait_for_existing_prompt, }; +enum class DocumentSaveIntent { + save, + save_as, + save_version, + save_dirty_version, +}; + +enum class DocumentSaveDecision { + no_op, + show_save_dialog, + save_existing, + save_version, +}; + [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept { return has_unsaved_changes @@ -33,4 +47,35 @@ enum class CloseRequestDecision { : CloseRequestDecision::show_unsaved_prompt; } +[[nodiscard]] constexpr DocumentSaveDecision plan_document_save( + bool is_new_document, + bool has_unsaved_changes, + DocumentSaveIntent intent) noexcept +{ + switch (intent) { + case DocumentSaveIntent::save: + if (is_new_document) { + return DocumentSaveDecision::show_save_dialog; + } + return has_unsaved_changes + ? DocumentSaveDecision::save_existing + : DocumentSaveDecision::no_op; + case DocumentSaveIntent::save_as: + return DocumentSaveDecision::show_save_dialog; + case DocumentSaveIntent::save_version: + return is_new_document + ? DocumentSaveDecision::show_save_dialog + : DocumentSaveDecision::save_version; + case DocumentSaveIntent::save_dirty_version: + if (is_new_document) { + return DocumentSaveDecision::show_save_dialog; + } + return has_unsaved_changes + ? DocumentSaveDecision::save_version + : DocumentSaveDecision::no_op; + } + + return DocumentSaveDecision::no_op; +} + } diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 39c980b..61f9616 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -366,6 +366,27 @@ void App::dialog_save_ver() canvas->m_canvas->project_save(doc_path); } +void App::save_document(pp::app::DocumentSaveIntent intent) +{ + const auto decision = pp::app::plan_document_save( + Canvas::I->m_newdoc, + Canvas::I->m_unsaved, + intent); + switch (decision) { + case pp::app::DocumentSaveDecision::show_save_dialog: + dialog_save(); + break; + case pp::app::DocumentSaveDecision::save_existing: + Canvas::I->project_save(); + break; + case pp::app::DocumentSaveDecision::save_version: + dialog_save_ver(); + break; + case pp::app::DocumentSaveDecision::no_op: + break; + } +} + void App::dialog_save() { if (!check_license()) diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 5470ed2..bd83659 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -586,26 +586,19 @@ void App::init_menu_file() }; if (auto b = popup->find("file-save")) b->on_click = [this, popup](Node*) { - if (Canvas::I->m_newdoc) - { - dialog_save(); - } - else if(Canvas::I->m_unsaved) - { - canvas->m_canvas->project_save(); - } + save_document(pp::app::DocumentSaveIntent::save); popup->mouse_release(); popup->destroy(); }; if (auto b = popup->find("file-save-as")) b->on_click = [this, popup](Node*) { - dialog_save(); + save_document(pp::app::DocumentSaveIntent::save_as); popup->mouse_release(); popup->destroy(); }; if (auto b = popup->find("file-save-ver")) b->on_click = [this, popup](Node*) { - Canvas::I->m_newdoc ? dialog_save() : dialog_save_ver(); + save_document(pp::app::DocumentSaveIntent::save_version); popup->mouse_release(); popup->destroy(); }; diff --git a/src/node_canvas.cpp b/src/node_canvas.cpp index 6061c83..4fe2446 100644 --- a/src/node_canvas.cpp +++ b/src/node_canvas.cpp @@ -617,25 +617,11 @@ kEventResult NodeCanvas::handle_event(Event* e) App::I->keys[(int)kKey::KeyShift] ? ActionManager::redo() : ActionManager::undo(); if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift]) { - if (Canvas::I->m_newdoc) - { - App::I->dialog_save(); - } - else if (Canvas::I->m_unsaved) - { - Canvas::I->project_save(); - } + App::I->save_document(pp::app::DocumentSaveIntent::save); } if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && App::I->keys[(int)kKey::KeyShift]) { - if (Canvas::I->m_newdoc) - { - App::I->dialog_save(); - } - else if (Canvas::I->m_unsaved) - { - App::I->dialog_save_ver(); - } + App::I->save_document(pp::app::DocumentSaveIntent::save_dirty_version); } if (ke->m_key == kKey::KeyBracketLeft) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e87f22a..740f630 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -375,19 +375,43 @@ if(TARGET pano_cli) COMMAND pano_cli simulate-app-session) set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES LABELS "app;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"unsaved\":false.*\"closePromptOpen\":false.*\"projectOpen\":\"open-now\".*\"closeRequest\":\"close-now\"") + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"newDocument\":false.*\"unsaved\":false.*\"closePromptOpen\":false.*\"projectOpen\":\"open-now\".*\"closeRequest\":\"close-now\".*\"saveIntent\":\"save\".*\"saveRequest\":\"no-op\"") add_test(NAME pano_cli_simulate_app_session_unsaved_smoke COMMAND pano_cli simulate-app-session --unsaved) set_tests_properties(pano_cli_simulate_app_session_unsaved_smoke PROPERTIES LABELS "app;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"unsaved\":true.*\"closePromptOpen\":false.*\"projectOpen\":\"prompt-discard-unsaved\".*\"closeRequest\":\"show-unsaved-prompt\"") + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"newDocument\":false.*\"unsaved\":true.*\"closePromptOpen\":false.*\"projectOpen\":\"prompt-discard-unsaved\".*\"closeRequest\":\"show-unsaved-prompt\".*\"saveRequest\":\"save-existing\"") add_test(NAME pano_cli_simulate_app_session_existing_prompt_smoke COMMAND pano_cli simulate-app-session --unsaved --close-prompt-open) set_tests_properties(pano_cli_simulate_app_session_existing_prompt_smoke PROPERTIES LABELS "app;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"unsaved\":true.*\"closePromptOpen\":true.*\"projectOpen\":\"prompt-discard-unsaved\".*\"closeRequest\":\"wait-for-existing-prompt\"") + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"newDocument\":false.*\"unsaved\":true.*\"closePromptOpen\":true.*\"projectOpen\":\"prompt-discard-unsaved\".*\"closeRequest\":\"wait-for-existing-prompt\"") + + add_test(NAME pano_cli_simulate_app_session_new_document_save_smoke + COMMAND pano_cli simulate-app-session --new-document) + set_tests_properties(pano_cli_simulate_app_session_new_document_save_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"newDocument\":true.*\"saveIntent\":\"save\".*\"saveRequest\":\"show-save-dialog\"") + + add_test(NAME pano_cli_simulate_app_session_save_as_smoke + COMMAND pano_cli simulate-app-session --save-intent save-as) + set_tests_properties(pano_cli_simulate_app_session_save_as_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"saveIntent\":\"save-as\".*\"saveRequest\":\"show-save-dialog\"") + + add_test(NAME pano_cli_simulate_app_session_save_version_smoke + COMMAND pano_cli simulate-app-session --save-intent save-version) + set_tests_properties(pano_cli_simulate_app_session_save_version_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"saveIntent\":\"save-version\".*\"saveRequest\":\"save-version\"") + + add_test(NAME pano_cli_simulate_app_session_save_dirty_version_clean_smoke + COMMAND pano_cli simulate-app-session --save-intent save-dirty-version) + set_tests_properties(pano_cli_simulate_app_session_save_dirty_version_clean_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"simulate-app-session\".*\"saveIntent\":\"save-dirty-version\".*\"saveRequest\":\"no-op\"") add_test(NAME pano_cli_save_project_roundtrip_smoke COMMAND "${CMAKE_COMMAND}" diff --git a/tests/app_core/document_session_tests.cpp b/tests/app_core/document_session_tests.cpp index d2f6b9a..74b2e34 100644 --- a/tests/app_core/document_session_tests.cpp +++ b/tests/app_core/document_session_tests.cpp @@ -35,6 +35,58 @@ void close_dirty_document_opens_one_prompt(pp::tests::Harness& harness) pp::app::plan_close_request(true, true) == pp::app::CloseRequestDecision::wait_for_existing_prompt); } +void save_clean_existing_document_is_no_op(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save) + == pp::app::DocumentSaveDecision::no_op); +} + +void save_new_or_dirty_document_has_user_visible_work(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_document_save(true, false, pp::app::DocumentSaveIntent::save) + == pp::app::DocumentSaveDecision::show_save_dialog); + PP_EXPECT( + harness, + pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save) + == pp::app::DocumentSaveDecision::save_existing); +} + +void save_as_always_shows_save_dialog(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_as) + == pp::app::DocumentSaveDecision::show_save_dialog); + PP_EXPECT( + harness, + pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save_as) + == pp::app::DocumentSaveDecision::show_save_dialog); +} + +void save_version_respects_menu_and_hotkey_behaviors(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_version) + == pp::app::DocumentSaveDecision::save_version); + PP_EXPECT( + harness, + pp::app::plan_document_save(false, false, pp::app::DocumentSaveIntent::save_dirty_version) + == pp::app::DocumentSaveDecision::no_op); + PP_EXPECT( + harness, + pp::app::plan_document_save(false, true, pp::app::DocumentSaveIntent::save_dirty_version) + == pp::app::DocumentSaveDecision::save_version); + PP_EXPECT( + harness, + pp::app::plan_document_save(true, false, pp::app::DocumentSaveIntent::save_version) + == pp::app::DocumentSaveDecision::show_save_dialog); +} + } int main() @@ -44,5 +96,9 @@ int main() harness.run("project open dirty document prompts for discard", project_open_dirty_document_prompts_for_discard); 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("save clean existing document is no op", save_clean_existing_document_is_no_op); + harness.run("save new or dirty document has user visible work", save_new_or_dirty_document_has_user_visible_work); + harness.run("save as always shows save dialog", save_as_always_shows_save_dialog); + harness.run("save version respects menu and hotkey behaviors", save_version_respects_menu_and_hotkey_behaviors); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 78bea2d..bbe4871 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -97,8 +97,10 @@ struct ClassifyOpenArgs { }; struct SimulateAppSessionArgs { + bool new_document = false; bool unsaved = false; bool close_prompt_open = false; + pp::app::DocumentSaveIntent save_intent = pp::app::DocumentSaveIntent::save; }; struct SimulateStrokeArgs { @@ -264,6 +266,38 @@ const char* close_request_decision_name(pp::app::CloseRequestDecision decision) return "close-now"; } +const char* document_save_intent_name(pp::app::DocumentSaveIntent intent) noexcept +{ + switch (intent) { + case pp::app::DocumentSaveIntent::save: + return "save"; + case pp::app::DocumentSaveIntent::save_as: + return "save-as"; + case pp::app::DocumentSaveIntent::save_version: + return "save-version"; + case pp::app::DocumentSaveIntent::save_dirty_version: + return "save-dirty-version"; + } + + return "save"; +} + +const char* document_save_decision_name(pp::app::DocumentSaveDecision decision) noexcept +{ + switch (decision) { + case pp::app::DocumentSaveDecision::no_op: + return "no-op"; + case pp::app::DocumentSaveDecision::show_save_dialog: + return "show-save-dialog"; + case pp::app::DocumentSaveDecision::save_existing: + return "save-existing"; + case pp::app::DocumentSaveDecision::save_version: + return "save-version"; + } + + return "no-op"; +} + pp::foundation::Result parse_float_arg(std::string_view text) { float value = 0.0F; @@ -302,7 +336,7 @@ void print_help() << " simulate-document-edits [--width N] [--height N]\n" << " simulate-document-export [--width N] [--height N]\n" << " simulate-document-history [--width N] [--height N] [--history N]\n" - << " simulate-app-session [--unsaved] [--close-prompt-open]\n" + << " simulate-app-session [--new-document] [--unsaved] [--close-prompt-open] [--save-intent save|save-as|save-version|save-dirty-version]\n" << " simulate-blend\n" << " simulate-image-import [--width N] [--height N]\n" << " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n" @@ -1142,10 +1176,28 @@ pp::foundation::Status parse_simulate_app_session_args( { for (int i = 2; i < argc; ++i) { const std::string_view key(argv[i]); - if (key == "--unsaved") { + if (key == "--new-document") { + args.new_document = true; + } else if (key == "--unsaved") { args.unsaved = true; } else if (key == "--close-prompt-open") { args.close_prompt_open = true; + } else if (key == "--save-intent") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const std::string_view value(argv[++i]); + if (value == "save") { + args.save_intent = pp::app::DocumentSaveIntent::save; + } else if (value == "save-as") { + args.save_intent = pp::app::DocumentSaveIntent::save_as; + } else if (value == "save-version") { + args.save_intent = pp::app::DocumentSaveIntent::save_version; + } else if (value == "save-dirty-version") { + args.save_intent = pp::app::DocumentSaveIntent::save_dirty_version; + } else { + return pp::foundation::Status::invalid_argument("unknown save intent"); + } } else { return pp::foundation::Status::invalid_argument("unknown option"); } @@ -1165,13 +1217,22 @@ int simulate_app_session(int argc, char** argv) const auto open_decision = pp::app::plan_project_open(args.unsaved); const auto close_decision = pp::app::plan_close_request(args.unsaved, args.close_prompt_open); + const auto save_decision = pp::app::plan_document_save( + args.new_document, + args.unsaved, + args.save_intent); std::cout << "{\"ok\":true,\"command\":\"simulate-app-session\"" - << ",\"state\":{\"unsaved\":" << json_bool(args.unsaved) + << ",\"state\":{\"newDocument\":" << json_bool(args.new_document) + << ",\"unsaved\":" << json_bool(args.unsaved) << ",\"closePromptOpen\":" << json_bool(args.close_prompt_open) << "},\"decisions\":{\"projectOpen\":\"" << project_open_decision_name(open_decision) << "\",\"closeRequest\":\"" << close_request_decision_name(close_decision) + << "\",\"saveIntent\":\"" + << document_save_intent_name(args.save_intent) + << "\",\"saveRequest\":\"" + << document_save_decision_name(save_decision) << "\"}}\n"; return 0; }