Plan document open actions in app core
This commit is contained in:
@@ -394,6 +394,9 @@ Known local toolchain state:
|
||||
- `pano_cli classify-open` exposes the `pp_app_core` document-open route
|
||||
contract as JSON and is covered for project files, ABR imports, PPBR
|
||||
imports, and malformed path rejection.
|
||||
- `pano_cli plan-open-route` exposes `pp_app_core` document-open action
|
||||
planning as JSON and is covered for clean project open, dirty project
|
||||
discard-prompt, and ABR import-prompt states.
|
||||
- `pano_cli plan-document-file` exposes `pp_app_core` document-name
|
||||
validation, legacy `.ppi` path construction, and overwrite-prompt decisions
|
||||
as JSON and is covered for save-now and existing-target overwrite states.
|
||||
@@ -417,9 +420,9 @@ Known local toolchain state:
|
||||
directory/stem targets, picked-directory stems, MP4 suggested names, and
|
||||
invalid export naming inputs.
|
||||
- `pp_app_core_document_session_tests` covers clean and dirty app session,
|
||||
save-request, save-before-workflow, document file target, overwrite, and
|
||||
save-version target decisions without requiring a window, canvas, or message
|
||||
box.
|
||||
document-open action planning, save-request, save-before-workflow, document
|
||||
file target, overwrite, and save-version target decisions without requiring
|
||||
a window, canvas, or message box.
|
||||
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
|
||||
through the vcpkg preset; default and Android validation still use the
|
||||
retained vendored fallback tracked by DEBT-0012.
|
||||
|
||||
@@ -12,7 +12,7 @@ and validation command.
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| 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, open-action plan tests, CLI route/action smoke, app 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/save-before-workflow/name/overwrite/version-target decision tests, CLI session, document-file, and document-version smoke, app close/open/save/new/browse smoke |
|
||||
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
|
||||
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
|
||||
|
||||
@@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets |
|
||||
| DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation |
|
||||
| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new/save-as document file naming and overwrite decisions, save-version target decisions, export target naming/path decisions, `pano_cli classify-open`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document loading, saving, and export execution 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_export_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `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, new/open/browse dirty-document workflow prompts, new/save-as document file naming and overwrite decisions, save-version target decisions, export target naming/path decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document loading, brush import execution, saving, and export execution still reach legacy `Canvas::I`/UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries |
|
||||
| DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path |
|
||||
| DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated |
|
||||
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |
|
||||
|
||||
@@ -423,9 +423,11 @@ the applied stroke payload survives inspect/load round-trip automation, with a
|
||||
rejection smoke test for unsafe tiny canvas dimensions.
|
||||
`pano_cli classify-open` exposes the pure `pp_app_core` document-open route
|
||||
contract for project files, ABR imports, PPBR imports, and malformed path
|
||||
rejection. `pano_cli simulate-app-session` exposes the pure `pp_app_core`
|
||||
session decisions used by project-open, app-close, save, save-as, and
|
||||
save-version flows, plus the save-before-continue workflow gate used by
|
||||
rejection. `pano_cli plan-open-route` exposes the pure `pp_app_core`
|
||||
document-open action plan for clean project open, dirty project prompt, and
|
||||
brush-import prompt flows. `pano_cli simulate-app-session` exposes the pure
|
||||
`pp_app_core` session decisions used by project-open, app-close, save, save-as,
|
||||
and save-version flows, plus the save-before-continue workflow gate used by
|
||||
new-document/open/browse dialogs. `pano_cli plan-document-file` exposes the
|
||||
same app-core document-name validation, legacy `.ppi` path construction, and
|
||||
overwrite prompt decision used by new-document and save-as dialogs.
|
||||
|
||||
10
src/app.cpp
10
src/app.cpp
@@ -193,7 +193,10 @@ void App::open_document(std::string path)
|
||||
if (!route)
|
||||
return;
|
||||
|
||||
if (route.value().kind == pp::app::DocumentOpenKind::import_abr)
|
||||
const bool has_unsaved_project =
|
||||
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);
|
||||
if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr)
|
||||
{
|
||||
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
|
||||
mb->on_submit = [this, path] (Node* target) {
|
||||
@@ -201,7 +204,7 @@ void App::open_document(std::string path)
|
||||
target->destroy();
|
||||
};
|
||||
}
|
||||
else if (route.value().kind == pp::app::DocumentOpenKind::import_ppbr)
|
||||
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) {
|
||||
@@ -239,8 +242,7 @@ void App::open_document(std::string path)
|
||||
});
|
||||
ActionManager::clear();
|
||||
};
|
||||
const auto open_decision = pp::app::plan_project_open(Canvas::I->m_unsaved);
|
||||
if (open_decision == pp::app::ProjectOpenDecision::open_now)
|
||||
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
|
||||
{
|
||||
open_action();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/document_route.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cctype>
|
||||
@@ -46,6 +47,13 @@ enum class DocumentFileWriteDecision {
|
||||
prompt_overwrite,
|
||||
};
|
||||
|
||||
enum class DocumentOpenPlanAction {
|
||||
open_project_now,
|
||||
prompt_discard_unsaved_project,
|
||||
prompt_import_abr,
|
||||
prompt_import_ppbr,
|
||||
};
|
||||
|
||||
struct DocumentFileTarget {
|
||||
std::string name;
|
||||
std::string directory;
|
||||
@@ -64,6 +72,24 @@ struct DocumentVersionTarget {
|
||||
: ProjectOpenDecision::open_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
|
||||
DocumentOpenKind kind,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentOpenKind::import_abr:
|
||||
return DocumentOpenPlanAction::prompt_import_abr;
|
||||
case DocumentOpenKind::import_ppbr:
|
||||
return DocumentOpenPlanAction::prompt_import_ppbr;
|
||||
case DocumentOpenKind::open_project:
|
||||
return has_unsaved_changes
|
||||
? DocumentOpenPlanAction::prompt_discard_unsaved_project
|
||||
: DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
return DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
|
||||
bool has_unsaved_changes,
|
||||
bool close_prompt_already_open) noexcept
|
||||
|
||||
@@ -381,6 +381,24 @@ if(TARGET pano_cli)
|
||||
LABELS "app;integration;desktop-fast;fuzz"
|
||||
)
|
||||
|
||||
add_test(NAME pano_cli_plan_open_route_project_clean_smoke
|
||||
COMMAND pano_cli plan-open-route --path "D:/Paint/Scenes/demo.ppi")
|
||||
set_tests_properties(pano_cli_plan_open_route_project_clean_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-open-route\".*\"kind\":\"open-project\".*\"unsaved\":false.*\"action\":\"open-project-now\"")
|
||||
|
||||
add_test(NAME pano_cli_plan_open_route_project_unsaved_smoke
|
||||
COMMAND pano_cli plan-open-route --path "D:/Paint/Scenes/demo.ppi" --unsaved)
|
||||
set_tests_properties(pano_cli_plan_open_route_project_unsaved_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-open-route\".*\"kind\":\"open-project\".*\"unsaved\":true.*\"action\":\"prompt-discard-unsaved-project\"")
|
||||
|
||||
add_test(NAME pano_cli_plan_open_route_abr_import_smoke
|
||||
COMMAND pano_cli plan-open-route --path "D:/Paint/Brushes/clouds.ABR" --unsaved)
|
||||
set_tests_properties(pano_cli_plan_open_route_abr_import_smoke PROPERTIES
|
||||
LABELS "app;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"plan-open-route\".*\"kind\":\"import-abr\".*\"unsaved\":true.*\"action\":\"prompt-import-abr\"")
|
||||
|
||||
add_test(NAME pano_cli_plan_document_file_save_now_smoke
|
||||
COMMAND pano_cli plan-document-file --work-dir D:/Paint --name demo)
|
||||
set_tests_properties(pano_cli_plan_document_file_save_now_smoke PROPERTIES
|
||||
|
||||
@@ -15,6 +15,38 @@ void project_open_dirty_document_prompts_for_discard(pp::tests::Harness& harness
|
||||
pp::app::plan_project_open(true) == pp::app::ProjectOpenDecision::prompt_discard_unsaved);
|
||||
}
|
||||
|
||||
void document_open_project_respects_unsaved_state(pp::tests::Harness& harness)
|
||||
{
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::open_project, false)
|
||||
== pp::app::DocumentOpenPlanAction::open_project_now);
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::open_project, true)
|
||||
== pp::app::DocumentOpenPlanAction::prompt_discard_unsaved_project);
|
||||
}
|
||||
|
||||
void document_open_brush_imports_prompt_regardless_of_unsaved_state(pp::tests::Harness& harness)
|
||||
{
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::import_abr, false)
|
||||
== pp::app::DocumentOpenPlanAction::prompt_import_abr);
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::import_abr, true)
|
||||
== pp::app::DocumentOpenPlanAction::prompt_import_abr);
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::import_ppbr, false)
|
||||
== pp::app::DocumentOpenPlanAction::prompt_import_ppbr);
|
||||
PP_EXPECT(
|
||||
harness,
|
||||
pp::app::plan_document_open(pp::app::DocumentOpenKind::import_ppbr, true)
|
||||
== pp::app::DocumentOpenPlanAction::prompt_import_ppbr);
|
||||
}
|
||||
|
||||
void close_clean_document_executes_immediately(pp::tests::Harness& harness)
|
||||
{
|
||||
PP_EXPECT(
|
||||
@@ -206,6 +238,10 @@ int main()
|
||||
pp::tests::Harness harness;
|
||||
harness.run("project open clean document executes immediately", project_open_clean_document_executes_immediately);
|
||||
harness.run("project open dirty document prompts for discard", project_open_dirty_document_prompts_for_discard);
|
||||
harness.run("document open project respects unsaved state", document_open_project_respects_unsaved_state);
|
||||
harness.run(
|
||||
"document open brush imports prompt regardless of unsaved state",
|
||||
document_open_brush_imports_prompt_regardless_of_unsaved_state);
|
||||
harness.run("close clean document executes immediately", close_clean_document_executes_immediately);
|
||||
harness.run("close dirty document opens one prompt", close_dirty_document_opens_one_prompt);
|
||||
harness.run("save clean existing document is no op", save_clean_existing_document_is_no_op);
|
||||
|
||||
@@ -97,6 +97,11 @@ struct ClassifyOpenArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct PlanOpenRouteArgs {
|
||||
std::string path;
|
||||
bool unsaved = false;
|
||||
};
|
||||
|
||||
struct PlanDocumentFileArgs {
|
||||
std::string work_directory;
|
||||
std::string name;
|
||||
@@ -275,6 +280,22 @@ const char* project_open_decision_name(pp::app::ProjectOpenDecision decision) no
|
||||
return "open-now";
|
||||
}
|
||||
|
||||
const char* document_open_plan_action_name(pp::app::DocumentOpenPlanAction action) noexcept
|
||||
{
|
||||
switch (action) {
|
||||
case pp::app::DocumentOpenPlanAction::open_project_now:
|
||||
return "open-project-now";
|
||||
case pp::app::DocumentOpenPlanAction::prompt_discard_unsaved_project:
|
||||
return "prompt-discard-unsaved-project";
|
||||
case pp::app::DocumentOpenPlanAction::prompt_import_abr:
|
||||
return "prompt-import-abr";
|
||||
case pp::app::DocumentOpenPlanAction::prompt_import_ppbr:
|
||||
return "prompt-import-ppbr";
|
||||
}
|
||||
|
||||
return "open-project-now";
|
||||
}
|
||||
|
||||
const char* close_request_decision_name(pp::app::CloseRequestDecision decision) noexcept
|
||||
{
|
||||
switch (decision) {
|
||||
@@ -377,6 +398,7 @@ void print_help()
|
||||
<< " import-image --path FILE [--document-width N] [--document-height N] [--face N] [--x N] [--y N]\n"
|
||||
<< " inspect-project --path FILE\n"
|
||||
<< " classify-open --path FILE\n"
|
||||
<< " plan-open-route --path FILE [--unsaved]\n"
|
||||
<< " plan-document-file --work-dir DIR --name NAME [--target-exists]\n"
|
||||
<< " plan-document-version --directory DIR --doc-name NAME [--existing-path FILE]\n"
|
||||
<< " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n"
|
||||
@@ -1221,6 +1243,60 @@ int classify_open(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_open_route_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
PlanOpenRouteArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--path") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
args.path = argv[++i];
|
||||
} else if (key == "--unsaved") {
|
||||
args.unsaved = true;
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
if (args.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
int plan_open_route(int argc, char** argv)
|
||||
{
|
||||
PlanOpenRouteArgs args;
|
||||
const auto status = parse_plan_open_route_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("plan-open-route", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto route = pp::app::classify_document_open_path(args.path);
|
||||
if (!route) {
|
||||
print_error("plan-open-route", route.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto action = pp::app::plan_document_open(route.value().kind, args.unsaved);
|
||||
std::cout << "{\"ok\":true,\"command\":\"plan-open-route\""
|
||||
<< ",\"route\":{\"kind\":\"" << document_open_kind_name(route.value().kind)
|
||||
<< "\",\"path\":\"" << json_escape(route.value().path)
|
||||
<< "\",\"directory\":\"" << json_escape(route.value().directory)
|
||||
<< "\",\"name\":\"" << json_escape(route.value().name)
|
||||
<< "\",\"extension\":\"" << json_escape(route.value().extension)
|
||||
<< "\"},\"state\":{\"unsaved\":" << json_bool(args.unsaved)
|
||||
<< "},\"plan\":{\"action\":\"" << document_open_plan_action_name(action)
|
||||
<< "\"}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_plan_document_file_args(
|
||||
int argc,
|
||||
char** argv,
|
||||
@@ -3475,6 +3551,10 @@ int main(int argc, char** argv)
|
||||
return classify_open(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-open-route") {
|
||||
return plan_open_route(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "plan-document-file") {
|
||||
return plan_document_file(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user