feat(xcui): add editor command and menu foundations
This commit is contained in:
@@ -2,9 +2,13 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
test_editor_shell_asset_validation.cpp
|
||||
test_input_modifier_tracker.cpp
|
||||
test_structured_editor_shell.cpp
|
||||
test_ui_editor_command_dispatcher.cpp
|
||||
test_ui_editor_command_registry.cpp
|
||||
test_ui_editor_menu_model.cpp
|
||||
test_ui_editor_panel_registry.cpp
|
||||
test_ui_editor_collection_primitives.cpp
|
||||
test_ui_editor_panel_chrome.cpp
|
||||
test_ui_editor_shortcut_manager.cpp
|
||||
test_ui_editor_workspace_controller.cpp
|
||||
test_ui_editor_workspace_layout_persistence.cpp
|
||||
test_ui_editor_workspace_model.cpp
|
||||
|
||||
118
tests/UI/Editor/unit/test_ui_editor_command_dispatcher.cpp
Normal file
118
tests/UI/Editor/unit/test_ui_editor_command_dispatcher.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorCommandDispatcher.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::NewEditor::GetUIEditorCommandDispatchStatusName;
|
||||
using XCEngine::NewEditor::UIEditorCommandDispatchStatus;
|
||||
using XCEngine::NewEditor::UIEditorCommandDispatcher;
|
||||
using XCEngine::NewEditor::UIEditorCommandEvaluationCode;
|
||||
using XCEngine::NewEditor::UIEditorCommandPanelSource;
|
||||
using XCEngine::NewEditor::UIEditorCommandRegistry;
|
||||
using XCEngine::NewEditor::UIEditorPanelRegistry;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceModel;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, EvaluateResolvesActivePanelCommandToCurrentWorkspaceTarget) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto evaluation = dispatcher.Evaluate("workspace.hide_active", controller);
|
||||
|
||||
EXPECT_EQ(evaluation.code, UIEditorCommandEvaluationCode::None);
|
||||
EXPECT_TRUE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.commandId, "workspace.hide_active");
|
||||
EXPECT_EQ(evaluation.workspaceCommand.kind, UIEditorWorkspaceCommandKind::HidePanel);
|
||||
EXPECT_EQ(evaluation.workspaceCommand.panelId, "doc-a");
|
||||
EXPECT_EQ(evaluation.previewResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, EvaluateRejectsActivePanelCommandWhenWorkspaceHasNoActivePanel) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
workspace.activePanelId.clear();
|
||||
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), workspace);
|
||||
|
||||
const auto evaluation = dispatcher.Evaluate("workspace.hide_active", controller);
|
||||
|
||||
EXPECT_EQ(evaluation.code, UIEditorCommandEvaluationCode::MissingActivePanel);
|
||||
EXPECT_FALSE(evaluation.executable);
|
||||
EXPECT_EQ(evaluation.previewResult.status, UIEditorWorkspaceCommandStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandDispatcherTest, DispatchUsesResolvedWorkspaceCommandAndMutatesController) {
|
||||
UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = dispatcher.Dispatch("workspace.activate_details", controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorCommandDispatchStatus::Dispatched);
|
||||
EXPECT_STREQ(GetUIEditorCommandDispatchStatusName(result.status).data(), "Dispatched");
|
||||
EXPECT_TRUE(result.commandExecuted);
|
||||
EXPECT_EQ(result.commandId, "workspace.activate_details");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
75
tests/UI/Editor/unit/test_ui_editor_command_registry.cpp
Normal file
75
tests/UI/Editor/unit/test_ui_editor_command_registry.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorCommandRegistry.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::NewEditor::FindUIEditorCommandDescriptor;
|
||||
using XCEngine::NewEditor::UIEditorCommandDescriptor;
|
||||
using XCEngine::NewEditor::UIEditorCommandPanelSource;
|
||||
using XCEngine::NewEditor::UIEditorCommandRegistry;
|
||||
using XCEngine::NewEditor::UIEditorCommandRegistryValidationCode;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::NewEditor::ValidateUIEditorCommandRegistry;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, FindDescriptorReturnsRegisteredCommand) {
|
||||
const UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
|
||||
const UIEditorCommandDescriptor* descriptor =
|
||||
FindUIEditorCommandDescriptor(registry, "workspace.activate_details");
|
||||
|
||||
ASSERT_NE(descriptor, nullptr);
|
||||
EXPECT_EQ(descriptor->displayName, "Activate Details");
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, ValidationRejectsDuplicateCommandIdsAndMissingDisplayName) {
|
||||
UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
registry.commands.push_back(registry.commands.front());
|
||||
|
||||
auto validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::DuplicateCommandId);
|
||||
|
||||
registry = BuildCommandRegistry();
|
||||
registry.commands[1].displayName.clear();
|
||||
validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::EmptyDisplayName);
|
||||
}
|
||||
|
||||
TEST(UIEditorCommandRegistryTest, ValidationRejectsInvalidPanelSourceUsage) {
|
||||
UIEditorCommandRegistry registry = BuildCommandRegistry();
|
||||
registry.commands[0].workspaceCommand.panelSource = UIEditorCommandPanelSource::None;
|
||||
|
||||
auto validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::MissingPanelSource);
|
||||
|
||||
registry = BuildCommandRegistry();
|
||||
registry.commands[2].workspaceCommand.panelSource =
|
||||
UIEditorCommandPanelSource::FixedPanelId;
|
||||
registry.commands[2].workspaceCommand.panelId = "details";
|
||||
validation = ValidateUIEditorCommandRegistry(registry);
|
||||
EXPECT_EQ(validation.code, UIEditorCommandRegistryValidationCode::UnexpectedPanelSource);
|
||||
}
|
||||
223
tests/UI/Editor/unit/test_ui_editor_menu_model.cpp
Normal file
223
tests/UI/Editor/unit/test_ui_editor_menu_model.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorMenuModel.h>
|
||||
#include <XCNewEditor/Editor/UIEditorShortcutManager.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::NewEditor::BuildUIEditorResolvedMenuModel;
|
||||
using XCEngine::NewEditor::UIEditorCommandDispatcher;
|
||||
using XCEngine::NewEditor::UIEditorCommandPanelSource;
|
||||
using XCEngine::NewEditor::UIEditorCommandRegistry;
|
||||
using XCEngine::NewEditor::UIEditorMenuCheckedStateSource;
|
||||
using XCEngine::NewEditor::UIEditorMenuItemDescriptor;
|
||||
using XCEngine::NewEditor::UIEditorMenuItemKind;
|
||||
using XCEngine::NewEditor::UIEditorMenuModel;
|
||||
using XCEngine::NewEditor::UIEditorMenuModelValidationCode;
|
||||
using XCEngine::NewEditor::UIEditorPanelRegistry;
|
||||
using XCEngine::NewEditor::UIEditorShortcutManager;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceModel;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::NewEditor::ValidateUIEditorMenuModel;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.show_details",
|
||||
"Show Details",
|
||||
{ UIEditorWorkspaceCommandKind::ShowPanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(
|
||||
std::string commandId,
|
||||
UIShortcutScope scope,
|
||||
XCEngine::UI::UIElementId ownerId,
|
||||
KeyCode keyCode) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = scope;
|
||||
binding.ownerId = ownerId;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = true;
|
||||
return binding;
|
||||
}
|
||||
|
||||
UIEditorMenuModel BuildMenuModel() {
|
||||
UIEditorMenuModel model = {};
|
||||
model.menus = {
|
||||
{
|
||||
"window",
|
||||
"Window",
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"show-details",
|
||||
{},
|
||||
"workspace.show_details",
|
||||
{ UIEditorMenuCheckedStateSource::PanelVisible, "details" },
|
||||
{}
|
||||
},
|
||||
{
|
||||
UIEditorMenuItemKind::Separator,
|
||||
"separator-1",
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
},
|
||||
{
|
||||
UIEditorMenuItemKind::Submenu,
|
||||
"layout",
|
||||
"Layout",
|
||||
{},
|
||||
{},
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"reset-layout",
|
||||
{},
|
||||
"workspace.reset",
|
||||
{},
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorMenuModelTest, ValidationRejectsUnknownCommandAndInvalidSubmenuShape) {
|
||||
UIEditorMenuModel model = BuildMenuModel();
|
||||
model.menus[0].items[0].commandId = "missing.command";
|
||||
|
||||
auto validation = ValidateUIEditorMenuModel(model, BuildCommandRegistry());
|
||||
EXPECT_EQ(validation.code, UIEditorMenuModelValidationCode::UnknownCommandId);
|
||||
|
||||
model = BuildMenuModel();
|
||||
model.menus[0].items[2].children.clear();
|
||||
validation = ValidateUIEditorMenuModel(model, BuildCommandRegistry());
|
||||
EXPECT_EQ(validation.code, UIEditorMenuModelValidationCode::SubmenuEmptyChildren);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuModelTest, ResolvedModelUsesCommandDisplayNameShortcutTextAndCheckedState) {
|
||||
UIEditorShortcutManager shortcutManager(BuildCommandRegistry());
|
||||
shortcutManager.RegisterBinding(
|
||||
MakeBinding("workspace.reset", UIShortcutScope::Panel, 22u, KeyCode::P));
|
||||
shortcutManager.RegisterBinding(
|
||||
MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::R));
|
||||
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
const UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
|
||||
const auto resolved = BuildUIEditorResolvedMenuModel(
|
||||
BuildMenuModel(),
|
||||
dispatcher,
|
||||
controller,
|
||||
&shortcutManager);
|
||||
|
||||
ASSERT_EQ(resolved.menus.size(), 1u);
|
||||
ASSERT_EQ(resolved.menus[0].items.size(), 3u);
|
||||
EXPECT_EQ(resolved.menus[0].items[0].label, "Show Details");
|
||||
EXPECT_TRUE(resolved.menus[0].items[0].checked);
|
||||
|
||||
const auto& submenu = resolved.menus[0].items[2];
|
||||
ASSERT_EQ(submenu.children.size(), 1u);
|
||||
EXPECT_EQ(submenu.children[0].label, "Reset Workspace");
|
||||
EXPECT_EQ(submenu.children[0].shortcutText, "Ctrl+R");
|
||||
EXPECT_TRUE(submenu.children[0].enabled);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuModelTest, ResolvedModelDisablesCommandWhenEvaluationCannotResolveActivePanel) {
|
||||
UIEditorMenuModel model = {};
|
||||
model.menus = {
|
||||
{
|
||||
"window",
|
||||
"Window",
|
||||
{
|
||||
{
|
||||
UIEditorMenuItemKind::Command,
|
||||
"hide-active",
|
||||
{},
|
||||
"workspace.hide_active",
|
||||
{},
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||
workspace.activePanelId.clear();
|
||||
const auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), workspace);
|
||||
const UIEditorCommandDispatcher dispatcher(BuildCommandRegistry());
|
||||
|
||||
const auto resolved = BuildUIEditorResolvedMenuModel(model, dispatcher, controller);
|
||||
|
||||
ASSERT_EQ(resolved.menus.size(), 1u);
|
||||
ASSERT_EQ(resolved.menus[0].items.size(), 1u);
|
||||
EXPECT_FALSE(resolved.menus[0].items[0].enabled);
|
||||
EXPECT_EQ(
|
||||
resolved.menus[0].items[0].previewStatus,
|
||||
UIEditorWorkspaceCommandStatus::Rejected);
|
||||
}
|
||||
194
tests/UI/Editor/unit/test_ui_editor_shortcut_manager.cpp
Normal file
194
tests/UI/Editor/unit/test_ui_editor_shortcut_manager.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCNewEditor/Editor/UIEditorShortcutManager.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceController;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::NewEditor::UIEditorCommandPanelSource;
|
||||
using XCEngine::NewEditor::UIEditorCommandRegistry;
|
||||
using XCEngine::NewEditor::UIEditorShortcutDispatchStatus;
|
||||
using XCEngine::NewEditor::UIEditorShortcutManager;
|
||||
using XCEngine::NewEditor::UIEditorShortcutManagerValidationCode;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceCommandStatus;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceModel;
|
||||
using XCEngine::NewEditor::UIEditorWorkspaceSplitAxis;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutContext;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
UIEditorCommandRegistry BuildCommandRegistry() {
|
||||
UIEditorCommandRegistry registry = {};
|
||||
registry.commands = {
|
||||
{
|
||||
"workspace.hide_active",
|
||||
"Hide Active",
|
||||
{ UIEditorWorkspaceCommandKind::HidePanel, UIEditorCommandPanelSource::ActivePanel, {} }
|
||||
},
|
||||
{
|
||||
"workspace.activate_details",
|
||||
"Activate Details",
|
||||
{ UIEditorWorkspaceCommandKind::ActivatePanel, UIEditorCommandPanelSource::FixedPanelId, "details" }
|
||||
},
|
||||
{
|
||||
"workspace.reset",
|
||||
"Reset Workspace",
|
||||
{ UIEditorWorkspaceCommandKind::ResetWorkspace, UIEditorCommandPanelSource::None, {} }
|
||||
}
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.66f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIShortcutBinding MakeBinding(
|
||||
std::string commandId,
|
||||
UIShortcutScope scope,
|
||||
XCEngine::UI::UIElementId ownerId,
|
||||
KeyCode keyCode,
|
||||
bool control = true) {
|
||||
UIShortcutBinding binding = {};
|
||||
binding.commandId = std::move(commandId);
|
||||
binding.scope = scope;
|
||||
binding.ownerId = ownerId;
|
||||
binding.triggerEventType = UIInputEventType::KeyDown;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
binding.chord.modifiers.control = control;
|
||||
return binding;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCtrlKeyEvent(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
event.modifiers.control = true;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIShortcutContext BuildPanelShortcutContext(XCEngine::UI::UIElementId panelOwnerId) {
|
||||
UIShortcutContext context = {};
|
||||
context.commandScope.path = { 10u, panelOwnerId, 30u };
|
||||
context.commandScope.windowId = 10u;
|
||||
context.commandScope.panelId = panelOwnerId;
|
||||
context.commandScope.widgetId = 30u;
|
||||
return context;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, ValidationRejectsUnknownCommandAndConflictingChord) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("missing.command", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto validation = manager.ValidateConfiguration();
|
||||
EXPECT_EQ(validation.code, UIEditorShortcutManagerValidationCode::UnknownCommandId);
|
||||
|
||||
manager = UIEditorShortcutManager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
manager.RegisterBinding(MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
validation = manager.ValidateConfiguration();
|
||||
EXPECT_EQ(validation.code, UIEditorShortcutManagerValidationCode::ConflictingBinding);
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchUsesActivePanelSourceForWorkspaceCommand) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::NewEditor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
const auto result =
|
||||
manager.Dispatch(MakeCtrlKeyEvent(KeyCode::H), {}, controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Dispatched);
|
||||
EXPECT_TRUE(result.commandExecuted);
|
||||
EXPECT_EQ(result.commandId, "workspace.hide_active");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchPrefersPanelScopeBindingOverGlobalBinding) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.reset", UIShortcutScope::Global, 0u, KeyCode::P));
|
||||
manager.RegisterBinding(MakeBinding("workspace.activate_details", UIShortcutScope::Panel, 20u, KeyCode::P));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::NewEditor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
const auto result = manager.Dispatch(
|
||||
MakeCtrlKeyEvent(KeyCode::P),
|
||||
BuildPanelShortcutContext(20u),
|
||||
controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Dispatched);
|
||||
EXPECT_EQ(result.commandId, "workspace.activate_details");
|
||||
EXPECT_EQ(result.commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
|
||||
}
|
||||
|
||||
TEST(UIEditorShortcutManagerTest, DispatchSuppressesMatchedShortcutWhenTextInputIsActive) {
|
||||
UIEditorShortcutManager manager(BuildCommandRegistry());
|
||||
manager.RegisterBinding(MakeBinding("workspace.hide_active", UIShortcutScope::Global, 0u, KeyCode::H));
|
||||
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(
|
||||
XCEngine::NewEditor::UIEditorPanelRegistry{
|
||||
{
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "details", "Details", {}, true, true, true }
|
||||
}
|
||||
},
|
||||
BuildWorkspace());
|
||||
|
||||
UIShortcutContext context = {};
|
||||
context.textInputActive = true;
|
||||
|
||||
const auto result =
|
||||
manager.Dispatch(MakeCtrlKeyEvent(KeyCode::H), context, controller);
|
||||
|
||||
EXPECT_EQ(result.status, UIEditorShortcutDispatchStatus::Suppressed);
|
||||
EXPECT_FALSE(result.commandExecuted);
|
||||
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
|
||||
}
|
||||
Reference in New Issue
Block a user