Centralize legacy document open bridge

This commit is contained in:
2026-06-04 13:20:14 +02:00
parent 1984b71a0a
commit d980b81bd7
9 changed files with 336 additions and 64 deletions

View File

@@ -84,6 +84,8 @@ set(PP_PANOPAINTER_APP_SOURCES
src/app_vr.cpp src/app_vr.cpp
src/legacy_cloud_services.cpp src/legacy_cloud_services.cpp
src/legacy_cloud_services.h src/legacy_cloud_services.h
src/legacy_document_open_services.cpp
src/legacy_document_open_services.h
src/platform_legacy/legacy_platform_services.cpp src/platform_legacy/legacy_platform_services.cpp
src/platform_legacy/legacy_platform_services.h src/platform_legacy/legacy_platform_services.h
src/version.cpp src/version.cpp

View File

@@ -583,10 +583,15 @@ Known local toolchain state:
decisions, plus bulk upload progress visibility, zero-file, and clamped decisions, plus bulk upload progress visibility, zero-file, and clamped
progress-total decisions. progress-total decisions.
- `pp_app_core_document_session_tests` covers clean and dirty app session, - `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning, save-request, save-before-workflow, document-open action planning and executor dispatch/rejection, save-request,
new-document target/resolution/overwrite planning, document file target, save-before-workflow, new-document target/resolution/overwrite planning,
combined save-file overwrite planning, and save-version target decisions document file target, combined save-file overwrite planning, and save-version
without requiring a window, canvas, or message box. 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_history_services.*` is the current app-shell bridge between - `src/legacy_history_services.*` is the current app-shell bridge between
`pp_app_core` history plans and legacy `ActionManager`; toolbar and `pp_app_core` history plans and legacy `ActionManager`; toolbar and
`NodeCanvas` hotkeys share it while document-history extraction remains `NodeCanvas` hotkeys share it while document-history extraction remains

View File

@@ -56,6 +56,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior | | DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| 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-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-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 |
## Closed Debt ## Closed Debt

View File

@@ -728,6 +728,11 @@ layer-refresh, and action-history work remains tracked under `DEBT-0038`.
`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.
`App::open_document` now routes through the app-core document-open executor and
`src/legacy_document_open_services.*`, preserving ABR/PPBR import prompts,
unsaved-project discard prompts, project open, layer refresh, title updates,
and history clearing while those live effects remain tracked under
`DEBT-0039`.
Implementation tasks: Implementation tasks:
@@ -1260,6 +1265,13 @@ Results:
- `scripts/automation/package-smoke.ps1 -Preset windows-msvc-default - `scripts/automation/package-smoke.ps1 -Preset windows-msvc-default
-Configuration Debug` passed executable/data checks after the cloud bridge -Configuration Debug` passed executable/data checks after the cloud bridge
split; package target migration blockers remain under `DEBT-0011`. split; package target migration blockers remain under `DEBT-0011`.
- `PanoPainter`, `pp_app_core_document_session_tests`, and `pano_cli` built
after `App::open_document` moved live execution behind the document-open
services bridge. A clean rebuild was required once because MSVC reported the
known Debug PDB `LNK1103` corruption, after which the build passed.
- 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.
- `pp_app_core_document_recording_tests` passed, covering recording start/stop, - `pp_app_core_document_recording_tests` passed, covering recording start/stop,
clear, platform recorded-file cleanup, frame-count reset, export progress clear, platform recorded-file cleanup, frame-count reset, export progress
totals, and oversized progress-total clamping. totals, and oversized progress-total clamping.

View File

@@ -10,7 +10,7 @@
#include "app_core/document_recording.h" #include "app_core/document_recording.h"
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "legacy_history_services.h" #include "legacy_document_open_services.h"
#include "legacy_recording_services.h" #include "legacy_recording_services.h"
#include "platform_api/platform_services.h" #include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
@@ -197,65 +197,9 @@ void App::open_document(std::string path)
const bool has_unsaved_project = const bool has_unsaved_project =
route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved; route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved;
const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project); const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project);
if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr) const auto status = pp::panopainter::execute_legacy_document_open_plan(*this, open_plan, route.value());
{ if (!status.ok())
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true); LOG("Document open action failed: %s", status.message);
mb->on_submit = [this, path] (Node* target) {
std::thread(&NodePanelBrushPreset::import_abr, presets, path).detach();
target->destroy();
};
}
else if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_ppbr)
{
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) {
std::thread(&NodePanelBrushPreset::import_ppbr, presets, path).detach();
target->destroy();
};
}
else
{
const std::string base = route.value().directory;
const std::string name = route.value().name;
auto open_action = [this, path, base, name] {
doc_name = name;
doc_dir = base;
doc_path = path;
canvas->reset_camera();
layers->clear();
canvas->m_canvas->project_open(path, [this](bool success){
// on complete
if (success)
{
title_update();
for (int layer_index = 0; layer_index < canvas->m_canvas->m_layers.size(); layer_index++)
{
auto l = layers->add_layer(canvas->m_canvas->m_layers[layer_index]->m_name.c_str(), false);
l->m_visibility->set_value(canvas->m_canvas->m_layers[layer_index]->m_visible);
}
}
else
{
message_box("Open Document Error",
"There was an error opening the document.\n"
"It may be inaccessible or corrupted.");
}
});
pp::panopainter::clear_legacy_history();
};
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
{
open_action();
}
else
{
auto mb = message_box("Unsaved document", "Do you want to close the unsaved document before opening the file?", true);
mb->on_submit = [this, open_action] (Node* target) {
open_action();
target->destroy();
};
}
}
} }
bool App::request_close() bool App::request_close()

View File

@@ -55,6 +55,16 @@ enum class DocumentOpenPlanAction {
prompt_import_ppbr, prompt_import_ppbr,
}; };
class DocumentOpenServices {
public:
virtual ~DocumentOpenServices() = default;
virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0;
virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0;
virtual void open_project_now(const DocumentOpenRoute& route) = 0;
virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0;
};
struct DocumentFileTarget { struct DocumentFileTarget {
std::string name; std::string name;
std::string directory; std::string directory;
@@ -102,6 +112,41 @@ struct NewDocumentPlan {
return DocumentOpenPlanAction::open_project_now; return DocumentOpenPlanAction::open_project_now;
} }
[[nodiscard]] inline pp::foundation::Status execute_document_open_plan(
DocumentOpenPlanAction action,
const DocumentOpenRoute& route,
DocumentOpenServices& services)
{
switch (action) {
case DocumentOpenPlanAction::open_project_now:
if (route.kind != DocumentOpenKind::open_project) {
return pp::foundation::Status::invalid_argument("open-project action requires a project route");
}
services.open_project_now(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_discard_unsaved_project:
if (route.kind != DocumentOpenKind::open_project) {
return pp::foundation::Status::invalid_argument("discard prompt requires a project route");
}
services.prompt_discard_unsaved_project(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_import_abr:
if (route.kind != DocumentOpenKind::import_abr) {
return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route");
}
services.prompt_import_abr(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_import_ppbr:
if (route.kind != DocumentOpenKind::import_ppbr) {
return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route");
}
services.prompt_import_ppbr(route);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document open action");
}
[[nodiscard]] constexpr CloseRequestDecision plan_close_request( [[nodiscard]] constexpr CloseRequestDecision plan_close_request(
bool has_unsaved_changes, bool has_unsaved_changes,
bool close_prompt_already_open) noexcept bool close_prompt_already_open) noexcept

View File

@@ -0,0 +1,101 @@
#include "pch.h"
#include "legacy_document_open_services.h"
#include "app.h"
#include "legacy_history_services.h"
#include "node_panel_brush.h"
#include "node_panel_layer.h"
namespace pp::panopainter {
namespace {
void open_legacy_project(App& app, const pp::app::DocumentOpenRoute& route)
{
app.doc_name = route.name;
app.doc_dir = route.directory;
app.doc_path = route.path;
app.canvas->reset_camera();
app.layers->clear();
app.canvas->m_canvas->project_open(route.path, [&app](bool success) {
if (success)
{
app.title_update();
for (std::size_t layer_index = 0; layer_index < app.canvas->m_canvas->m_layers.size(); ++layer_index)
{
auto layer = app.layers->add_layer(app.canvas->m_canvas->m_layers[layer_index]->m_name.c_str(), false);
layer->m_visibility->set_value(app.canvas->m_canvas->m_layers[layer_index]->m_visible);
}
}
else
{
app.message_box(
"Open Document Error",
"There was an error opening the document.\n"
"It may be inaccessible or corrupted.");
}
});
pp::panopainter::clear_legacy_history();
}
class LegacyDocumentOpenServices final : public pp::app::DocumentOpenServices {
public:
explicit LegacyDocumentOpenServices(App& app) noexcept
: app_(app)
{
}
void prompt_import_abr(const pp::app::DocumentOpenRoute& route) override
{
auto* app = &app_;
auto mb = app_.message_box("Import ABR", "Would you like to import the brushes?", true);
mb->on_submit = [app, path = route.path](Node* target) {
std::thread(&NodePanelBrushPreset::import_abr, app->presets, path).detach();
target->destroy();
};
}
void prompt_import_ppbr(const pp::app::DocumentOpenRoute& route) override
{
auto* app = &app_;
auto mb = app_.message_box("Import PPBR", "Would you like to import the brushes?", true);
mb->on_submit = [app, path = route.path](Node* target) {
std::thread(&NodePanelBrushPreset::import_ppbr, app->presets, path).detach();
target->destroy();
};
}
void open_project_now(const pp::app::DocumentOpenRoute& route) override
{
open_legacy_project(app_, route);
}
void prompt_discard_unsaved_project(const pp::app::DocumentOpenRoute& route) override
{
auto* app = &app_;
auto mb = app_.message_box(
"Unsaved document",
"Do you want to close the unsaved document before opening the file?",
true);
mb->on_submit = [app, route](Node* target) {
open_legacy_project(*app, route);
target->destroy();
};
}
private:
App& app_;
};
} // namespace
pp::foundation::Status execute_legacy_document_open_plan(
App& app,
pp::app::DocumentOpenPlanAction action,
const pp::app::DocumentOpenRoute& route)
{
LegacyDocumentOpenServices services(app);
return pp::app::execute_document_open_plan(action, route, services);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,15 @@
#pragma once
#include "app_core/document_session.h"
#include "foundation/result.h"
class App;
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_document_open_plan(
App& app,
pp::app::DocumentOpenPlanAction action,
const pp::app::DocumentOpenRoute& route);
} // namespace pp::panopainter

View File

@@ -1,8 +1,81 @@
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "test_harness.h" #include "test_harness.h"
#include <string>
namespace { namespace {
class FakeDocumentOpenServices final : public pp::app::DocumentOpenServices {
public:
void prompt_import_abr(const pp::app::DocumentOpenRoute& route) override
{
abr_prompts += 1;
last_path = route.path;
call_order += "abr;";
}
void prompt_import_ppbr(const pp::app::DocumentOpenRoute& route) override
{
ppbr_prompts += 1;
last_path = route.path;
call_order += "ppbr;";
}
void open_project_now(const pp::app::DocumentOpenRoute& route) override
{
project_opens += 1;
last_path = route.path;
call_order += "open;";
}
void prompt_discard_unsaved_project(const pp::app::DocumentOpenRoute& route) override
{
discard_prompts += 1;
last_path = route.path;
call_order += "discard;";
}
int abr_prompts = 0;
int ppbr_prompts = 0;
int project_opens = 0;
int discard_prompts = 0;
std::string last_path;
std::string call_order;
};
[[nodiscard]] pp::app::DocumentOpenRoute project_route()
{
return {
.kind = pp::app::DocumentOpenKind::open_project,
.path = "D:/Paint/demo.ppi",
.directory = "D:/Paint",
.name = "demo",
.extension = "ppi",
};
}
[[nodiscard]] pp::app::DocumentOpenRoute abr_route()
{
return {
.kind = pp::app::DocumentOpenKind::import_abr,
.path = "D:/Paint/clouds.abr",
.directory = "D:/Paint",
.name = "clouds",
.extension = "abr",
};
}
[[nodiscard]] pp::app::DocumentOpenRoute ppbr_route()
{
return {
.kind = pp::app::DocumentOpenKind::import_ppbr,
.path = "D:/Paint/brush.ppbr",
.directory = "D:/Paint",
.name = "brush",
.extension = "ppbr",
};
}
void project_open_clean_document_executes_immediately(pp::tests::Harness& harness) void project_open_clean_document_executes_immediately(pp::tests::Harness& harness)
{ {
PP_EXPECT(harness, pp::app::plan_project_open(false) == pp::app::ProjectOpenDecision::open_now); PP_EXPECT(harness, pp::app::plan_project_open(false) == pp::app::ProjectOpenDecision::open_now);
@@ -47,6 +120,78 @@ void document_open_brush_imports_prompt_regardless_of_unsaved_state(pp::tests::H
== pp::app::DocumentOpenPlanAction::prompt_import_ppbr); == pp::app::DocumentOpenPlanAction::prompt_import_ppbr);
} }
void document_open_executor_dispatches_all_actions(pp::tests::Harness& harness)
{
FakeDocumentOpenServices services;
const auto project = project_route();
const auto abr = abr_route();
const auto ppbr = ppbr_route();
PP_EXPECT(
harness,
pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::open_project_now,
project,
services)
.ok());
PP_EXPECT(
harness,
pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::prompt_discard_unsaved_project,
project,
services)
.ok());
PP_EXPECT(
harness,
pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::prompt_import_abr,
abr,
services)
.ok());
PP_EXPECT(
harness,
pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::prompt_import_ppbr,
ppbr,
services)
.ok());
PP_EXPECT(harness, services.project_opens == 1);
PP_EXPECT(harness, services.discard_prompts == 1);
PP_EXPECT(harness, services.abr_prompts == 1);
PP_EXPECT(harness, services.ppbr_prompts == 1);
PP_EXPECT(harness, services.last_path == "D:/Paint/brush.ppbr");
PP_EXPECT(harness, services.call_order == "open;discard;abr;ppbr;");
}
void document_open_executor_rejects_mismatched_routes(pp::tests::Harness& harness)
{
FakeDocumentOpenServices services;
PP_EXPECT(
harness,
!pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::open_project_now,
abr_route(),
services)
.ok());
PP_EXPECT(
harness,
!pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::prompt_import_abr,
project_route(),
services)
.ok());
PP_EXPECT(
harness,
!pp::app::execute_document_open_plan(
pp::app::DocumentOpenPlanAction::prompt_import_ppbr,
abr_route(),
services)
.ok());
PP_EXPECT(harness, services.call_order.empty());
}
void close_clean_document_executes_immediately(pp::tests::Harness& harness) void close_clean_document_executes_immediately(pp::tests::Harness& harness)
{ {
PP_EXPECT( PP_EXPECT(
@@ -335,6 +480,8 @@ int main()
harness.run( harness.run(
"document open brush imports prompt regardless of unsaved state", "document open brush imports prompt regardless of unsaved state",
document_open_brush_imports_prompt_regardless_of_unsaved_state); document_open_brush_imports_prompt_regardless_of_unsaved_state);
harness.run("document open executor dispatches all actions", document_open_executor_dispatches_all_actions);
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 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 clean existing document is no op", save_clean_existing_document_is_no_op);