From 4d06608cc9a2b3492e7f34a47866f35c9e7898d6 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 09:45:12 +0200 Subject: [PATCH] Extract app status planning into app core --- CMakeLists.txt | 1 + docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 5 ++ src/app.cpp | 30 ++++--- src/app_core/app_status.h | 119 ++++++++++++++++++++++++++++ src/app_layout.cpp | 16 +++- tests/CMakeLists.txt | 31 ++++++++ tests/app_core/app_status_tests.cpp | 86 ++++++++++++++++++++ tools/pano_cli/main.cpp | 116 +++++++++++++++++++++++++++ 9 files changed, 385 insertions(+), 21 deletions(-) create mode 100644 src/app_core/app_status.h create mode 100644 tests/app_core/app_status_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a7e4c03..e505ef2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,6 +224,7 @@ target_link_libraries(pp_platform_api add_library(pp_app_core STATIC src/app_core/app_preferences.h + src/app_core/app_status.h src/app_core/document_cloud.h src/app_core/document_export.cpp src/app_core/document_platform_io.h diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 0586c49..337e347 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/share/platform-I/O/display/keyboard/cloud contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform 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_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_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-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `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-start --requires-license --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `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`; `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`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/share/platform-I/O/display/keyboard/cloud contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, status/display UI rendering, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform 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_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_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-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `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-start --requires-license --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `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`; `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 | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c536be0..e1e7039 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -180,6 +180,11 @@ scale option selection, viewport scale, RTL layout direction, timelapse recording toggles, VR controller enablement, and canvas cursor mode; the live tools/options menu and `pano_cli plan-app-preferences` consume those contracts while legacy widgets and settings persistence execute them. +It also owns tested app status/display plans for document title text, +resolution mapping/labels, DPI text, history-memory text, and recording-frame +status text; `App::title_update`, `App::update_memory_usage`, +`App::update_rec_frames`, resolution helpers, and `pano_cli plan-app-status` +consume those contracts while legacy UI nodes still render the strings. `panopainter_app` is now a real static target that owns app orchestration sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, diff --git a/src/app.cpp b/src/app.cpp index a6c23c0..45ba764 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5,6 +5,7 @@ #include "node_dialog_open.h" #include "node_progress_bar.h" #include "mp4enc.h" +#include "app_core/app_status.h" #include "app_core/document_recording.h" #include "app_core/document_route.h" #include "app_core/document_session.h" @@ -682,9 +683,8 @@ void App::update_memory_usage(size_t bytes) { if (auto txt = layout[main_id]->find("txt-memory")) { - static char buffer[128]; - sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f); - txt->set_text(buffer); + const auto label = pp::app::make_history_memory_label(bytes); + txt->set_text(label.c_str()); } } @@ -692,32 +692,30 @@ void App::update_rec_frames() { if (auto txt = layout[main_id]->find("txt-rec")) { - if (rec_running && Canvas::I->m_encoder) - { - static char buffer[128]; - sprintf(buffer, "Recorded %d frames", Canvas::I->m_encoder->frames_count()); - txt->set_text(buffer); - } - else - { - txt->set_text(""); - } + const auto label = pp::app::make_recording_frame_label( + rec_running, + Canvas::I->m_encoder != nullptr, + Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0); + txt->set_text(label.text.c_str()); } } int App::res_from_index(int i) { - return res_map[i]; + const auto resolution = pp::app::display_resolution_from_index(i); + return resolution ? resolution.value() : pp::app::document_resolution_values.front(); } int App::res_to_index(int res) { - return (int)std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res)); + const auto index = pp::app::document_resolution_to_index(res); + return index ? static_cast(index.value()) : static_cast(pp::app::document_resolution_values.size()); } std::string App::res_to_string(int res) { - return res_map_str[res_to_index(res)]; + const auto label = pp::app::document_resolution_label(res); + return label ? std::string(label.value()) : std::string("unknown"); } void App::renderdoc_frame_start() diff --git a/src/app_core/app_status.h b/src/app_core/app_status.h new file mode 100644 index 0000000..3b57338 --- /dev/null +++ b/src/app_core/app_status.h @@ -0,0 +1,119 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include +#include + +namespace pp::app { + +inline constexpr std::array document_resolution_values { + 512, + 1024, + 1536, + 2048, + 4096, + 8192, +}; + +inline constexpr std::array document_resolution_labels { + "2K", + "4K", + "6K", + "8K", + "16K", + "32K", +}; + +struct RecordingFrameLabel { + bool visible = false; + std::string text; +}; + +[[nodiscard]] inline pp::foundation::Result display_resolution_from_index(int index) +{ + if (index < 0 || static_cast(index) >= document_resolution_values.size()) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document resolution index is out of range")); + } + return pp::foundation::Result::success( + document_resolution_values[static_cast(index)]); +} + +[[nodiscard]] inline pp::foundation::Result document_resolution_to_index(int resolution) +{ + for (std::size_t index = 0; index < document_resolution_values.size(); ++index) { + if (document_resolution_values[index] == resolution) { + return pp::foundation::Result::success(index); + } + } + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document resolution is not supported")); +} + +[[nodiscard]] inline pp::foundation::Result document_resolution_label(int resolution) +{ + const auto index = document_resolution_to_index(resolution); + if (!index) { + return pp::foundation::Result::failure(index.status()); + } + return pp::foundation::Result::success(document_resolution_labels[index.value()]); +} + +[[nodiscard]] inline std::string make_document_title( + std::string_view document_name, + bool has_unsaved_changes, + int resolution) +{ + const auto label = document_resolution_label(resolution); + const auto resolution_label = label ? label.value() : std::string_view("unknown"); + std::string title = "Panodoc: "; + title.append(document_name); + if (has_unsaved_changes) { + title.push_back('*'); + } + title.append(" ("); + title.append(resolution_label); + title.push_back(')'); + return title; +} + +[[nodiscard]] inline std::string make_dpi_label(float zoom) +{ + char buffer[64] {}; + std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom); + return buffer; +} + +[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes) +{ + char buffer[128] {}; + std::snprintf( + buffer, + sizeof(buffer), + "History memory: %.2f Mb", + static_cast(bytes) / 1024.0 / 1024.0); + return buffer; +} + +[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label( + bool is_recording, + bool encoder_available, + int encoded_frames) +{ + if (!is_recording || !encoder_available) { + return {}; + } + + char buffer[128] {}; + std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames); + return { + true, + buffer, + }; +} + +} diff --git a/src/app_layout.cpp b/src/app_layout.cpp index 93f1cd0..7292de1 100644 --- a/src/app_layout.cpp +++ b/src/app_layout.cpp @@ -7,6 +7,7 @@ #include "node_dialog_picker.h" #include "node_panel_floating.h" #include "app_core/app_preferences.h" +#include "app_core/app_status.h" #include "settings.h" #include "serializer.h" #include "font.h" @@ -17,12 +18,19 @@ void App::title_update() { - static char str[256]; - snprintf(str, 256, "Panodoc: %s%s (%s)", doc_name.c_str(), canvas->m_canvas->m_unsaved ? "*" : "", res_to_string(canvas->m_canvas->m_width).c_str()); if (auto docname = layout[main_id]->find("txt-docname")) - docname->set_text(str); + { + const auto title = pp::app::make_document_title( + doc_name, + canvas->m_canvas->m_unsaved, + canvas->m_canvas->m_width); + docname->set_text(title.c_str()); + } if (auto node = layout[main_id]->find("txt-dpi")) - node->set_text(fmt::format("{:.1f}x-dpi", zoom).c_str()); + { + const auto label = pp::app::make_dpi_label(zoom); + node->set_text(label.c_str()); + } } void App::init_toolbar_main() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a1f336f..a07c9b5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -328,6 +328,16 @@ add_test(NAME pp_app_core_app_preferences_tests COMMAND pp_app_core_app_preferen set_tests_properties(pp_app_core_app_preferences_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_status_tests + app_core/app_status_tests.cpp) +target_link_libraries(pp_app_core_app_status_tests PRIVATE + pp_app_core + pp_test_harness) + +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_document_sharing_tests app_core/document_sharing_tests.cpp) target_link_libraries(pp_app_core_document_sharing_tests PRIVATE @@ -637,6 +647,27 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-preferences\".*\"scaleSelection\":\\{\"hasSelection\":false,\"index\":0\\}.*\"direction\":\"left-to-right\".*\"timelapse\":\\{\"enabled\":false,\"recordingAction\":\"stop-recording\"\\}.*\"vrControllers\":\\{\"enabled\":false\\}") + add_test(NAME pano_cli_plan_app_status_smoke + COMMAND pano_cli plan-app-status + --doc-name demo + --unsaved + --resolution 2048 + --resolution-index 3 + --zoom 1.26 + --history-bytes 1572864 + --recording-running + --encoder-available + --encoded-frames 12) + set_tests_properties(pano_cli_plan_app_status_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-status\".*\"title\":\"Panodoc: demo\\* \\(8K\\)\".*\"dpi\":\"1.3x-dpi\".*\"memory\":\"History memory: 1.50 Mb\".*\"recording\":\\{\"visible\":true,\"text\":\"Recorded 12 frames\"\\}.*\"fromIndexValid\":true.*\"fromIndex\":2048.*\"toIndexValid\":true.*\"toIndex\":3.*\"labelValid\":true.*\"label\":\"8K\"") + + add_test(NAME pano_cli_plan_app_status_unknown_resolution_smoke + COMMAND pano_cli plan-app-status --doc-name demo --resolution 1234 --resolution-index 9 --recording-running) + set_tests_properties(pano_cli_plan_app_status_unknown_resolution_smoke PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-status\".*\"title\":\"Panodoc: demo \\(unknown\\)\".*\"recording\":\\{\"visible\":false,\"text\":\"\"\\}.*\"fromIndexValid\":false.*\"toIndexValid\":false.*\"labelValid\":false.*\"label\":\"\"") + add_test(NAME pano_cli_plan_share_file_unsaved_smoke COMMAND pano_cli plan-share-file) set_tests_properties(pano_cli_plan_share_file_unsaved_smoke PROPERTIES diff --git a/tests/app_core/app_status_tests.cpp b/tests/app_core/app_status_tests.cpp new file mode 100644 index 0000000..4affe0f --- /dev/null +++ b/tests/app_core/app_status_tests.cpp @@ -0,0 +1,86 @@ +#include "app_core/app_status.h" +#include "test_harness.h" + +namespace { + +void resolution_maps_supported_indices_and_labels(pp::tests::Harness& harness) +{ + const auto resolution = pp::app::display_resolution_from_index(3); + PP_EXPECT(harness, resolution); + if (resolution) { + PP_EXPECT(harness, resolution.value() == 2048); + } + + const auto index = pp::app::document_resolution_to_index(4096); + PP_EXPECT(harness, index); + if (index) { + PP_EXPECT(harness, index.value() == 4U); + } + + const auto label = pp::app::document_resolution_label(8192); + PP_EXPECT(harness, label); + if (label) { + PP_EXPECT(harness, label.value() == "32K"); + } +} + +void resolution_mapping_rejects_out_of_range_values(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::display_resolution_from_index(-1)); + PP_EXPECT(harness, !pp::app::display_resolution_from_index(6)); + PP_EXPECT(harness, !pp::app::document_resolution_to_index(1234)); + PP_EXPECT(harness, !pp::app::document_resolution_label(1234)); +} + +void document_title_marks_unsaved_documents(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::make_document_title("demo", false, 2048) == "Panodoc: demo (8K)"); + PP_EXPECT( + harness, + pp::app::make_document_title("demo", true, 2048) == "Panodoc: demo* (8K)"); +} + +void document_title_survives_unknown_resolution(pp::tests::Harness& harness) +{ + PP_EXPECT( + harness, + pp::app::make_document_title("demo", true, 1234) == "Panodoc: demo* (unknown)"); +} + +void status_labels_match_legacy_text(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, pp::app::make_dpi_label(1.25F) == "1.2x-dpi"); + PP_EXPECT(harness, pp::app::make_dpi_label(1.26F) == "1.3x-dpi"); + PP_EXPECT(harness, pp::app::make_history_memory_label(1024U * 1024U * 3U / 2U) == "History memory: 1.50 Mb"); +} + +void recording_label_only_shows_when_recording_with_encoder(pp::tests::Harness& harness) +{ + const auto inactive = pp::app::make_recording_frame_label(false, true, 12); + PP_EXPECT(harness, !inactive.visible); + PP_EXPECT(harness, inactive.text.empty()); + + const auto missing_encoder = pp::app::make_recording_frame_label(true, false, 12); + PP_EXPECT(harness, !missing_encoder.visible); + PP_EXPECT(harness, missing_encoder.text.empty()); + + const auto active = pp::app::make_recording_frame_label(true, true, 12); + PP_EXPECT(harness, active.visible); + PP_EXPECT(harness, active.text == "Recorded 12 frames"); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("resolution maps supported indices and labels", resolution_maps_supported_indices_and_labels); + harness.run("resolution mapping rejects out of range values", resolution_mapping_rejects_out_of_range_values); + harness.run("document title marks unsaved documents", document_title_marks_unsaved_documents); + harness.run("document title survives unknown resolution", document_title_survives_unknown_resolution); + harness.run("status labels match legacy text", status_labels_match_legacy_text); + harness.run("recording label only shows when recording with encoder", recording_label_only_shows_when_recording_with_encoder); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 902095c..7e3d214 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,4 +1,5 @@ #include "app_core/app_preferences.h" +#include "app_core/app_status.h" #include "app_core/document_export.h" #include "app_core/document_cloud.h" #include "app_core/document_platform_io.h" @@ -200,6 +201,18 @@ struct PlanAppPreferencesArgs { int cursor_mode = 0; }; +struct PlanAppStatusArgs { + std::string document_name = "no-name"; + bool unsaved = false; + int resolution = 512; + int resolution_index = 0; + float zoom = 1.0F; + std::uint32_t history_bytes = 0; + bool recording_running = false; + bool encoder_available = false; + std::uint32_t encoded_frames = 0; +}; + struct SimulateAppSessionArgs { bool has_canvas = true; bool new_document = false; @@ -668,6 +681,7 @@ void print_help() << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\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-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N]\n" << " plan-share-file [--path FILE]\n" << " plan-picked-path [--path FILE]\n" << " plan-display-file [--path FILE]\n" @@ -2107,6 +2121,104 @@ int plan_app_preferences(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_status_args( + int argc, + char** argv, + PlanAppStatusArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--doc-name") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.document_name = argv[++i]; + } else if (key == "--unsaved") { + args.unsaved = true; + } else if (key == "--resolution" || key == "--resolution-index" || key == "--history-bytes" + || key == "--encoded-frames") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--resolution") { + args.resolution = static_cast(value.value()); + } else if (key == "--resolution-index") { + args.resolution_index = static_cast(value.value()); + } else if (key == "--history-bytes") { + args.history_bytes = value.value(); + } else { + args.encoded_frames = value.value(); + } + } else if (key == "--zoom") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + args.zoom = value.value(); + } else if (key == "--recording-running") { + args.recording_running = true; + } else if (key == "--encoder-available") { + args.encoder_available = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_status(int argc, char** argv) +{ + PlanAppStatusArgs args; + const auto status = parse_plan_app_status_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-status", status.message); + return 2; + } + + const auto resolution_from_index = pp::app::display_resolution_from_index(args.resolution_index); + const auto resolution_index = pp::app::document_resolution_to_index(args.resolution); + const auto resolution_label = pp::app::document_resolution_label(args.resolution); + const auto recording_label = pp::app::make_recording_frame_label( + args.recording_running, + args.encoder_available, + static_cast(args.encoded_frames)); + + std::cout << "{\"ok\":true,\"command\":\"plan-app-status\"" + << ",\"state\":{\"documentName\":\"" << json_escape(args.document_name) + << "\",\"unsaved\":" << json_bool(args.unsaved) + << ",\"resolution\":" << args.resolution + << ",\"resolutionIndex\":" << args.resolution_index + << ",\"zoom\":" << args.zoom + << ",\"historyBytes\":" << args.history_bytes + << ",\"recordingRunning\":" << json_bool(args.recording_running) + << ",\"encoderAvailable\":" << json_bool(args.encoder_available) + << ",\"encodedFrames\":" << args.encoded_frames + << "},\"title\":\"" << json_escape(pp::app::make_document_title( + args.document_name, + args.unsaved, + args.resolution)) + << "\",\"dpi\":\"" << json_escape(pp::app::make_dpi_label(args.zoom)) + << "\",\"memory\":\"" << json_escape(pp::app::make_history_memory_label(args.history_bytes)) + << "\",\"recording\":{\"visible\":" << json_bool(recording_label.visible) + << ",\"text\":\"" << json_escape(recording_label.text) + << "\"},\"resolutionMap\":{\"fromIndexValid\":" << json_bool(static_cast(resolution_from_index)) + << ",\"fromIndex\":" << (resolution_from_index ? resolution_from_index.value() : 0) + << ",\"toIndexValid\":" << json_bool(static_cast(resolution_index)) + << ",\"toIndex\":" << (resolution_index ? resolution_index.value() : 0) + << ",\"labelValid\":" << json_bool(static_cast(resolution_label)) + << ",\"label\":\"" << json_escape(resolution_label ? std::string(resolution_label.value()) : std::string()) + << "\"}}\n"; + return 0; +} + pp::foundation::Status parse_plan_share_file_args( int argc, char** argv, @@ -4503,6 +4615,10 @@ int main(int argc, char** argv) return plan_app_preferences(argc, argv); } + if (command == "plan-app-status") { + return plan_app_status(argc, argv); + } + if (command == "plan-share-file") { return plan_share_file(argc, argv); }