Extract tools menu planning
This commit is contained in:
@@ -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
@@ -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
141
src/app_core/tools_menu.h
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
80
tests/app_core/tools_menu_tests.cpp
Normal file
80
tests/app_core/tools_menu_tests.cpp
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user