From 4528edfb2ce8fa949d25f0d5def03f300c8e095a Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 13:30:22 +0200 Subject: [PATCH] Centralize legacy document session bridge --- cmake/PanoPainterSources.cmake | 2 + docs/modernization/build-inventory.md | 13 +- docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 15 +++ src/app.cpp | 29 ++--- src/app_core/document_session.h | 82 +++++++++++++ src/app_dialogs.cpp | 53 ++------ src/legacy_document_session_services.cpp | 143 ++++++++++++++++++++++ src/legacy_document_session_services.h | 26 ++++ tests/app_core/document_session_tests.cpp | 137 +++++++++++++++++++++ 10 files changed, 437 insertions(+), 64 deletions(-) create mode 100644 src/legacy_document_session_services.cpp create mode 100644 src/legacy_document_session_services.h diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index e7ed526..8aa5eb1 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -86,6 +86,8 @@ set(PP_PANOPAINTER_APP_SOURCES src/legacy_cloud_services.h src/legacy_document_open_services.cpp src/legacy_document_open_services.h + src/legacy_document_session_services.cpp + src/legacy_document_session_services.h src/platform_legacy/legacy_platform_services.cpp src/platform_legacy/legacy_platform_services.h src/version.cpp diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index dc9ee20..b282705 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -584,14 +584,21 @@ Known local toolchain state: progress-total decisions. - `pp_app_core_document_session_tests` covers clean and dirty app session, document-open action planning and executor dispatch/rejection, save-request, - save-before-workflow, new-document target/resolution/overwrite planning, - document file target, combined save-file overwrite planning, and save-version - target decisions without requiring a window, canvas, or message box. + close-request executor dispatch/no-op preservation, document-save executor + dispatch/no-op preservation, save-before-workflow executor dispatch, + new-document target/resolution/overwrite planning, document file target, + combined save-file overwrite planning, and save-version target decisions + without requiring a window, canvas, or message box. - `src/legacy_document_open_services.*` is the current app-shell bridge between `pp_app_core` document-open plans and live ABR/PPBR import prompts, unsaved-project discard prompts, project opening, layer UI refresh, title updates, and action-history clearing; remaining legacy execution ownership is tracked by `DEBT-0039`. +- `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; retained legacy UI/canvas execution remains + tracked by `DEBT-0040`. - `src/legacy_history_services.*` is the current app-shell bridge between `pp_app_core` history plans and legacy `ActionManager`; toolbar and `NodeCanvas` hotkeys share it while document-history extraction remains diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 085b3e0..c65f8a2 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -57,6 +57,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters | | DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, but the bridge still uses legacy save-before-upload, `upload`/`download` network helpers, 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`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, 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, launches legacy brush preset import threads, 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/execution, project-open execution, unsaved-project discard prompting, layer refresh, title updates, and history clearing are owned by injected app/document/asset/UI services with `App::open_document` acting only as an adapter | +| DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`, but the bridge still opens legacy message boxes/save dialogs, calls `Canvas::I->project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6fd5946..1e39153 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -733,6 +733,12 @@ behind the renderer boundary. unsaved-project discard prompts, project open, layer refresh, title updates, and history clearing while those live effects remain tracked under `DEBT-0039`. +`App::request_close`, `App::save_document`, and +`App::continue_document_workflow_after_optional_save` now route through +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`. Implementation tasks: @@ -1272,6 +1278,15 @@ Results: - Focused document-open CTest coverage passed for `pp_app_core_document_route_tests`, `pp_app_core_document_session_tests`, and the `pano_cli_plan_open_route_*` smoke tests after the live bridge split. +- `PanoPainter`, `pp_app_core_document_session_tests`, and `pano_cli` built + after close request, document save, and dirty-workflow continuation execution + moved behind document-session services. A clean rebuild was required once + because MSVC reported the known Debug PDB `LNK1103` corruption, after which + the build passed. +- Focused document-session CTest coverage passed for + `pp_app_core_document_session_tests`, `pano_cli_simulate_app_session_*`, and + `pano_cli_plan_document_file/version_*` smoke tests after the live bridge + split. - `pp_app_core_document_recording_tests` passed, covering recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, and oversized progress-total clamping. diff --git a/src/app.cpp b/src/app.cpp index fb33336..39745d1 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -11,6 +11,7 @@ #include "app_core/document_route.h" #include "app_core/document_session.h" #include "legacy_document_open_services.h" +#include "legacy_document_session_services.h" #include "legacy_recording_services.h" #include "platform_api/platform_services.h" #include "renderer_gl/opengl_capabilities.h" @@ -208,26 +209,14 @@ bool App::request_close() const auto close_decision = pp::app::plan_close_request( Canvas::I->m_unsaved, dialog_already_opened); - if (close_decision == pp::app::CloseRequestDecision::close_now) - return true; - if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt) - { - auto* m = layout[main_id]->add_child(); - m->m_title->set_text("Unsaved document"); - m->m_message->set_text("Do you want to close without saving?"); - m->btn_ok->m_text->set_text("Yes"); - m->btn_ok->on_click = [this](Node*) { - request_app_close(); - Canvas::I->m_unsaved = false; - }; - m->btn_cancel->m_text->set_text("No"); - m->btn_cancel->on_click = [this,m](Node*) { - m->destroy(); - dialog_already_opened = false; - }; - dialog_already_opened = true; - } - return false; + const auto status = pp::panopainter::execute_legacy_close_request_decision( + *this, + close_decision, + dialog_already_opened); + if (!status.ok()) + LOG("Close request action failed: %s", status.message); + + return close_decision == pp::app::CloseRequestDecision::close_now; } void App::clear() diff --git a/src/app_core/document_session.h b/src/app_core/document_session.h index bee85b2..e8edd08 100644 --- a/src/app_core/document_session.h +++ b/src/app_core/document_session.h @@ -65,6 +65,31 @@ public: virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0; }; +class CloseRequestServices { +public: + virtual ~CloseRequestServices() = default; + + virtual void request_close_now() = 0; + virtual void show_unsaved_close_prompt() = 0; +}; + +class DocumentSaveServices { +public: + virtual ~DocumentSaveServices() = default; + + virtual void show_save_dialog() = 0; + virtual void save_existing_document() = 0; + virtual void save_document_version() = 0; +}; + +class DocumentWorkflowServices { +public: + virtual ~DocumentWorkflowServices() = default; + + virtual void continue_workflow_now() = 0; + virtual void prompt_save_before_continue() = 0; +}; + struct DocumentFileTarget { std::string name; std::string directory; @@ -160,6 +185,24 @@ struct NewDocumentPlan { : CloseRequestDecision::show_unsaved_prompt; } +[[nodiscard]] inline pp::foundation::Status execute_close_request_decision( + CloseRequestDecision decision, + CloseRequestServices& services) +{ + switch (decision) { + case CloseRequestDecision::close_now: + services.request_close_now(); + return pp::foundation::Status::success(); + case CloseRequestDecision::show_unsaved_prompt: + services.show_unsaved_close_prompt(); + return pp::foundation::Status::success(); + case CloseRequestDecision::wait_for_existing_prompt: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown close request decision"); +} + [[nodiscard]] constexpr DocumentSaveDecision plan_document_save( bool is_new_document, bool has_unsaved_changes, @@ -191,6 +234,27 @@ struct NewDocumentPlan { return DocumentSaveDecision::no_op; } +[[nodiscard]] inline pp::foundation::Status execute_document_save_decision( + DocumentSaveDecision decision, + DocumentSaveServices& services) +{ + switch (decision) { + case DocumentSaveDecision::no_op: + return pp::foundation::Status::success(); + case DocumentSaveDecision::show_save_dialog: + services.show_save_dialog(); + return pp::foundation::Status::success(); + case DocumentSaveDecision::save_existing: + services.save_existing_document(); + return pp::foundation::Status::success(); + case DocumentSaveDecision::save_version: + services.save_document_version(); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown document save decision"); +} + [[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow( bool has_canvas, bool has_unsaved_changes) noexcept @@ -204,6 +268,24 @@ struct NewDocumentPlan { : DocumentWorkflowDecision::continue_now; } +[[nodiscard]] inline pp::foundation::Status execute_document_workflow_decision( + DocumentWorkflowDecision decision, + DocumentWorkflowServices& services) +{ + switch (decision) { + case DocumentWorkflowDecision::unavailable: + return pp::foundation::Status::success(); + case DocumentWorkflowDecision::continue_now: + services.continue_workflow_now(); + return pp::foundation::Status::success(); + case DocumentWorkflowDecision::prompt_save_before_continue: + services.prompt_save_before_continue(); + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("unknown document workflow decision"); +} + [[nodiscard]] inline pp::foundation::Result make_document_file_target( std::string_view work_directory, std::string_view document_name) diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 386267d..d7a9e64 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -6,6 +6,7 @@ #include "app_core/document_session.h" #include "legacy_document_canvas_services.h" #include "legacy_document_layer_services.h" +#include "legacy_document_session_services.h" #include "legacy_history_services.h" #include "settings.h" #include "node_dialog_open.h" @@ -23,6 +24,8 @@ #define MP4V2_NO_STDINT_DEFS #include +#include + #ifdef __QUEST__ #include "oculus_vr.h" #elif __WEB__ @@ -138,34 +141,12 @@ void App::continue_document_workflow_after_optional_save(std::function a const bool has_canvas = canvas != nullptr; const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved; const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes); - switch (decision) { - case pp::app::DocumentWorkflowDecision::unavailable: - return; - case pp::app::DocumentWorkflowDecision::continue_now: - action(); - return; - case pp::app::DocumentWorkflowDecision::prompt_save_before_continue: - break; - } - - auto m = layout[main_id]->add_child(); - m->m_title->set_text("Unsaved document"); - m->m_message->set_text("Would you like to save this document before closing?"); - m->btn_ok->m_text->set_text("Yes"); - m->btn_cancel->m_text->set_text("No"); - m->btn_ok->on_click = [this, m, action](Node*) { - Canvas::I->project_save([this, m, action](bool success) { - if (success) - action(); - else - message_box("Saving Error", "There was a problem saving the document"); - }); - m->destroy(); - }; - m->btn_cancel->on_click = [m, action](Node*) { - action(); - m->destroy(); - }; + const auto status = pp::panopainter::execute_legacy_document_workflow_decision( + *this, + decision, + std::move(action)); + if (!status.ok()) + LOG("Document workflow action failed: %s", status.message); } void App::dialog_newdoc() @@ -350,19 +331,9 @@ void App::save_document(pp::app::DocumentSaveIntent intent) 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; - } + const auto status = pp::panopainter::execute_legacy_document_save_decision(*this, decision); + if (!status.ok()) + LOG("Document save action failed: %s", status.message); } void App::dialog_save() diff --git a/src/legacy_document_session_services.cpp b/src/legacy_document_session_services.cpp new file mode 100644 index 0000000..039a756 --- /dev/null +++ b/src/legacy_document_session_services.cpp @@ -0,0 +1,143 @@ +#include "pch.h" + +#include "legacy_document_session_services.h" + +#include "app.h" + +namespace pp::panopainter { +namespace { + +class LegacyCloseRequestServices final : public pp::app::CloseRequestServices { +public: + LegacyCloseRequestServices(App& app, bool& dialog_already_opened) noexcept + : app_(app) + , dialog_already_opened_(dialog_already_opened) + { + } + + void request_close_now() override + { + } + + void show_unsaved_close_prompt() override + { + auto* app = &app_; + auto* dialog_already_opened = &dialog_already_opened_; + auto* m = app_.layout[app_.main_id]->add_child(); + m->m_title->set_text("Unsaved document"); + m->m_message->set_text("Do you want to close without saving?"); + m->btn_ok->m_text->set_text("Yes"); + 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; + }; + dialog_already_opened_ = true; + } + +private: + App& app_; + bool& dialog_already_opened_; +}; + +class LegacyDocumentSaveServices final : public pp::app::DocumentSaveServices { +public: + explicit LegacyDocumentSaveServices(App& app) noexcept + : app_(app) + { + } + + void show_save_dialog() override + { + app_.dialog_save(); + } + + void save_existing_document() override + { + Canvas::I->project_save(); + } + + void save_document_version() override + { + app_.dialog_save_ver(); + } + +private: + App& app_; +}; + +class LegacyDocumentWorkflowServices final : public pp::app::DocumentWorkflowServices { +public: + LegacyDocumentWorkflowServices(App& app, std::function action) noexcept + : app_(app) + , action_(std::move(action)) + { + } + + void continue_workflow_now() override + { + action_(); + } + + void prompt_save_before_continue() override + { + auto m = app_.layout[app_.main_id]->add_child(); + m->m_title->set_text("Unsaved document"); + m->m_message->set_text("Would you like to save this document before closing?"); + m->btn_ok->m_text->set_text("Yes"); + m->btn_cancel->m_text->set_text("No"); + 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"); + }); + m->destroy(); + }; + m->btn_cancel->on_click = [m, action](Node*) { + action(); + m->destroy(); + }; + } + +private: + App& app_; + std::function action_; +}; + +} // namespace + +pp::foundation::Status execute_legacy_close_request_decision( + App& app, + pp::app::CloseRequestDecision decision, + bool& dialog_already_opened) +{ + LegacyCloseRequestServices services(app, dialog_already_opened); + return pp::app::execute_close_request_decision(decision, services); +} + +pp::foundation::Status execute_legacy_document_save_decision( + App& app, + pp::app::DocumentSaveDecision decision) +{ + LegacyDocumentSaveServices services(app); + return pp::app::execute_document_save_decision(decision, services); +} + +pp::foundation::Status execute_legacy_document_workflow_decision( + App& app, + pp::app::DocumentWorkflowDecision decision, + std::function action) +{ + LegacyDocumentWorkflowServices services(app, std::move(action)); + return pp::app::execute_document_workflow_decision(decision, services); +} + +} // namespace pp::panopainter diff --git a/src/legacy_document_session_services.h b/src/legacy_document_session_services.h new file mode 100644 index 0000000..05706ab --- /dev/null +++ b/src/legacy_document_session_services.h @@ -0,0 +1,26 @@ +#pragma once + +#include "app_core/document_session.h" +#include "foundation/result.h" + +#include + +class App; + +namespace pp::panopainter { + +[[nodiscard]] pp::foundation::Status execute_legacy_close_request_decision( + App& app, + pp::app::CloseRequestDecision decision, + bool& dialog_already_opened); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_save_decision( + App& app, + pp::app::DocumentSaveDecision decision); + +[[nodiscard]] pp::foundation::Status execute_legacy_document_workflow_decision( + App& app, + pp::app::DocumentWorkflowDecision decision, + std::function action); + +} // namespace pp::panopainter diff --git a/tests/app_core/document_session_tests.cpp b/tests/app_core/document_session_tests.cpp index 9209a55..4d98012 100644 --- a/tests/app_core/document_session_tests.cpp +++ b/tests/app_core/document_session_tests.cpp @@ -43,6 +43,70 @@ public: std::string call_order; }; +class FakeCloseRequestServices final : public pp::app::CloseRequestServices { +public: + void request_close_now() override + { + close_now += 1; + call_order += "close;"; + } + + void show_unsaved_close_prompt() override + { + close_prompts += 1; + call_order += "prompt;"; + } + + int close_now = 0; + int close_prompts = 0; + std::string call_order; +}; + +class FakeDocumentSaveServices final : public pp::app::DocumentSaveServices { +public: + void show_save_dialog() override + { + dialogs += 1; + call_order += "dialog;"; + } + + void save_existing_document() override + { + saves += 1; + call_order += "save;"; + } + + void save_document_version() override + { + versions += 1; + call_order += "version;"; + } + + int dialogs = 0; + int saves = 0; + int versions = 0; + std::string call_order; +}; + +class FakeDocumentWorkflowServices final : public pp::app::DocumentWorkflowServices { +public: + void continue_workflow_now() override + { + continues += 1; + call_order += "continue;"; + } + + void prompt_save_before_continue() override + { + prompts += 1; + call_order += "prompt-save;"; + } + + int continues = 0; + int prompts = 0; + std::string call_order; +}; + [[nodiscard]] pp::app::DocumentOpenRoute project_route() { return { @@ -212,6 +276,25 @@ 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 close_request_executor_dispatches_and_preserves_wait(pp::tests::Harness& harness) +{ + FakeCloseRequestServices services; + + PP_EXPECT( + harness, + pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::close_now, services).ok()); + PP_EXPECT( + harness, + pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::show_unsaved_prompt, services).ok()); + PP_EXPECT( + harness, + pp::app::execute_close_request_decision(pp::app::CloseRequestDecision::wait_for_existing_prompt, services).ok()); + + PP_EXPECT(harness, services.close_now == 1); + PP_EXPECT(harness, services.close_prompts == 1); + PP_EXPECT(harness, services.call_order == "close;prompt;"); +} + void save_clean_existing_document_is_no_op(pp::tests::Harness& harness) { PP_EXPECT( @@ -220,6 +303,29 @@ void save_clean_existing_document_is_no_op(pp::tests::Harness& harness) == pp::app::DocumentSaveDecision::no_op); } +void save_executor_dispatches_visible_work_and_no_ops_cleanly(pp::tests::Harness& harness) +{ + FakeDocumentSaveServices services; + + PP_EXPECT( + harness, + pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::show_save_dialog, services).ok()); + PP_EXPECT( + harness, + pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::save_existing, services).ok()); + PP_EXPECT( + harness, + pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::save_version, services).ok()); + PP_EXPECT( + harness, + pp::app::execute_document_save_decision(pp::app::DocumentSaveDecision::no_op, services).ok()); + + PP_EXPECT(harness, services.dialogs == 1); + PP_EXPECT(harness, services.saves == 1); + PP_EXPECT(harness, services.versions == 1); + PP_EXPECT(harness, services.call_order == "dialog;save;version;"); +} + void save_new_or_dirty_document_has_user_visible_work(pp::tests::Harness& harness) { PP_EXPECT( @@ -292,6 +398,34 @@ void workflow_with_dirty_canvas_prompts_for_save(pp::tests::Harness& harness) == pp::app::DocumentWorkflowDecision::prompt_save_before_continue); } +void workflow_executor_dispatches_continue_prompt_and_unavailable(pp::tests::Harness& harness) +{ + FakeDocumentWorkflowServices services; + + PP_EXPECT( + harness, + pp::app::execute_document_workflow_decision( + pp::app::DocumentWorkflowDecision::continue_now, + services) + .ok()); + PP_EXPECT( + harness, + pp::app::execute_document_workflow_decision( + pp::app::DocumentWorkflowDecision::prompt_save_before_continue, + services) + .ok()); + PP_EXPECT( + harness, + pp::app::execute_document_workflow_decision( + pp::app::DocumentWorkflowDecision::unavailable, + services) + .ok()); + + PP_EXPECT(harness, services.continues == 1); + PP_EXPECT(harness, services.prompts == 1); + PP_EXPECT(harness, services.call_order == "continue;prompt-save;"); +} + void document_file_target_rejects_empty_name(pp::tests::Harness& harness) { const auto target = pp::app::make_document_file_target("D:/Paint", ""); @@ -484,13 +618,16 @@ int main() harness.run("document open executor rejects mismatched routes", document_open_executor_rejects_mismatched_routes); 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("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); 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); harness.run("workflow without canvas is unavailable", workflow_without_canvas_is_unavailable); harness.run("workflow with clean canvas continues now", workflow_with_clean_canvas_continues_now); harness.run("workflow with dirty canvas prompts for save", workflow_with_dirty_canvas_prompts_for_save); + harness.run("workflow executor dispatches continue prompt and unavailable", workflow_executor_dispatches_continue_prompt_and_unavailable); harness.run("document file target rejects empty name", document_file_target_rejects_empty_name); harness.run("document file target builds legacy ppi path", document_file_target_builds_legacy_ppi_path); harness.run("document file write prompts only for existing targets", document_file_write_prompts_only_for_existing_targets);