Route app save decisions through app core

This commit is contained in:
2026-06-02 22:26:58 +02:00
parent 76808d60e3
commit d28aa25358
12 changed files with 243 additions and 52 deletions

View File

@@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets.
| Platform/Target | Current Entrypoint | Notes | | 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 | | Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today | | macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES 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 - `pano_cli classify-open` exposes the `pp_app_core` document-open route
contract as JSON and is covered for project files, ABR imports, PPBR contract as JSON and is covered for project files, ABR imports, PPBR
imports, and malformed path rejection. imports, and malformed path rejection.
- `pano_cli simulate-app-session` exposes `pp_app_core` unsaved-document - `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
decisions for project-open and app-close flows as JSON and is covered for app-close, save, save-as, and save-version decisions as JSON and is covered
clean, dirty, and already-prompting states. 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 - `pp_app_core_document_route_tests` covers the app document-open route
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or and malformed paths before the live `App::open_document` performs UI or
legacy canvas work. legacy canvas work.
- `pp_app_core_document_session_tests` covers clean and dirty app session - `pp_app_core_document_session_tests` covers clean and dirty app session plus
decisions without requiring a window, canvas, or message box. save-request decisions without requiring a window, canvas, or message box.
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
through the vcpkg preset; default and Android validation still use the through the vcpkg preset; default and Android validation still use the
retained vendored fallback tracked by DEBT-0012. retained vendored fallback tracked by DEBT-0012.

View File

@@ -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 | | 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 | | 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 | | 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 | | 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 ## Image And Export

View File

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

View File

@@ -168,12 +168,13 @@ targets.
`pp_app_core` now owns tested app-level document-open routing for project `pp_app_core` now owns tested app-level document-open routing for project
files, ABR imports, and PPBR imports without UI, filesystem, platform, or files, ABR imports, and PPBR imports without UI, filesystem, platform, or
renderer dependencies; `App::open_document` and `pano_cli classify-open` renderer dependencies; `App::open_document` and `pano_cli classify-open`
consume this route contract. It also owns tested unsaved-document decisions for consume this route contract. It also owns tested session decisions for
project-open and app-close flows; `App::open_document`, `App::request_close`, project-open, app-close, save, save-as, and save-version flows;
and `pano_cli simulate-app-session` consume those contracts while legacy `App::open_document`, `App::request_close`, file-menu save actions,
canvas/project loading remains in place. `panopainter_app` is now a real `NodeCanvas` save hotkeys, and `pano_cli simulate-app-session` consume those
static target that owns app orchestration sources, app version metadata, and contracts while legacy canvas/project loading remains in place.
version-header generation. `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, `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas,
viewport, color-picker, stroke-preview, and tool UI workflow nodes outside viewport, color-picker, stroke-preview, and tool UI workflow nodes outside
`pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy `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 PowerShell/bash wrappers exist for
configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists
with JSON automation commands for app document-open routing, app session with JSON automation commands for app document-open routing, app session
dirty-state decisions, creating a `pp_document` model, metadata-only PPI dirty-state and save decisions, creating a `pp_document` model, metadata-only
project loading, and inspecting image signatures, PPI headers, and layout XML; PPI project loading, and inspecting image signatures, PPI headers, and layout
full document/app integration is debt-tracked as DEBT-0010 and full PPI body XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI
parsing is debt-tracked as DEBT-0013. body parsing is debt-tracked as DEBT-0013.
Implementation tasks: 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 `pano_cli classify-open` exposes the pure `pp_app_core` document-open route
contract for project files, ABR imports, PPBR imports, and malformed path contract for project files, ABR imports, PPBR imports, and malformed path
rejection. `pano_cli simulate-app-session` exposes the pure `pp_app_core` 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 `pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary. behind the renderer boundary.

View File

@@ -24,6 +24,7 @@
#include "node_input_box.h" #include "node_input_box.h"
#include "node_panel_animation.h" #include "node_panel_animation.h"
#include "layout.h" #include "layout.h"
#include "app_core/document_session.h"
#if defined(__OBJC__) && defined(__IOS__) #if defined(__OBJC__) && defined(__IOS__)
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@@ -248,6 +249,7 @@ public:
void dialog_usermanual(); void dialog_usermanual();
void dialog_changelog(); void dialog_changelog();
void dialog_about(); void dialog_about();
void save_document(pp::app::DocumentSaveIntent intent);
void dialog_newdoc(); void dialog_newdoc();
void dialog_save(); void dialog_save();
void dialog_save_ver(); void dialog_save_ver();

View File

@@ -13,6 +13,20 @@ enum class CloseRequestDecision {
wait_for_existing_prompt, 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 [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
{ {
return has_unsaved_changes return has_unsaved_changes
@@ -33,4 +47,35 @@ enum class CloseRequestDecision {
: CloseRequestDecision::show_unsaved_prompt; : 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;
}
} }

View File

@@ -366,6 +366,27 @@ void App::dialog_save_ver()
canvas->m_canvas->project_save(doc_path); 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() void App::dialog_save()
{ {
if (!check_license()) if (!check_license())

View File

@@ -586,26 +586,19 @@ void App::init_menu_file()
}; };
if (auto b = popup->find<NodeButtonCustom>("file-save")) if (auto b = popup->find<NodeButtonCustom>("file-save"))
b->on_click = [this, popup](Node*) { b->on_click = [this, popup](Node*) {
if (Canvas::I->m_newdoc) save_document(pp::app::DocumentSaveIntent::save);
{
dialog_save();
}
else if(Canvas::I->m_unsaved)
{
canvas->m_canvas->project_save();
}
popup->mouse_release(); popup->mouse_release();
popup->destroy(); popup->destroy();
}; };
if (auto b = popup->find<NodeButtonCustom>("file-save-as")) if (auto b = popup->find<NodeButtonCustom>("file-save-as"))
b->on_click = [this, popup](Node*) { b->on_click = [this, popup](Node*) {
dialog_save(); save_document(pp::app::DocumentSaveIntent::save_as);
popup->mouse_release(); popup->mouse_release();
popup->destroy(); popup->destroy();
}; };
if (auto b = popup->find<NodeButtonCustom>("file-save-ver")) if (auto b = popup->find<NodeButtonCustom>("file-save-ver"))
b->on_click = [this, popup](Node*) { 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->mouse_release();
popup->destroy(); popup->destroy();
}; };

View File

@@ -617,25 +617,11 @@ kEventResult NodeCanvas::handle_event(Event* e)
App::I->keys[(int)kKey::KeyShift] ? ActionManager::redo() : ActionManager::undo(); 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 (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift])
{ {
if (Canvas::I->m_newdoc) App::I->save_document(pp::app::DocumentSaveIntent::save);
{
App::I->dialog_save();
}
else if (Canvas::I->m_unsaved)
{
Canvas::I->project_save();
}
} }
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && App::I->keys[(int)kKey::KeyShift]) 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->save_document(pp::app::DocumentSaveIntent::save_dirty_version);
{
App::I->dialog_save();
}
else if (Canvas::I->m_unsaved)
{
App::I->dialog_save_ver();
}
} }
if (ke->m_key == kKey::KeyBracketLeft) if (ke->m_key == kKey::KeyBracketLeft)
{ {

View File

@@ -375,19 +375,43 @@ if(TARGET pano_cli)
COMMAND pano_cli simulate-app-session) COMMAND pano_cli simulate-app-session)
set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES
LABELS "app;integration;desktop-fast" 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 add_test(NAME pano_cli_simulate_app_session_unsaved_smoke
COMMAND pano_cli simulate-app-session --unsaved) COMMAND pano_cli simulate-app-session --unsaved)
set_tests_properties(pano_cli_simulate_app_session_unsaved_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_unsaved_smoke PROPERTIES
LABELS "app;integration;desktop-fast" 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 add_test(NAME pano_cli_simulate_app_session_existing_prompt_smoke
COMMAND pano_cli simulate-app-session --unsaved --close-prompt-open) COMMAND pano_cli simulate-app-session --unsaved --close-prompt-open)
set_tests_properties(pano_cli_simulate_app_session_existing_prompt_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_existing_prompt_smoke PROPERTIES
LABELS "app;integration;desktop-fast" 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 add_test(NAME pano_cli_save_project_roundtrip_smoke
COMMAND "${CMAKE_COMMAND}" COMMAND "${CMAKE_COMMAND}"

View File

@@ -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); 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() 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("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 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 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(); return harness.finish();
} }

View File

@@ -97,8 +97,10 @@ struct ClassifyOpenArgs {
}; };
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool new_document = false;
bool unsaved = false; bool unsaved = false;
bool close_prompt_open = false; bool close_prompt_open = false;
pp::app::DocumentSaveIntent save_intent = pp::app::DocumentSaveIntent::save;
}; };
struct SimulateStrokeArgs { struct SimulateStrokeArgs {
@@ -264,6 +266,38 @@ const char* close_request_decision_name(pp::app::CloseRequestDecision decision)
return "close-now"; 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<float> parse_float_arg(std::string_view text) pp::foundation::Result<float> parse_float_arg(std::string_view text)
{ {
float value = 0.0F; float value = 0.0F;
@@ -302,7 +336,7 @@ void print_help()
<< " simulate-document-edits [--width N] [--height N]\n" << " simulate-document-edits [--width N] [--height N]\n"
<< " simulate-document-export [--width N] [--height N]\n" << " simulate-document-export [--width N] [--height N]\n"
<< " simulate-document-history [--width N] [--height N] [--history 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-blend\n"
<< " simulate-image-import [--width N] [--height N]\n" << " simulate-image-import [--width N] [--height N]\n"
<< " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing 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) { for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[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; args.unsaved = true;
} else if (key == "--close-prompt-open") { } else if (key == "--close-prompt-open") {
args.close_prompt_open = true; 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 { } else {
return pp::foundation::Status::invalid_argument("unknown option"); 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 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 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\"" 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) << ",\"closePromptOpen\":" << json_bool(args.close_prompt_open)
<< "},\"decisions\":{\"projectOpen\":\"" << "},\"decisions\":{\"projectOpen\":\""
<< project_open_decision_name(open_decision) << project_open_decision_name(open_decision)
<< "\",\"closeRequest\":\"" << "\",\"closeRequest\":\""
<< close_request_decision_name(close_decision) << close_request_decision_name(close_decision)
<< "\",\"saveIntent\":\""
<< document_save_intent_name(args.save_intent)
<< "\",\"saveRequest\":\""
<< document_save_decision_name(save_decision)
<< "\"}}\n"; << "\"}}\n";
return 0; return 0;
} }