Extract tools menu planning

This commit is contained in:
2026-06-03 12:15:57 +02:00
parent fb844f79fd
commit c640519772
8 changed files with 651 additions and 51 deletions

View File

@@ -242,7 +242,8 @@ add_library(pp_app_core STATIC
src/app_core/file_menu.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
src/app_core/tools_menu.h)
target_include_directories(pp_app_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")

File diff suppressed because one or more lines are too long

View File

@@ -538,6 +538,13 @@ legacy `ActionManager` stack execution continues.
slot selection versus popup opening, plus quick mini-state restore/reset
validation used by the live quick panel before legacy `Brush`, color picker,
stroke preview, and preset popup execution continue.
`pano_cli plan-tools-menu` and `pano_cli plan-tools-panel` expose app-core
planning for top-level Tools commands and floating-panel requests, including
already-visible no-ops, panel chrome metadata, shortcuts, camera reset,
grid-clear, and platform-only SonarPen gating before legacy UI/panel/canvas
execution continues. The live animation panel route now also checks animation
panel visibility and applies animation panel layout state instead of using the
grid panel by mistake.
`pp_platform_api` now owns a headless `PlatformServices` interface for
startup storage path preparation, clipboard text, cursor visibility,
virtual-keyboard visibility, UI-thread lifecycle hooks, render-context
@@ -1256,6 +1263,16 @@ Results:
`pano_cli_plan_quick_operation_rejects_bad_slot`, and
`pano_cli_plan_quick_operation_rejects_bad_restore` passed and expose live
quick-panel planning as JSON automation.
- `pp_app_core_tools_menu_tests` passed, covering Tools submenu routing,
root-closing commands, platform-only SonarPen gating, floating panel chrome
metadata, already-visible panel no-ops, and animation panel non-droppable
state.
- `pano_cli_plan_tools_menu_shortcuts_smoke`,
`pano_cli_plan_tools_menu_sonarpen_unavailable_smoke`,
`pano_cli_plan_tools_panel_layers_smoke`,
`pano_cli_plan_tools_panel_visible_noop_smoke`, and
`pano_cli_plan_tools_panel_rejects_unknown` passed and expose live Tools
menu/panel planning as JSON automation.
- `pp_app_core_document_sharing_tests` passed, covering saved-path gating before
platform share execution.
- `pano_cli_plan_share_file_unsaved_smoke` and

141
src/app_core/tools_menu.h Normal file
View File

@@ -0,0 +1,141 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class ToolsMenuCommand {
panels,
options,
clear_grids,
reset_camera,
shortcuts,
sonarpen,
};
enum class ToolsMenuAction {
show_panels_submenu,
show_options_submenu,
clear_grid_overlays,
reset_camera,
show_shortcuts_dialog,
start_sonarpen,
no_op_unavailable,
};
enum class ToolsPanel {
presets,
color,
color_advanced,
layers,
brush,
grids,
animation,
};
enum class ToolsPanelAction {
open_floating_panel,
no_op_already_visible,
};
struct ToolsMenuPlan {
ToolsMenuCommand command = ToolsMenuCommand::panels;
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
std::string_view label;
bool closes_root_popup = false;
};
struct ToolsPanelPlan {
ToolsPanel panel = ToolsPanel::presets;
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
std::string_view title;
int width = 0;
int height = 0;
int min_width = 0;
int min_height = 0;
bool droppable = true;
bool hides_embedded_title = false;
};
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
ToolsMenuCommand command,
bool sonarpen_available = false) noexcept
{
switch (command) {
case ToolsMenuCommand::panels:
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
case ToolsMenuCommand::options:
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
case ToolsMenuCommand::clear_grids:
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
case ToolsMenuCommand::reset_camera:
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
case ToolsMenuCommand::shortcuts:
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
case ToolsMenuCommand::sonarpen:
return {
command,
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
"SonarPen",
sonarpen_available,
};
}
return { command, ToolsMenuAction::no_op_unavailable, "", false };
}
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
ToolsPanel panel,
bool already_visible) noexcept
{
ToolsPanelPlan plan;
plan.panel = panel;
plan.action = already_visible
? ToolsPanelAction::no_op_already_visible
: ToolsPanelAction::open_floating_panel;
switch (panel) {
case ToolsPanel::presets:
plan.title = "Brushes";
plan.height = 300;
plan.min_height = 300;
plan.min_width = 100;
break;
case ToolsPanel::color:
plan.title = "Color Picker";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::color_advanced:
plan.title = "Color Picker";
plan.width = 300;
plan.height = 300;
break;
case ToolsPanel::layers:
plan.title = "Layers";
plan.height = 300;
plan.min_height = 100;
plan.hides_embedded_title = true;
break;
case ToolsPanel::brush:
plan.title = "Brush Settings";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::grids:
plan.title = "Grid";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::animation:
plan.title = "Animation";
plan.width = 500;
plan.height = 300;
plan.droppable = false;
break;
}
return plan;
}
}

View File

@@ -16,6 +16,7 @@
#include "app_core/file_menu.h"
#include "app_core/app_status.h"
#include "app_core/history_ui.h"
#include "app_core/tools_menu.h"
#include "settings.h"
#include "serializer.h"
#include "font.h"
@@ -223,6 +224,29 @@ pp::app::DocumentLayerMenuPlan make_layer_menu_plan(
return {};
}
[[nodiscard]] bool should_open_tools_panel(const pp::app::ToolsPanelPlan& plan) noexcept
{
return plan.action == pp::app::ToolsPanelAction::open_floating_panel;
}
void apply_tools_panel_chrome(NodePanelFloating& panel, const pp::app::ToolsPanelPlan& plan)
{
if (plan.width > 0 && plan.height > 0) {
panel.SetSize(static_cast<float>(plan.width), static_cast<float>(plan.height));
} else {
if (plan.width > 0)
panel.SetWidth(static_cast<float>(plan.width));
if (plan.height > 0)
panel.SetHeight(static_cast<float>(plan.height));
}
if (plan.min_width > 0)
panel.SetMinWidth(static_cast<float>(plan.min_width));
if (plan.min_height > 0)
panel.SetMinHeight(static_cast<float>(plan.min_height));
panel.m_title->set_text(plan.title.data());
panel.m_droppable = plan.droppable;
}
} // namespace
void App::title_update()
@@ -1052,6 +1076,10 @@ void App::init_menu_tools()
if (auto tick = popup_exp->find<NodeButtonCustom>("tools-panels")) tick->on_click = [this, popup_exp](Node* b)
{
const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels);
if (menu_plan.action != pp::app::ToolsMenuAction::show_panels_submenu)
return;
glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0);
auto popup_time = layout[const_hash("panels-menu")]->m_children[0]->clone<NodePopupMenu>();
popup_time->update();
@@ -1076,14 +1104,14 @@ void App::init_menu_tools()
};
popup_time->find<NodeButtonCustom>("panel-presets")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(floating_presets.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::presets,
visible(floating_presets.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Presets;
fpanel->SetHeight(300);
fpanel->SetMinHeight(300);
fpanel->SetMinWidth(100);
fpanel->m_title->set_text("Brushes");
apply_tools_panel_chrome(*fpanel, plan);
if (!floating_presets)
{
floating_presets = fpanel->m_container->add_child_ref<NodePanelBrushPreset>();
@@ -1103,19 +1131,21 @@ void App::init_menu_tools()
};
popup_time->find<NodeButtonCustom>("panel-color")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(floating_color.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::color,
visible(floating_color.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Color;
fpanel->SetHeight(300);
fpanel->SetMinHeight(300);
fpanel->m_title->set_text("Color Picker");
apply_tools_panel_chrome(*fpanel, plan);
if (!floating_color)
{
floating_color = fpanel->m_container->add_child_ref<NodePanelColor>();
floating_color->SetHeightP(100);
//floating_color->SetMinHeight(300);
floating_color->find("title")->SetVisibility(false);
if (plan.hides_embedded_title)
floating_color->find("title")->SetVisibility(false);
floating_color->on_color_changed = [this](Node* target, glm::vec4 color) {
apply_brush_color_plan(*this, color, false, false);
};
@@ -1128,13 +1158,14 @@ void App::init_menu_tools()
popup_time->destroy();
};
popup_time->find<NodeButtonCustom>("panel-color-adv")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(floating_picker.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::color_advanced,
visible(floating_picker.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::ColorAdv;
fpanel->SetHeight(300);
fpanel->SetWidth(300);
fpanel->m_title->set_text("Color Picker");
apply_tools_panel_chrome(*fpanel, plan);
if (!floating_picker)
{
floating_picker = fpanel->m_container->add_child_ref<NodeColorPicker>();
@@ -1154,72 +1185,80 @@ void App::init_menu_tools()
popup_time->destroy();
};
popup_time->find<NodeButtonCustom>("panel-layers")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(layers.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::layers,
visible(layers.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Layers;
fpanel->SetMinHeight(100);
fpanel->SetHeight(300);
apply_tools_panel_chrome(*fpanel, plan);
fpanel->m_container->add_child(layers);
fpanel->m_title->set_text("Layers");
layers->SetPositioning(YGPositionTypeRelative);
layers->SetPosition(0, 0);
layers->SetWidthP(100);
layers->SetHeightP(100);
layers->SetFlexShrink(0);
layers->find("title")->SetVisibility(false);
if (plan.hides_embedded_title)
layers->find("title")->SetVisibility(false);
popup_exp->destroy();
popup_time->destroy();
};
popup_time->find<NodeButtonCustom>("panel-brush")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(stroke.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::brush,
visible(stroke.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Brush;
fpanel->m_container->add_child(stroke);
fpanel->SetHeight(300);
fpanel->m_title->set_text("Brush Settings");
apply_tools_panel_chrome(*fpanel, plan);
stroke->SetPositioning(YGPositionTypeRelative);
stroke->SetPosition(0, 0);
stroke->SetWidthP(100);
stroke->SetHeightP(100);
stroke->find("title")->SetVisibility(false);
if (plan.hides_embedded_title)
stroke->find("title")->SetVisibility(false);
popup_exp->destroy();
popup_time->destroy();
};
popup_time->find<NodeButtonCustom>("panel-grids")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(grid.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::grids,
visible(grid.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Grids;
fpanel->m_container->add_child(grid);
fpanel->SetHeight(300);
fpanel->m_title->set_text("Grid");
apply_tools_panel_chrome(*fpanel, plan);
grid->SetPositioning(YGPositionTypeRelative);
grid->SetPosition(0, 0);
grid->SetWidthP(100);
grid->SetHeightP(100);
grid->find("title")->SetVisibility(false);
if (plan.hides_embedded_title)
grid->find("title")->SetVisibility(false);
popup_exp->destroy();
popup_time->destroy();
};
popup_time->find<NodeButtonCustom>("panel-animation")->on_click = [this, popup_time, popup_exp, visible](Node*) {
if (visible(grid.get()))
const auto plan = pp::app::plan_tools_panel(
pp::app::ToolsPanel::animation,
visible(animation.get()));
if (!should_open_tools_panel(plan))
return;
auto fpanel = floatings_container->add_child<NodePanelFloating>();
fpanel->m_class = NodePanelFloating::kClass::Animation;
fpanel->m_container->add_child(animation);
fpanel->SetSize(500, 300);
fpanel->m_title->set_text("Animation");
fpanel->m_droppable = false;
grid->SetPositioning(YGPositionTypeRelative);
grid->SetPosition(0, 0);
grid->SetWidthP(100);
grid->SetHeightP(100);
grid->find("title")->SetVisibility(false);
apply_tools_panel_chrome(*fpanel, plan);
animation->SetPositioning(YGPositionTypeRelative);
animation->SetPosition(0, 0);
animation->SetWidthP(100);
animation->SetHeightP(100);
popup_exp->destroy();
popup_time->destroy();
@@ -1228,6 +1267,10 @@ void App::init_menu_tools()
if (auto options = popup_exp->find<NodeButtonCustom>("tools-options")) options->on_click = [this, options, main](Node* b)
{
const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options);
if (menu_plan.action != pp::app::ToolsMenuAction::show_options_submenu)
return;
glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0);
auto popup_time = layout[const_hash("options-menu")]->m_children[0]->clone<NodePopupMenu>();
popup_time->update();
@@ -1378,22 +1421,39 @@ void App::init_menu_tools()
};
popup_exp->find<NodeButtonCustom>("clear-grids")->on_click = [this, popup_exp](Node*) {
CanvasModeGrid* mode = (CanvasModeGrid*)Canvas::modes[(int)kCanvasMode::Grid][0];
mode->clear();
popup_exp->mouse_release();
popup_exp->destroy();
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids);
if (plan.action == pp::app::ToolsMenuAction::clear_grid_overlays)
{
CanvasModeGrid* mode = (CanvasModeGrid*)Canvas::modes[(int)kCanvasMode::Grid][0];
mode->clear();
}
if (plan.closes_root_popup)
{
popup_exp->mouse_release();
popup_exp->destroy();
}
};
popup_exp->find<NodeButtonCustom>("camera-reset")->on_click = [this, popup_exp](Node*) {
canvas->reset_camera();
popup_exp->mouse_release();
popup_exp->destroy();
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera);
if (plan.action == pp::app::ToolsMenuAction::reset_camera)
canvas->reset_camera();
if (plan.closes_root_popup)
{
popup_exp->mouse_release();
popup_exp->destroy();
}
};
popup_exp->find<NodeButtonCustom>("shortcuts")->on_click = [this, popup_exp](Node*) {
dialog_shortcuts();
popup_exp->mouse_release();
popup_exp->destroy();
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
if (plan.action == pp::app::ToolsMenuAction::show_shortcuts_dialog)
dialog_shortcuts();
if (plan.closes_root_popup)
{
popup_exp->mouse_release();
popup_exp->destroy();
}
};
/*
@@ -1406,9 +1466,14 @@ void App::init_menu_tools()
#if __IOS__
popup_exp->find<NodeButtonCustom>("sonarpen")->on_click = [this, popup_exp](Node*) {
[ios_app sonarpen_start];
popup_exp->mouse_release();
popup_exp->destroy();
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true);
if (plan.action == pp::app::ToolsMenuAction::start_sonarpen)
[ios_app sonarpen_start];
if (plan.closes_root_popup)
{
popup_exp->mouse_release();
popup_exp->destroy();
}
};
#endif
};

View File

@@ -318,6 +318,16 @@ add_test(NAME pp_app_core_quick_ui_tests COMMAND pp_app_core_quick_ui_tests)
set_tests_properties(pp_app_core_quick_ui_tests PROPERTIES
LABELS "app;ui;paint;desktop-fast;fuzz")
add_executable(pp_app_core_tools_menu_tests
app_core/tools_menu_tests.cpp)
target_link_libraries(pp_app_core_tools_menu_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_tools_menu_tests COMMAND pp_app_core_tools_menu_tests)
set_tests_properties(pp_app_core_tools_menu_tests PROPERTIES
LABELS "app;ui;desktop-fast;fuzz")
add_executable(pp_app_core_document_route_tests
app_core/document_route_tests.cpp)
target_link_libraries(pp_app_core_document_route_tests PRIVATE
@@ -811,6 +821,36 @@ 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_tools_menu_shortcuts_smoke
COMMAND pano_cli plan-tools-menu --command shortcuts)
set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-menu\".*\"command\":\"shortcuts\".*\"action\":\"show-shortcuts-dialog\".*\"closesRootPopup\":true")
add_test(NAME pano_cli_plan_tools_menu_sonarpen_unavailable_smoke
COMMAND pano_cli plan-tools-menu --command sonarpen)
set_tests_properties(pano_cli_plan_tools_menu_sonarpen_unavailable_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-menu\".*\"sonarpenAvailable\":false.*\"action\":\"no-op-unavailable\".*\"closesRootPopup\":false")
add_test(NAME pano_cli_plan_tools_panel_layers_smoke
COMMAND pano_cli plan-tools-panel --panel layers)
set_tests_properties(pano_cli_plan_tools_panel_layers_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-panel\".*\"panel\":\"layers\".*\"action\":\"open-floating-panel\".*\"title\":\"Layers\".*\"height\":300.*\"minHeight\":100.*\"hidesEmbeddedTitle\":true")
add_test(NAME pano_cli_plan_tools_panel_visible_noop_smoke
COMMAND pano_cli plan-tools-panel --panel animation --already-visible)
set_tests_properties(pano_cli_plan_tools_panel_visible_noop_smoke PROPERTIES
LABELS "app;ui;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-tools-panel\".*\"alreadyVisible\":true.*\"action\":\"no-op-already-visible\".*\"title\":\"Animation\".*\"droppable\":false")
add_test(NAME pano_cli_plan_tools_panel_rejects_unknown
COMMAND pano_cli plan-tools-panel --panel missing)
set_tests_properties(pano_cli_plan_tools_panel_rejects_unknown PROPERTIES
LABELS "app;ui;integration;desktop-fast;fuzz"
WILL_FAIL TRUE)
add_test(NAME pano_cli_plan_app_status_smoke
COMMAND pano_cli plan-app-status
--doc-name demo

View File

@@ -0,0 +1,80 @@
#include "app_core/tools_menu.h"
#include "test_harness.h"
namespace {
void tools_menu_maps_submenus_and_commands(pp::tests::Harness& harness)
{
const auto panels = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels);
const auto options = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options);
const auto clear = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids);
const auto camera = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera);
const auto shortcuts = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
PP_EXPECT(harness, panels.action == pp::app::ToolsMenuAction::show_panels_submenu);
PP_EXPECT(harness, !panels.closes_root_popup);
PP_EXPECT(harness, options.action == pp::app::ToolsMenuAction::show_options_submenu);
PP_EXPECT(harness, clear.action == pp::app::ToolsMenuAction::clear_grid_overlays);
PP_EXPECT(harness, clear.closes_root_popup);
PP_EXPECT(harness, camera.action == pp::app::ToolsMenuAction::reset_camera);
PP_EXPECT(harness, camera.closes_root_popup);
PP_EXPECT(harness, shortcuts.action == pp::app::ToolsMenuAction::show_shortcuts_dialog);
PP_EXPECT(harness, shortcuts.closes_root_popup);
}
void tools_menu_gates_platform_only_actions(pp::tests::Harness& harness)
{
const auto unavailable = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen);
const auto available = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::sonarpen, true);
PP_EXPECT(harness, unavailable.action == pp::app::ToolsMenuAction::no_op_unavailable);
PP_EXPECT(harness, !unavailable.closes_root_popup);
PP_EXPECT(harness, available.action == pp::app::ToolsMenuAction::start_sonarpen);
PP_EXPECT(harness, available.closes_root_popup);
}
void tools_panel_plans_floating_panel_metadata(pp::tests::Harness& harness)
{
const auto presets = pp::app::plan_tools_panel(pp::app::ToolsPanel::presets, false);
const auto color = pp::app::plan_tools_panel(pp::app::ToolsPanel::color, false);
const auto layers = pp::app::plan_tools_panel(pp::app::ToolsPanel::layers, false);
const auto animation = pp::app::plan_tools_panel(pp::app::ToolsPanel::animation, false);
PP_EXPECT(harness, presets.action == pp::app::ToolsPanelAction::open_floating_panel);
PP_EXPECT(harness, presets.title == "Brushes");
PP_EXPECT(harness, presets.height == 300);
PP_EXPECT(harness, presets.min_height == 300);
PP_EXPECT(harness, presets.min_width == 100);
PP_EXPECT(harness, color.title == "Color Picker");
PP_EXPECT(harness, color.hides_embedded_title);
PP_EXPECT(harness, layers.title == "Layers");
PP_EXPECT(harness, layers.min_height == 100);
PP_EXPECT(harness, layers.hides_embedded_title);
PP_EXPECT(harness, animation.title == "Animation");
PP_EXPECT(harness, animation.width == 500);
PP_EXPECT(harness, animation.height == 300);
PP_EXPECT(harness, !animation.droppable);
}
void tools_panel_no_ops_when_panel_is_already_visible(pp::tests::Harness& harness)
{
const auto visible = pp::app::plan_tools_panel(pp::app::ToolsPanel::brush, true);
const auto hidden = pp::app::plan_tools_panel(pp::app::ToolsPanel::brush, false);
PP_EXPECT(harness, visible.action == pp::app::ToolsPanelAction::no_op_already_visible);
PP_EXPECT(harness, hidden.action == pp::app::ToolsPanelAction::open_floating_panel);
PP_EXPECT(harness, visible.title == hidden.title);
PP_EXPECT(harness, visible.hides_embedded_title == hidden.hides_embedded_title);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("tools menu maps submenus and commands", tools_menu_maps_submenus_and_commands);
harness.run("tools menu gates platform only actions", tools_menu_gates_platform_only_actions);
harness.run("tools panel plans floating panel metadata", tools_panel_plans_floating_panel_metadata);
harness.run("tools panel no ops when panel is already visible", tools_panel_no_ops_when_panel_is_already_visible);
return harness.finish();
}

View File

@@ -18,6 +18,7 @@
#include "app_core/grid_ui.h"
#include "app_core/history_ui.h"
#include "app_core/quick_ui.h"
#include "app_core/tools_menu.h"
#include "assets/image_format.h"
#include "assets/image_metadata.h"
#include "assets/image_pixels.h"
@@ -341,6 +342,16 @@ struct PlanQuickOperationArgs {
bool fire_event = false;
};
struct PlanToolsMenuArgs {
std::string command = "shortcuts";
bool sonarpen_available = false;
};
struct PlanToolsPanelArgs {
std::string panel = "layers";
bool already_visible = false;
};
struct SimulateAppSessionArgs {
bool has_canvas = true;
bool new_document = false;
@@ -650,6 +661,135 @@ const char* file_menu_action_name(pp::app::FileMenuAction action) noexcept
return "show-new-document-dialog";
}
const char* tools_menu_command_name(pp::app::ToolsMenuCommand command) noexcept
{
switch (command) {
case pp::app::ToolsMenuCommand::panels:
return "panels";
case pp::app::ToolsMenuCommand::options:
return "options";
case pp::app::ToolsMenuCommand::clear_grids:
return "clear-grids";
case pp::app::ToolsMenuCommand::reset_camera:
return "reset-camera";
case pp::app::ToolsMenuCommand::shortcuts:
return "shortcuts";
case pp::app::ToolsMenuCommand::sonarpen:
return "sonarpen";
}
return "shortcuts";
}
const char* tools_menu_action_name(pp::app::ToolsMenuAction action) noexcept
{
switch (action) {
case pp::app::ToolsMenuAction::show_panels_submenu:
return "show-panels-submenu";
case pp::app::ToolsMenuAction::show_options_submenu:
return "show-options-submenu";
case pp::app::ToolsMenuAction::clear_grid_overlays:
return "clear-grid-overlays";
case pp::app::ToolsMenuAction::reset_camera:
return "reset-camera";
case pp::app::ToolsMenuAction::show_shortcuts_dialog:
return "show-shortcuts-dialog";
case pp::app::ToolsMenuAction::start_sonarpen:
return "start-sonarpen";
case pp::app::ToolsMenuAction::no_op_unavailable:
return "no-op-unavailable";
}
return "no-op-unavailable";
}
const char* tools_panel_name(pp::app::ToolsPanel panel) noexcept
{
switch (panel) {
case pp::app::ToolsPanel::presets:
return "presets";
case pp::app::ToolsPanel::color:
return "color";
case pp::app::ToolsPanel::color_advanced:
return "color-advanced";
case pp::app::ToolsPanel::layers:
return "layers";
case pp::app::ToolsPanel::brush:
return "brush";
case pp::app::ToolsPanel::grids:
return "grids";
case pp::app::ToolsPanel::animation:
return "animation";
}
return "layers";
}
const char* tools_panel_action_name(pp::app::ToolsPanelAction action) noexcept
{
switch (action) {
case pp::app::ToolsPanelAction::open_floating_panel:
return "open-floating-panel";
case pp::app::ToolsPanelAction::no_op_already_visible:
return "no-op-already-visible";
}
return "no-op-already-visible";
}
pp::foundation::Result<pp::app::ToolsMenuCommand> parse_tools_menu_command(std::string_view command)
{
if (command == "panels") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::panels);
}
if (command == "options") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::options);
}
if (command == "clear-grids") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::clear_grids);
}
if (command == "reset-camera") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::reset_camera);
}
if (command == "shortcuts") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::shortcuts);
}
if (command == "sonarpen") {
return pp::foundation::Result<pp::app::ToolsMenuCommand>::success(pp::app::ToolsMenuCommand::sonarpen);
}
return pp::foundation::Result<pp::app::ToolsMenuCommand>::failure(
pp::foundation::Status::invalid_argument("unknown tools menu command"));
}
pp::foundation::Result<pp::app::ToolsPanel> parse_tools_panel(std::string_view panel)
{
if (panel == "presets") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::presets);
}
if (panel == "color") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::color);
}
if (panel == "color-advanced" || panel == "advanced-color") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::color_advanced);
}
if (panel == "layers") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::layers);
}
if (panel == "brush") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::brush);
}
if (panel == "grids" || panel == "grid") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::grids);
}
if (panel == "animation") {
return pp::foundation::Result<pp::app::ToolsPanel>::success(pp::app::ToolsPanel::animation);
}
return pp::foundation::Result<pp::app::ToolsPanel>::failure(
pp::foundation::Status::invalid_argument("unknown tools panel"));
}
const char* document_layer_rename_action_name(pp::app::DocumentLayerRenameAction action) noexcept
{
switch (action) {
@@ -1264,6 +1404,8 @@ void print_help()
<< " 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-tools-menu --command panels|options|clear-grids|reset-camera|shortcuts|sonarpen [--sonarpen-available]\n"
<< " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n"
<< " plan-canvas-clear [--no-canvas] [--r N] [--g N] [--b N] [--a N]\n"
<< " plan-image-import --width N --height N\n"
<< " plan-document-resize [--current-resolution N] [--selected-resolution-index N]\n"
@@ -2816,6 +2958,111 @@ int plan_app_preferences(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_plan_tools_menu_args(
int argc,
char** argv,
PlanToolsMenuArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--command") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.command = argv[++i];
} else if (key == "--sonarpen-available") {
args.sonarpen_available = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_tools_menu(int argc, char** argv)
{
PlanToolsMenuArgs args;
const auto status = parse_plan_tools_menu_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-tools-menu", status.message);
return 2;
}
const auto command = parse_tools_menu_command(args.command);
if (!command) {
print_error("plan-tools-menu", command.status().message);
return 2;
}
const auto plan = pp::app::plan_tools_menu_command(command.value(), args.sonarpen_available);
std::cout << "{\"ok\":true,\"command\":\"plan-tools-menu\""
<< ",\"state\":{\"command\":\"" << json_escape(args.command)
<< "\",\"sonarpenAvailable\":" << json_bool(args.sonarpen_available)
<< "},\"plan\":{\"command\":\"" << tools_menu_command_name(plan.command)
<< "\",\"action\":\"" << tools_menu_action_name(plan.action)
<< "\",\"label\":\"" << json_escape(std::string(plan.label))
<< "\",\"closesRootPopup\":" << json_bool(plan.closes_root_popup)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_tools_panel_args(
int argc,
char** argv,
PlanToolsPanelArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--panel") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.panel = argv[++i];
} else if (key == "--already-visible") {
args.already_visible = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_tools_panel(int argc, char** argv)
{
PlanToolsPanelArgs args;
const auto status = parse_plan_tools_panel_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-tools-panel", status.message);
return 2;
}
const auto panel = parse_tools_panel(args.panel);
if (!panel) {
print_error("plan-tools-panel", panel.status().message);
return 2;
}
const auto plan = pp::app::plan_tools_panel(panel.value(), args.already_visible);
std::cout << "{\"ok\":true,\"command\":\"plan-tools-panel\""
<< ",\"state\":{\"panel\":\"" << json_escape(args.panel)
<< "\",\"alreadyVisible\":" << json_bool(args.already_visible)
<< "},\"plan\":{\"panel\":\"" << tools_panel_name(plan.panel)
<< "\",\"action\":\"" << tools_panel_action_name(plan.action)
<< "\",\"title\":\"" << json_escape(std::string(plan.title))
<< "\",\"width\":" << plan.width
<< ",\"height\":" << plan.height
<< ",\"minWidth\":" << plan.min_width
<< ",\"minHeight\":" << plan.min_height
<< ",\"droppable\":" << json_bool(plan.droppable)
<< ",\"hidesEmbeddedTitle\":" << json_bool(plan.hides_embedded_title)
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_app_status_args(
int argc,
char** argv,
@@ -6579,6 +6826,14 @@ int main(int argc, char** argv)
return plan_app_status(argc, argv);
}
if (command == "plan-tools-menu") {
return plan_tools_menu(argc, argv);
}
if (command == "plan-tools-panel") {
return plan_tools_panel(argc, argv);
}
if (command == "plan-document-resize") {
return plan_document_resize(argc, argv);
}