Extract history UI operation planning

This commit is contained in:
2026-06-03 11:13:57 +02:00
parent 8dc476d205
commit 58afa672c7
9 changed files with 392 additions and 7 deletions

View File

@@ -237,6 +237,7 @@ add_library(pp_app_core STATIC
src/app_core/document_sharing.h src/app_core/document_sharing.h
src/app_core/document_session.cpp src/app_core/document_session.cpp
src/app_core/grid_ui.h src/app_core/grid_ui.h
src/app_core/history_ui.h
src/app_core/quick_ui.h) src/app_core/quick_ui.h)
target_include_directories(pp_app_core target_include_directories(pp_app_core
PUBLIC PUBLIC

View File

@@ -43,6 +43,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely | | DEBT-0023 | Open | Modernization | Brush/color/preset UI planning now consumes pure `pp_app_core` through `App::init_sidebar`, restored/docked floating-panel callbacks, and `pano_cli plan-brush-operation`, but live execution still mutates legacy `Brush`, calls legacy brush texture loading, and refreshes legacy quick/stroke/color widgets directly | Preserve existing brush UI behavior while brush commands move toward a brush/app command boundary and asset-managed texture selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset execution is owned by a brush/app command boundary with legacy `Brush`/UI nodes acting only as adapters or removed entirely |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter | | DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning now consumes pure `pp_app_core` through `NodePanelGrid` and `pano_cli plan-grid-operation`, but live execution still performs legacy image loading, OpenGL texture updates, nanort lightmap baking, progress UI, and `Canvas::draw_objects` commit directly | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |
| DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by app/brush/UI services with `NodePanelQuick` acting only as UI adapter | | DEBT-0025 | Open | Modernization | Quick brush/color slot and mini-state planning now consumes pure `pp_app_core` through `NodePanelQuick` and `pano_cli plan-quick-operation`, but live execution still mutates legacy quick UI widgets, `Brush` previews, color picker popup state, and preset popup state directly | Preserve quick-panel behavior while quick brush/color commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, and color execution are owned by app/brush/UI services with `NodePanelQuick` acting only as UI adapter |
| DEBT-0026 | Open | Modernization | Toolbar and canvas history command planning now consumes pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, and `pano_cli plan-history-operation`, but live execution still mutates legacy `ActionManager` stacks and `Canvas::I` unsaved state directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by document/app history services with toolbar and canvas input acting only as adapters |
## Closed Debt ## Closed Debt

View File

@@ -503,6 +503,9 @@ callbacks before legacy `Brush` mutation and resource loading continue.
pick/load/reload/clear, lightmap render capability/limit checks, and heightmap pick/load/reload/clear, lightmap render capability/limit checks, and heightmap
commit used by the live grid panel before legacy image loading, OpenGL texture commit used by the live grid panel before legacy image loading, OpenGL texture
updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue. updates, nanort lightmap baking, and `Canvas::draw_objects` execution continue.
`pano_cli plan-history-operation` exposes app-core planning for undo, redo, and
clear-history availability used by toolbar buttons and canvas shortcuts before
legacy `ActionManager` stack execution continues.
`pano_cli plan-quick-operation` exposes app-core planning for quick brush/color `pano_cli plan-quick-operation` exposes app-core planning for quick brush/color
slot selection versus popup opening, plus quick mini-state restore/reset slot selection versus popup opening, plus quick mini-state restore/reset
validation used by the live quick panel before legacy `Brush`, color picker, validation used by the live quick panel before legacy `Brush`, color picker,
@@ -1155,6 +1158,14 @@ Results:
`pano_cli_plan_grid_operation_rejects_empty_reload`, and `pano_cli_plan_grid_operation_rejects_empty_reload`, and
`pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live `pano_cli_plan_grid_operation_rejects_bad_samples` passed and expose live
grid/heightmap/lightmap planning as JSON automation. grid/heightmap/lightmap planning as JSON automation.
- `pp_app_core_history_ui_tests` passed, covering undo/redo availability,
no-op history commands, clear-history stack/memory state, memory-only clear,
and negative metric rejection.
- `pano_cli_plan_history_operation_undo_smoke`,
`pano_cli_plan_history_operation_redo_empty_smoke`,
`pano_cli_plan_history_operation_clear_smoke`, and
`pano_cli_plan_history_operation_rejects_negative_count` passed and expose
toolbar/canvas history planning as JSON automation.
- `pp_app_core_quick_ui_tests` passed, covering quick brush/color slot - `pp_app_core_quick_ui_tests` passed, covering quick brush/color slot
selection, active-slot popup decisions, invalid slot rejection, restore-state selection, active-slot popup decisions, invalid slot rejection, restore-state
validation, and reset-state validation. validation, and reset-state validation.

110
src/app_core/history_ui.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
#include "foundation/result.h"
namespace pp::app {
enum class HistoryUiOperation {
undo,
redo,
clear,
};
struct HistoryUiPlan {
HistoryUiOperation operation = HistoryUiOperation::undo;
int undo_count = 0;
int redo_count = 0;
int memory_bytes = 0;
bool invokes_undo = false;
bool invokes_redo = false;
bool clears_history = false;
bool updates_memory_label = false;
bool updates_title = false;
bool no_op = false;
};
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
{
if (value < 0) {
return pp::foundation::Status::out_of_range(message);
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
{
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::undo;
plan.undo_count = undo_count;
if (undo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_undo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
{
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::redo;
plan.redo_count = redo_count;
if (redo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_redo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
int undo_count,
int redo_count,
int memory_bytes)
{
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
}
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
}
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::clear;
plan.undo_count = undo_count;
plan.redo_count = redo_count;
plan.memory_bytes = memory_bytes;
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.clears_history = true;
plan.updates_memory_label = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
} // namespace pp::app

View File

@@ -10,6 +10,7 @@
#include "app_core/brush_ui.h" #include "app_core/brush_ui.h"
#include "app_core/document_layer.h" #include "app_core/document_layer.h"
#include "app_core/app_status.h" #include "app_core/app_status.h"
#include "app_core/history_ui.h"
#include "settings.h" #include "settings.h"
#include "serializer.h" #include "serializer.h"
#include "font.h" #include "font.h"
@@ -119,19 +120,28 @@ void App::init_toolbar_main()
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-undo")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-undo"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
ActionManager::undo(); const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
if (plan && plan.value().invokes_undo)
ActionManager::undo();
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-redo")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-redo"))
{ {
button->on_click = [this, button](Node*) { button->on_click = [this, button](Node*) {
ActionManager::redo(); const auto plan = pp::app::plan_history_redo(static_cast<int>(ActionManager::I.m_redos.size()));
if (plan && plan.value().invokes_redo)
ActionManager::redo();
}; };
} }
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-clean-memory")) if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-clean-memory"))
{ {
button->on_click = [this](Node*) { button->on_click = [this](Node*) {
ActionManager::clear(); const auto plan = pp::app::plan_history_clear(
static_cast<int>(ActionManager::I.m_actions.size()),
static_cast<int>(ActionManager::I.m_redos.size()),
static_cast<int>(ActionManager::I.m_memory));
if (plan && plan.value().clears_history)
ActionManager::clear();
}; };
} }
if (auto* button = layout[main_id]->find<NodeButton>("btn-clear")) if (auto* button = layout[main_id]->find<NodeButton>("btn-clear"))

View File

@@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include "app_core/history_ui.h"
#include "app.h" #include "app.h"
#include "log.h" #include "log.h"
#include "node_canvas.h" #include "node_canvas.h"
@@ -21,6 +22,20 @@ void unbind_texture_2d()
glBindTexture(pp::renderer::gl::texture_2d_target(), 0); glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
} }
void run_history_undo_if_available()
{
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
if (plan && plan.value().invokes_undo)
ActionManager::undo();
}
void run_history_redo_if_available()
{
const auto plan = pp::app::plan_history_redo(static_cast<int>(ActionManager::I.m_redos.size()));
if (plan && plan.value().invokes_redo)
ActionManager::redo();
}
} }
Node* NodeCanvas::clone_instantiate() const Node* NodeCanvas::clone_instantiate() const
@@ -600,8 +615,7 @@ kEventResult NodeCanvas::handle_event(Event* e)
if (ke->m_key == kKey::KeyE) if (ke->m_key == kKey::KeyE)
Canvas::set_mode(kCanvasMode::Erase); Canvas::set_mode(kCanvasMode::Erase);
if (ke->m_key == kKey::AndroidBack) if (ke->m_key == kKey::AndroidBack)
if (!ActionManager::empty()) run_history_undo_if_available();
ActionManager::undo();
if (ke->m_key == kKey::KeyAlt && m_mouse_focus) if (ke->m_key == kKey::KeyAlt && m_mouse_focus)
App::I->show_cursor(); App::I->show_cursor();
for (auto& mode : *m_canvas->m_mode) for (auto& mode : *m_canvas->m_mode)
@@ -614,7 +628,7 @@ kEventResult NodeCanvas::handle_event(Event* e)
if (ke->m_key == kKey::KeyTab) if (ke->m_key == kKey::KeyTab)
App::I->toggle_ui(); App::I->toggle_ui();
if (ke->m_key == kKey::KeyZ && App::I->keys[(int)kKey::KeyCtrl]) if (ke->m_key == kKey::KeyZ && App::I->keys[(int)kKey::KeyCtrl])
App::I->keys[(int)kKey::KeyShift] ? ActionManager::redo() : ActionManager::undo(); App::I->keys[(int)kKey::KeyShift] ? run_history_redo_if_available() : run_history_undo_if_available();
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift]) if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift])
{ {
App::I->save_document(pp::app::DocumentSaveIntent::save); App::I->save_document(pp::app::DocumentSaveIntent::save);
@@ -654,7 +668,7 @@ kEventResult NodeCanvas::handle_event(Event* e)
break; break;
case kEventType::TouchTap: case kEventType::TouchTap:
if (te->m_finger_count == 2) if (te->m_finger_count == 2)
ActionManager::undo(); run_history_undo_if_available();
break; break;
default: default:
return kEventResult::Available; return kEventResult::Available;

View File

@@ -288,6 +288,16 @@ add_test(NAME pp_app_core_grid_ui_tests COMMAND pp_app_core_grid_ui_tests)
set_tests_properties(pp_app_core_grid_ui_tests PROPERTIES set_tests_properties(pp_app_core_grid_ui_tests PROPERTIES
LABELS "app;ui;renderer;desktop-fast;fuzz") LABELS "app;ui;renderer;desktop-fast;fuzz")
add_executable(pp_app_core_history_ui_tests
app_core/history_ui_tests.cpp)
target_link_libraries(pp_app_core_history_ui_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_history_ui_tests COMMAND pp_app_core_history_ui_tests)
set_tests_properties(pp_app_core_history_ui_tests PROPERTIES
LABELS "app;document;ui;desktop-fast;fuzz")
add_executable(pp_app_core_quick_ui_tests add_executable(pp_app_core_quick_ui_tests
app_core/quick_ui_tests.cpp) app_core/quick_ui_tests.cpp)
target_link_libraries(pp_app_core_quick_ui_tests PRIVATE target_link_libraries(pp_app_core_quick_ui_tests PRIVATE
@@ -872,6 +882,30 @@ if(TARGET pano_cli)
LABELS "app;ui;renderer;integration;desktop-fast;fuzz" LABELS "app;ui;renderer;integration;desktop-fast;fuzz"
WILL_FAIL TRUE) WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_history_operation_undo_smoke
COMMAND pano_cli plan-history-operation --kind undo --undo-count 2)
set_tests_properties(pano_cli_plan_history_operation_undo_smoke PROPERTIES
LABELS "app;document;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-history-operation\".*\"operation\":\"undo\".*\"undoCount\":2.*\"invokesUndo\":true.*\"updatesMemoryLabel\":true.*\"updatesTitle\":true")
add_test(NAME pano_cli_plan_history_operation_redo_empty_smoke
COMMAND pano_cli plan-history-operation --kind redo)
set_tests_properties(pano_cli_plan_history_operation_redo_empty_smoke PROPERTIES
LABELS "app;document;ui;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-history-operation\".*\"operation\":\"redo\".*\"redoCount\":0.*\"invokesRedo\":false.*\"noOp\":true")
add_test(NAME pano_cli_plan_history_operation_clear_smoke
COMMAND pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096)
set_tests_properties(pano_cli_plan_history_operation_clear_smoke PROPERTIES
LABELS "app;document;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-history-operation\".*\"operation\":\"clear\".*\"undoCount\":2.*\"redoCount\":1.*\"memoryBytes\":4096.*\"clearsHistory\":true.*\"updatesMemoryLabel\":true")
add_test(NAME pano_cli_plan_history_operation_rejects_negative_count
COMMAND pano_cli plan-history-operation --kind undo --undo-count -1)
set_tests_properties(pano_cli_plan_history_operation_rejects_negative_count PROPERTIES
LABELS "app;document;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_quick_operation_select_brush_smoke add_test(NAME pano_cli_plan_quick_operation_select_brush_smoke
COMMAND pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2) COMMAND pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2)
set_tests_properties(pano_cli_plan_quick_operation_select_brush_smoke PROPERTIES set_tests_properties(pano_cli_plan_quick_operation_select_brush_smoke PROPERTIES

View File

@@ -0,0 +1,90 @@
#include "app_core/history_ui.h"
#include "test_harness.h"
namespace {
void undo_and_redo_plan_availability(pp::tests::Harness& harness)
{
const auto undo = pp::app::plan_history_undo(2);
PP_EXPECT(harness, undo);
if (undo) {
PP_EXPECT(harness, undo.value().operation == pp::app::HistoryUiOperation::undo);
PP_EXPECT(harness, undo.value().undo_count == 2);
PP_EXPECT(harness, undo.value().invokes_undo);
PP_EXPECT(harness, undo.value().updates_memory_label);
PP_EXPECT(harness, undo.value().updates_title);
PP_EXPECT(harness, !undo.value().no_op);
}
const auto undo_empty = pp::app::plan_history_undo(0);
PP_EXPECT(harness, undo_empty);
if (undo_empty) {
PP_EXPECT(harness, undo_empty.value().no_op);
PP_EXPECT(harness, !undo_empty.value().invokes_undo);
}
const auto redo = pp::app::plan_history_redo(1);
PP_EXPECT(harness, redo);
if (redo) {
PP_EXPECT(harness, redo.value().operation == pp::app::HistoryUiOperation::redo);
PP_EXPECT(harness, redo.value().redo_count == 1);
PP_EXPECT(harness, redo.value().invokes_redo);
PP_EXPECT(harness, redo.value().updates_title);
}
const auto redo_empty = pp::app::plan_history_redo(0);
PP_EXPECT(harness, redo_empty);
if (redo_empty) {
PP_EXPECT(harness, redo_empty.value().no_op);
PP_EXPECT(harness, !redo_empty.value().invokes_redo);
}
}
void clear_plan_tracks_stacks_and_memory(pp::tests::Harness& harness)
{
const auto clear = pp::app::plan_history_clear(2, 1, 4096);
PP_EXPECT(harness, clear);
if (clear) {
PP_EXPECT(harness, clear.value().operation == pp::app::HistoryUiOperation::clear);
PP_EXPECT(harness, clear.value().undo_count == 2);
PP_EXPECT(harness, clear.value().redo_count == 1);
PP_EXPECT(harness, clear.value().memory_bytes == 4096);
PP_EXPECT(harness, clear.value().clears_history);
PP_EXPECT(harness, clear.value().updates_memory_label);
PP_EXPECT(harness, !clear.value().updates_title);
}
const auto memory_only = pp::app::plan_history_clear(0, 0, 1024);
PP_EXPECT(harness, memory_only);
if (memory_only) {
PP_EXPECT(harness, memory_only.value().clears_history);
PP_EXPECT(harness, !memory_only.value().no_op);
}
const auto empty = pp::app::plan_history_clear(0, 0, 0);
PP_EXPECT(harness, empty);
if (empty) {
PP_EXPECT(harness, empty.value().no_op);
PP_EXPECT(harness, !empty.value().clears_history);
}
}
void rejects_negative_metrics(pp::tests::Harness& harness)
{
PP_EXPECT(harness, !pp::app::plan_history_undo(-1));
PP_EXPECT(harness, !pp::app::plan_history_redo(-1));
PP_EXPECT(harness, !pp::app::plan_history_clear(-1, 0, 0));
PP_EXPECT(harness, !pp::app::plan_history_clear(0, -1, 0));
PP_EXPECT(harness, !pp::app::plan_history_clear(0, 0, -1));
}
} // namespace
int main()
{
pp::tests::Harness harness;
harness.run("undo and redo plan availability", undo_and_redo_plan_availability);
harness.run("clear plan tracks stacks and memory", clear_plan_tracks_stacks_and_memory);
harness.run("rejects negative metrics", rejects_negative_metrics);
return harness.finish();
}

View File

@@ -12,6 +12,7 @@
#include "app_core/document_sharing.h" #include "app_core/document_sharing.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "app_core/grid_ui.h" #include "app_core/grid_ui.h"
#include "app_core/history_ui.h"
#include "app_core/quick_ui.h" #include "app_core/quick_ui.h"
#include "assets/image_format.h" #include "assets/image_format.h"
#include "assets/image_metadata.h" #include "assets/image_metadata.h"
@@ -276,6 +277,13 @@ struct PlanGridOperationArgs {
int sample_count = 32; int sample_count = 32;
}; };
struct PlanHistoryOperationArgs {
std::string kind = "undo";
int undo_count = 0;
int redo_count = 0;
int memory_bytes = 0;
};
struct PlanQuickOperationArgs { struct PlanQuickOperationArgs {
std::string kind = "brush"; std::string kind = "brush";
int current_index = 0; int current_index = 0;
@@ -635,6 +643,20 @@ const char* grid_ui_operation_name(pp::app::GridUiOperation operation) noexcept
return "request-heightmap-pick"; return "request-heightmap-pick";
} }
const char* history_ui_operation_name(pp::app::HistoryUiOperation operation) noexcept
{
switch (operation) {
case pp::app::HistoryUiOperation::undo:
return "undo";
case pp::app::HistoryUiOperation::redo:
return "redo";
case pp::app::HistoryUiOperation::clear:
return "clear";
}
return "undo";
}
const char* quick_ui_slot_kind_name(pp::app::QuickUiSlotKind kind) noexcept const char* quick_ui_slot_kind_name(pp::app::QuickUiSlotKind kind) noexcept
{ {
switch (kind) { switch (kind) {
@@ -919,6 +941,7 @@ void print_help()
<< " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n" << " plan-animation-operation --kind add|duplicate|remove|duration|move|goto|next|prev|onion [--frame-count N] [--total-duration N] [--current-frame N] [--selected-frame N] [--current-duration N] [--delta N] [--offset N] [--onion-size N]\n"
<< " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n" << " plan-brush-operation --kind color|tip|pattern|dual|preset|settings [--path FILE] [--thumb FILE] [--r N] [--g N] [--b N] [--a N] [--no-brush]\n"
<< " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n"
<< " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n"
<< " plan-quick-operation --kind brush|color|restore|reset [--current-index N] [--slot-index N] [--brush-index N] [--color-index N] [--slot-count N] [--fire-event]\n" << " plan-quick-operation --kind brush|color|restore|reset [--current-index N] [--slot-index N] [--brush-index N] [--color-index N] [--slot-count N] [--fire-event]\n"
<< " plan-share-file [--path FILE]\n" << " plan-share-file [--path FILE]\n"
<< " plan-picked-path [--path FILE]\n" << " plan-picked-path [--path FILE]\n"
@@ -3079,6 +3102,93 @@ int plan_grid_operation(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_history_operation_args(
int argc,
char** argv,
PlanHistoryOperationArgs& 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");
}
args.kind = argv[++i];
} else if (key == "--undo-count" || key == "--redo-count" || key == "--memory-bytes") {
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();
}
if (key == "--undo-count") {
args.undo_count = value.value();
} else if (key == "--redo-count") {
args.redo_count = value.value();
} else {
args.memory_bytes = value.value();
}
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Result<pp::app::HistoryUiPlan> make_history_operation_plan(
const PlanHistoryOperationArgs& args)
{
if (args.kind == "undo") {
return pp::app::plan_history_undo(args.undo_count);
}
if (args.kind == "redo") {
return pp::app::plan_history_redo(args.redo_count);
}
if (args.kind == "clear") {
return pp::app::plan_history_clear(args.undo_count, args.redo_count, args.memory_bytes);
}
return pp::foundation::Result<pp::app::HistoryUiPlan>::failure(
pp::foundation::Status::invalid_argument("unknown history operation kind"));
}
int plan_history_operation(int argc, char** argv)
{
PlanHistoryOperationArgs args;
const auto status = parse_plan_history_operation_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-history-operation", status.message);
return 2;
}
const auto plan = make_history_operation_plan(args);
if (!plan) {
print_error("plan-history-operation", plan.status().message);
return 2;
}
const auto& value = plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-history-operation\""
<< ",\"state\":{\"kind\":\"" << json_escape(args.kind)
<< "\",\"undoCount\":" << args.undo_count
<< ",\"redoCount\":" << args.redo_count
<< ",\"memoryBytes\":" << args.memory_bytes
<< "},\"plan\":{\"operation\":\"" << history_ui_operation_name(value.operation)
<< "\",\"undoCount\":" << value.undo_count
<< ",\"redoCount\":" << value.redo_count
<< ",\"memoryBytes\":" << value.memory_bytes
<< ",\"invokesUndo\":" << json_bool(value.invokes_undo)
<< ",\"invokesRedo\":" << json_bool(value.invokes_redo)
<< ",\"clearsHistory\":" << json_bool(value.clears_history)
<< ",\"updatesMemoryLabel\":" << json_bool(value.updates_memory_label)
<< ",\"updatesTitle\":" << json_bool(value.updates_title)
<< ",\"noOp\":" << json_bool(value.no_op)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_quick_operation_args( pp::foundation::Status parse_plan_quick_operation_args(
int argc, int argc,
char** argv, char** argv,
@@ -5619,6 +5729,10 @@ int main(int argc, char** argv)
return plan_grid_operation(argc, argv); return plan_grid_operation(argc, argv);
} }
if (command == "plan-history-operation") {
return plan_history_operation(argc, argv);
}
if (command == "plan-quick-operation") { if (command == "plan-quick-operation") {
return plan_quick_operation(argc, argv); return plan_quick_operation(argc, argv);
} }