Plan app dialog factories

This commit is contained in:
2026-06-05 07:36:56 +02:00
parent a79ef4cda8
commit 062fdaa982
9 changed files with 374 additions and 12 deletions

View File

@@ -240,6 +240,7 @@ target_link_libraries(pp_platform_api
add_library(pp_app_core STATIC
src/app_core/about_menu.h
src/app_core/app_dialog.h
src/app_core/app_frame.h
src/app_core/app_input.h
src/app_core/app_preferences.h

View File

@@ -832,6 +832,10 @@ Known local toolchain state:
timelapse start/stop/no-op decisions, VR mode success/failure dispatch,
simple stored preferences, and `AppPreferenceServices` execution dispatch for
options-menu side effects.
- `pp_app_core_app_dialog_tests` covers app-level progress/message/input dialog
metadata planning, progress initialization, negative progress-total clamping,
message cancel-button policy, input OK-caption propagation, and malformed
empty OK-caption rejection without requiring legacy `Node*` dialogs.
- `pp_app_core_app_startup_tests` covers startup run-counter increment
planning, optional auto-timelapse/license/VR-controller decisions, negative
and overflow run-counter rejection, stable full startup dispatch ordering,

File diff suppressed because one or more lines are too long

View File

@@ -198,6 +198,13 @@ and floating-point render targets; `App::title_update`,
`App::update_memory_usage`, `App::update_rec_frames`, resolution helpers,
`App::initLayout`, and `pano_cli plan-app-status` consume those contracts while
legacy UI nodes still render the strings and status lights.
App-level progress, message, and input dialog metadata now also lives in
`pp_app_core` through `plan_app_progress_dialog`,
`plan_app_message_dialog`, and `plan_app_input_dialog`; `App::show_progress`,
`App::message_box`, `App::input_box`, and `pano_cli plan-app-dialog` consume
those plans before retained `NodeProgressBar`, `NodeMessageBox`, and
`NodeInputBox` creation. Legacy dialog node lifetime/layout ownership remains
tracked under `DEBT-0058`.
Frame-level app decisions for the initial surface size, redraw/animation update
gating, layout ticking, resize render-target recreation, canvas-stroke drawing,
VR UI drawing, main UI drawing, UI observer clipping/on-screen transition/scissor
@@ -1691,6 +1698,15 @@ Results:
- Focused preference CTest coverage passed for
`pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests
after the live bridge split, including VR mode failed-start status coverage.
- `PanoPainter`, `pp_app_core_app_dialog_tests`, and `pano_cli` built after
progress/message/input dialog metadata moved into `pp_app_core` while live
`App` factories kept retained `Node*` creation.
- Focused app-dialog CTest coverage passed for
`pp_app_core_app_dialog_tests` and the `pano_cli_plan_app_dialog_*` smoke
tests, including negative progress-total clamping and rejected empty
input-dialog OK captions.
- Android arm64 headless `pp_app_core`, `pano_cli`, and
`pp_app_core_app_dialog_tests` built after the app-dialog planning slice.
- `PanoPainter`, `pp_app_core_app_startup_tests`, and `pano_cli` built after
startup preference/runtime execution and startup resource sequencing moved
behind app startup services.

78
src/app_core/app_dialog.h Normal file
View File

@@ -0,0 +1,78 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
enum class AppDialogKind {
progress,
message,
input,
};
struct AppProgressDialogPlan {
std::string title;
int total = 0;
int count = 0;
float progress_fraction = 0.0F;
};
struct AppMessageDialogPlan {
std::string title;
std::string message;
std::string ok_caption = "Ok";
bool show_cancel = false;
};
struct AppInputDialogPlan {
std::string title;
std::string field_name;
std::string ok_caption = "Ok";
};
[[nodiscard]] inline AppProgressDialogPlan plan_app_progress_dialog(
std::string_view title,
int total) noexcept
{
return {
std::string(title),
total < 0 ? 0 : total,
0,
0.0F,
};
}
[[nodiscard]] inline AppMessageDialogPlan plan_app_message_dialog(
std::string_view title,
std::string_view message,
bool show_cancel)
{
return {
std::string(title),
std::string(message),
"Ok",
show_cancel,
};
}
[[nodiscard]] inline pp::foundation::Result<AppInputDialogPlan> plan_app_input_dialog(
std::string_view title,
std::string_view field_name,
std::string_view ok_caption)
{
if (ok_caption.empty()) {
return pp::foundation::Result<AppInputDialogPlan>::failure(
pp::foundation::Status::invalid_argument("input dialog ok caption must not be empty"));
}
return pp::foundation::Result<AppInputDialogPlan>::success({
std::string(title),
std::string(field_name),
std::string(ok_caption),
});
}
} // namespace pp::app

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "app.h"
#include "app_core/app_dialog.h"
#include "app_core/document_layer.h"
#include "app_core/document_resize.h"
#include "app_core/document_export.h"
@@ -108,30 +109,32 @@ void start_document_export_collection(
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
{
const auto plan = pp::app::plan_app_progress_dialog(title, total);
auto pb = std::make_shared<NodeProgressBar>();
pb->set_manager(&layout);
pb->init();
pb->create();
pb->loaded();
pb->m_progress->SetWidthP(0);
pb->m_title->set_text(title.c_str());
pb->m_total = total;
pb->m_count = 0;
pb->m_progress->SetWidthP(plan.progress_fraction);
pb->m_title->set_text(plan.title.c_str());
pb->m_total = plan.total;
pb->m_count = plan.count;
layout[main_id]->add_child(pb);
return pb;
}
std::shared_ptr<NodeMessageBox> App::message_box(const std::string &title, const std::string& text, bool cancel_button)
{
const auto plan = pp::app::plan_app_message_dialog(title, text, cancel_button);
auto m = std::make_shared<NodeMessageBox>();
m->set_manager(&layout);
m->init();
m->create();
m->loaded();
m->m_title->set_text(title.c_str());
m->m_message->set_text(text.c_str());
m->btn_ok->m_text->set_text("Ok");
if (!cancel_button)
m->m_title->set_text(plan.title.c_str());
m->m_message->set_text(plan.message.c_str());
m->btn_ok->m_text->set_text(plan.ok_caption.c_str());
if (!plan.show_cancel)
m->btn_cancel->destroy();
layout[main_id]->add_child(m);
return m;
@@ -140,14 +143,20 @@ std::shared_ptr<NodeMessageBox> App::message_box(const std::string &title, const
std::shared_ptr<NodeInputBox> App::input_box(const std::string& title,
const std::string& field_name, const std::string& ok_caption /*= "Ok"*/)
{
const auto plan_result = pp::app::plan_app_input_dialog(title, field_name, ok_caption);
if (!plan_result) {
LOG("input dialog skipped: %s", plan_result.status().message);
return nullptr;
}
const auto& plan = plan_result.value();
auto m = std::make_shared<NodeInputBox>();
m->set_manager(&layout);
m->init();
m->create();
m->loaded();
m->m_title->set_text(title.c_str());
m->m_field_name->set_text(field_name.c_str());
m->btn_ok->m_text->set_text(ok_caption.c_str());
m->m_title->set_text(plan.title.c_str());
m->m_field_name->set_text(plan.field_name.c_str());
m->btn_ok->m_text->set_text(plan.ok_caption.c_str());
layout[main_id]->add_child(m);
return m;
}

View File

@@ -545,6 +545,16 @@ add_test(NAME pp_app_core_app_status_tests COMMAND pp_app_core_app_status_tests)
set_tests_properties(pp_app_core_app_status_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_app_dialog_tests
app_core/app_dialog_tests.cpp)
target_link_libraries(pp_app_core_app_dialog_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_app_dialog_tests COMMAND pp_app_core_app_dialog_tests)
set_tests_properties(pp_app_core_app_dialog_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_app_startup_tests
app_core/app_startup_tests.cpp)
target_link_libraries(pp_app_core_app_startup_tests PRIVATE
@@ -948,6 +958,31 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-transfer\".*\"action\":\"start-transfer\".*\"notify\":false.*\"fraction\":0")
add_test(NAME pano_cli_plan_app_dialog_progress_smoke
COMMAND pano_cli plan-app-dialog --kind progress --title Saving --total -4)
set_tests_properties(pano_cli_plan_app_dialog_progress_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-dialog\".*\"kind\":\"progress\".*\"title\":\"Saving\".*\"total\":0.*\"count\":0.*\"progressFraction\":0")
add_test(NAME pano_cli_plan_app_dialog_message_smoke
COMMAND pano_cli plan-app-dialog --kind message --title Import --message Brushes --cancel)
set_tests_properties(pano_cli_plan_app_dialog_message_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-dialog\".*\"kind\":\"message\".*\"title\":\"Import\".*\"message\":\"Brushes\".*\"okCaption\":\"Ok\".*\"showCancel\":true")
add_test(NAME pano_cli_plan_app_dialog_input_smoke
COMMAND pano_cli plan-app-dialog --kind input --title Rename --field-name Layer --ok-caption Save)
set_tests_properties(pano_cli_plan_app_dialog_input_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-dialog\".*\"kind\":\"input\".*\"title\":\"Rename\".*\"fieldName\":\"Layer\".*\"okCaption\":\"Save")
add_test(NAME pano_cli_plan_app_dialog_rejects_empty_ok_caption
COMMAND pano_cli plan-app-dialog --kind input --ok-caption "")
set_tests_properties(pano_cli_plan_app_dialog_rejects_empty_ok_caption PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
WILL_FAIL TRUE
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-dialog\".*\"message\":\"input dialog ok caption")
add_test(NAME pano_cli_plan_recording_session_stopped_smoke
COMMAND pano_cli plan-recording-session --frame-count 12)
set_tests_properties(pano_cli_plan_recording_session_stopped_smoke PROPERTIES

View File

@@ -0,0 +1,68 @@
#include "app_core/app_dialog.h"
#include "test_harness.h"
namespace {
void progress_dialog_initializes_progress_state(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_progress_dialog("Saving", 12);
PP_EXPECT(harness, plan.title == "Saving");
PP_EXPECT(harness, plan.total == 12);
PP_EXPECT(harness, plan.count == 0);
PP_EXPECT(harness, plan.progress_fraction == 0.0F);
}
void progress_dialog_clamps_negative_total(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_progress_dialog("Broken", -4);
PP_EXPECT(harness, plan.total == 0);
PP_EXPECT(harness, plan.count == 0);
PP_EXPECT(harness, plan.progress_fraction == 0.0F);
}
void message_dialog_preserves_cancel_policy(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_message_dialog("Import", "Import brushes?", true);
PP_EXPECT(harness, plan.title == "Import");
PP_EXPECT(harness, plan.message == "Import brushes?");
PP_EXPECT(harness, plan.ok_caption == "Ok");
PP_EXPECT(harness, plan.show_cancel);
}
void message_dialog_defaults_to_no_cancel(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_message_dialog("License", "Disabled", false);
PP_EXPECT(harness, plan.title == "License");
PP_EXPECT(harness, plan.message == "Disabled");
PP_EXPECT(harness, !plan.show_cancel);
}
void input_dialog_preserves_ok_caption(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_input_dialog("Rename", "Layer", "Save");
PP_EXPECT(harness, plan);
PP_EXPECT(harness, plan.value().title == "Rename");
PP_EXPECT(harness, plan.value().field_name == "Layer");
PP_EXPECT(harness, plan.value().ok_caption == "Save");
}
void input_dialog_rejects_empty_ok_caption(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_input_dialog("Rename", "Layer", "");
PP_EXPECT(harness, !plan);
PP_EXPECT(harness, plan.status().code == pp::foundation::StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("progress dialog initializes progress state", progress_dialog_initializes_progress_state);
harness.run("progress dialog clamps negative total", progress_dialog_clamps_negative_total);
harness.run("message dialog preserves cancel policy", message_dialog_preserves_cancel_policy);
harness.run("message dialog defaults to no cancel", message_dialog_defaults_to_no_cancel);
harness.run("input dialog preserves ok caption", input_dialog_preserves_ok_caption);
harness.run("input dialog rejects empty ok caption", input_dialog_rejects_empty_ok_caption);
return harness.finish();
}

View File

@@ -1,4 +1,5 @@
#include "app_core/about_menu.h"
#include "app_core/app_dialog.h"
#include "app_core/app_preferences.h"
#include "app_core/app_frame.h"
#include "app_core/app_input.h"
@@ -249,6 +250,16 @@ struct PlanAppPreferencesArgs {
int cursor_mode = 0;
};
struct PlanAppDialogArgs {
pp::app::AppDialogKind kind = pp::app::AppDialogKind::progress;
std::string title = "Saving";
std::string message = "Done";
std::string field_name = "Name";
std::string ok_caption = "Ok";
int total = 0;
bool cancel = false;
};
struct PlanAppStartupArgs {
int run_counter = 0;
bool auto_timelapse_enabled = true;
@@ -1931,6 +1942,20 @@ const char* cloud_transfer_action_name(pp::app::CloudTransferAction action) noex
return "reject-missing-source";
}
const char* app_dialog_kind_name(pp::app::AppDialogKind kind) noexcept
{
switch (kind) {
case pp::app::AppDialogKind::progress:
return "progress";
case pp::app::AppDialogKind::message:
return "message";
case pp::app::AppDialogKind::input:
return "input";
}
return "progress";
}
const char* recording_start_action_name(pp::app::RecordingStartAction action) noexcept
{
switch (action) {
@@ -2147,6 +2172,7 @@ void print_help()
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n"
<< " plan-cloud-transfer [--direction download|upload] [--source TEXT] [--destination FILE] [--progress] [--disable-tls-verification] [--progress-total N] [--progress-current N]\n"
<< " plan-app-dialog --kind progress|message|input [--title TEXT] [--message TEXT] [--field-name TEXT] [--ok-caption TEXT] [--total N] [--cancel]\n"
<< " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files] [--no-encoder] [--no-canvas]\n"
<< " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n"
<< " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n"
@@ -3675,6 +3701,121 @@ int plan_cloud_transfer(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_plan_app_dialog_args(
int argc,
char** argv,
PlanAppDialogArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--kind") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const std::string_view value(argv[++i]);
if (value == "progress") {
args.kind = pp::app::AppDialogKind::progress;
} else if (value == "message") {
args.kind = pp::app::AppDialogKind::message;
} else if (value == "input") {
args.kind = pp::app::AppDialogKind::input;
} else {
return pp::foundation::Status::invalid_argument("unknown app dialog kind");
}
} else if (key == "--title") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.title = argv[++i];
} else if (key == "--message") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.message = argv[++i];
} else if (key == "--field-name") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.field_name = argv[++i];
} else if (key == "--ok-caption") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.ok_caption = argv[++i];
} else if (key == "--total") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i32_arg(argv[++i]);
if (!value) {
return value.status();
}
args.total = value.value();
} else if (key == "--cancel") {
args.cancel = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_app_dialog(int argc, char** argv)
{
PlanAppDialogArgs args;
const auto status = parse_plan_app_dialog_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-app-dialog", status.message);
return 2;
}
switch (args.kind) {
case pp::app::AppDialogKind::progress:
{
const auto plan = pp::app::plan_app_progress_dialog(args.title, args.total);
std::cout << "{\"ok\":true,\"command\":\"plan-app-dialog\""
<< ",\"kind\":\"" << app_dialog_kind_name(args.kind)
<< "\",\"plan\":{\"title\":\"" << json_escape(plan.title)
<< "\",\"total\":" << plan.total
<< ",\"count\":" << plan.count
<< ",\"progressFraction\":" << plan.progress_fraction
<< "}}\n";
return 0;
}
case pp::app::AppDialogKind::message:
{
const auto plan = pp::app::plan_app_message_dialog(args.title, args.message, args.cancel);
std::cout << "{\"ok\":true,\"command\":\"plan-app-dialog\""
<< ",\"kind\":\"" << app_dialog_kind_name(args.kind)
<< "\",\"plan\":{\"title\":\"" << json_escape(plan.title)
<< "\",\"message\":\"" << json_escape(plan.message)
<< "\",\"okCaption\":\"" << json_escape(plan.ok_caption)
<< "\",\"showCancel\":" << json_bool(plan.show_cancel)
<< "}}\n";
return 0;
}
case pp::app::AppDialogKind::input:
{
const auto plan = pp::app::plan_app_input_dialog(args.title, args.field_name, args.ok_caption);
if (!plan) {
print_error("plan-app-dialog", plan.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"plan-app-dialog\""
<< ",\"kind\":\"" << app_dialog_kind_name(args.kind)
<< "\",\"plan\":{\"title\":\"" << json_escape(plan.value().title)
<< "\",\"fieldName\":\"" << json_escape(plan.value().field_name)
<< "\",\"okCaption\":\"" << json_escape(plan.value().ok_caption)
<< "}}\n";
return 0;
}
}
print_error("plan-app-dialog", "unknown app dialog kind");
return 2;
}
pp::foundation::Status parse_plan_recording_session_args(
int argc,
char** argv,
@@ -10879,6 +11020,10 @@ int main(int argc, char** argv)
return plan_cloud_transfer(argc, argv);
}
if (command == "plan-app-dialog") {
return plan_app_dialog(argc, argv);
}
if (command == "plan-recording-session") {
return plan_recording_session(argc, argv);
}