Harden shell interaction modal recovery
This commit is contained in:
@@ -137,7 +137,10 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
|
|||||||
|
|
||||||
const UIEditorResolvedMenuItem* item =
|
const UIEditorResolvedMenuItem* item =
|
||||||
FindResolvedMenuItemRecursive(menu->items, popupState.itemId);
|
FindResolvedMenuItemRecursive(menu->items, popupState.itemId);
|
||||||
if (item == nullptr || item->kind != UIEditorMenuItemKind::Submenu) {
|
if (item == nullptr ||
|
||||||
|
item->kind != UIEditorMenuItemKind::Submenu ||
|
||||||
|
!item->enabled ||
|
||||||
|
item->children.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
|
|||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||||
|
using XCEngine::UI::Editor::FindUIEditorWorkspacePanelPresentationState;
|
||||||
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionRequest;
|
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionRequest;
|
||||||
using XCEngine::UI::Editor::UIEditorCommandDispatchStatus;
|
using XCEngine::UI::Editor::UIEditorCommandDispatchStatus;
|
||||||
using XCEngine::UI::Editor::UIEditorCommandDispatcher;
|
using XCEngine::UI::Editor::UIEditorCommandDispatcher;
|
||||||
@@ -479,6 +480,69 @@ TEST(UIEditorShellInteractionTest, DisabledSubmenuDoesNotOpenChildPopup) {
|
|||||||
EXPECT_TRUE(state.menuSession.HasOpenMenu());
|
EXPECT_TRUE(state.menuSession.HasOpenMenu());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorShellInteractionTest, HoverDisabledSubmenuSiblingClosesOnlyOpenChildChain) {
|
||||||
|
auto controller = BuildController();
|
||||||
|
auto model = BuildInteractionModel();
|
||||||
|
UIEditorResolvedMenuItem disabledSibling = {};
|
||||||
|
disabledSibling.kind = UIEditorMenuItemKind::Submenu;
|
||||||
|
disabledSibling.itemId = "file-disabled-tools";
|
||||||
|
disabledSibling.label = "Disabled Tools";
|
||||||
|
disabledSibling.enabled = false;
|
||||||
|
disabledSibling.children = {
|
||||||
|
UIEditorResolvedMenuItem{
|
||||||
|
UIEditorMenuItemKind::Command,
|
||||||
|
"file-disabled-child",
|
||||||
|
"Disabled Child",
|
||||||
|
"workspace.close",
|
||||||
|
"Disabled Child",
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
model.resolvedMenuModel.menus[0].items.push_back(disabledSibling);
|
||||||
|
|
||||||
|
UIEditorShellInteractionState state = {};
|
||||||
|
auto frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) });
|
||||||
|
const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools");
|
||||||
|
const auto* disabledSiblingItem = FindPopupItem(frame, "file-disabled-tools");
|
||||||
|
ASSERT_NE(submenuItem, nullptr);
|
||||||
|
ASSERT_NE(disabledSiblingItem, nullptr);
|
||||||
|
const UIRect submenuRect = submenuItem->rect;
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakePointerMove(RectCenter(submenuRect)) });
|
||||||
|
ASSERT_EQ(frame.request.popupRequests.size(), 2u);
|
||||||
|
const std::string childPopupId = frame.request.popupRequests[1].popupId;
|
||||||
|
disabledSiblingItem = FindPopupItem(frame, "file-disabled-tools");
|
||||||
|
ASSERT_NE(disabledSiblingItem, nullptr);
|
||||||
|
const UIRect disabledSiblingRect = disabledSiblingItem->rect;
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakePointerMove(RectCenter(disabledSiblingRect)) });
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.menuMutation.changed);
|
||||||
|
ASSERT_EQ(frame.result.menuMutation.closedPopupIds.size(), 1u);
|
||||||
|
EXPECT_EQ(frame.result.menuMutation.closedPopupIds.front(), childPopupId);
|
||||||
|
EXPECT_EQ(frame.openRootMenuId, "file");
|
||||||
|
ASSERT_EQ(frame.request.popupRequests.size(), 1u);
|
||||||
|
EXPECT_EQ(frame.request.popupRequests.front().menuId, "file");
|
||||||
|
EXPECT_TRUE(state.menuSession.HasOpenMenu());
|
||||||
|
}
|
||||||
|
|
||||||
TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) {
|
TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) {
|
||||||
auto controller = BuildController();
|
auto controller = BuildController();
|
||||||
const auto model = BuildInteractionModel();
|
const auto model = BuildInteractionModel();
|
||||||
@@ -515,6 +579,77 @@ TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) {
|
|||||||
EXPECT_TRUE(frame.request.popupRequests.empty());
|
EXPECT_TRUE(frame.request.popupRequests.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorShellInteractionTest, FocusLostWhileMenuOpenReleasesExistingViewportCapture) {
|
||||||
|
auto controller = BuildController();
|
||||||
|
const auto model = BuildInteractionModel();
|
||||||
|
|
||||||
|
UIEditorShellInteractionState state = {};
|
||||||
|
auto frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{});
|
||||||
|
const auto* fileButton = FindMenuButton(frame, "file");
|
||||||
|
ASSERT_NE(fileButton, nullptr);
|
||||||
|
const UIRect fileButtonRect = fileButton->rect;
|
||||||
|
ASSERT_FALSE(frame.shellFrame.workspaceFrame.viewportFrames.empty());
|
||||||
|
|
||||||
|
const UIRect inputRect =
|
||||||
|
frame.shellFrame.workspaceFrame.viewportFrames.front().viewportShellFrame.slotLayout.inputRect;
|
||||||
|
const UIPoint center = RectCenter(inputRect);
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
MakePointerMove(center),
|
||||||
|
MakeLeftPointerDown(center)
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(frame.result.requestPointerCapture);
|
||||||
|
const auto* sceneStateBeforeMenu = FindUIEditorWorkspacePanelPresentationState(
|
||||||
|
state.workspaceInteractionState.composeState,
|
||||||
|
"scene");
|
||||||
|
ASSERT_NE(sceneStateBeforeMenu, nullptr);
|
||||||
|
ASSERT_TRUE(sceneStateBeforeMenu->viewportShellState.inputBridgeState.captured);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakeLeftPointerDown(RectCenter(fileButtonRect)) });
|
||||||
|
ASSERT_TRUE(state.menuSession.HasOpenMenu());
|
||||||
|
const auto* sceneStateWithMenu = FindUIEditorWorkspacePanelPresentationState(
|
||||||
|
state.workspaceInteractionState.composeState,
|
||||||
|
"scene");
|
||||||
|
ASSERT_NE(sceneStateWithMenu, nullptr);
|
||||||
|
ASSERT_TRUE(sceneStateWithMenu->viewportShellState.inputBridgeState.captured);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakeFocusLost() });
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.consumed);
|
||||||
|
EXPECT_TRUE(frame.result.menuMutation.changed);
|
||||||
|
EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::FocusLoss);
|
||||||
|
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||||
|
EXPECT_TRUE(frame.result.workspaceResult.releasePointerCapture);
|
||||||
|
EXPECT_EQ(frame.result.viewportPanelId, "scene");
|
||||||
|
EXPECT_TRUE(frame.result.viewportInputFrame.focusLost);
|
||||||
|
EXPECT_TRUE(frame.result.viewportInputFrame.captureEnded);
|
||||||
|
EXPECT_FALSE(state.menuSession.HasOpenMenu());
|
||||||
|
const auto* sceneStateAfterFocusLoss = FindUIEditorWorkspacePanelPresentationState(
|
||||||
|
state.workspaceInteractionState.composeState,
|
||||||
|
"scene");
|
||||||
|
ASSERT_NE(sceneStateAfterFocusLoss, nullptr);
|
||||||
|
EXPECT_FALSE(sceneStateAfterFocusLoss->viewportShellState.inputBridgeState.captured);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(UIEditorShellInteractionTest, OpenMenuConsumesWorkspacePointerDownForThatFrame) {
|
TEST(UIEditorShellInteractionTest, OpenMenuConsumesWorkspacePointerDownForThatFrame) {
|
||||||
auto controller = BuildController();
|
auto controller = BuildController();
|
||||||
const auto model = BuildInteractionModel();
|
const auto model = BuildInteractionModel();
|
||||||
@@ -648,3 +783,77 @@ TEST(UIEditorShellInteractionTest, InvalidResolvedMenuStateClosesInvisibleModalC
|
|||||||
EXPECT_FALSE(state.menuSession.HasOpenMenu());
|
EXPECT_FALSE(state.menuSession.HasOpenMenu());
|
||||||
EXPECT_TRUE(frame.request.popupRequests.empty());
|
EXPECT_TRUE(frame.request.popupRequests.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorShellInteractionTest, OpenedSubmenuClosesWhenSourceItemBecomesDisabled) {
|
||||||
|
auto controller = BuildController();
|
||||||
|
auto model = BuildInteractionModel();
|
||||||
|
|
||||||
|
UIEditorShellInteractionState state = {};
|
||||||
|
auto frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) });
|
||||||
|
const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools");
|
||||||
|
ASSERT_NE(submenuItem, nullptr);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakePointerMove(RectCenter(submenuItem->rect)) });
|
||||||
|
ASSERT_EQ(frame.request.popupRequests.size(), 2u);
|
||||||
|
|
||||||
|
model.resolvedMenuModel.menus[0].items[0].enabled = false;
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{});
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.consumed);
|
||||||
|
EXPECT_TRUE(frame.result.menuMutation.changed);
|
||||||
|
EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::Programmatic);
|
||||||
|
EXPECT_FALSE(state.menuSession.HasOpenMenu());
|
||||||
|
EXPECT_TRUE(frame.request.popupRequests.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorShellInteractionTest, OpenedSubmenuClosesWhenSourceItemLosesChildren) {
|
||||||
|
auto controller = BuildController();
|
||||||
|
auto model = BuildInteractionModel();
|
||||||
|
|
||||||
|
UIEditorShellInteractionState state = {};
|
||||||
|
auto frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) });
|
||||||
|
const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools");
|
||||||
|
ASSERT_NE(submenuItem, nullptr);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{ MakePointerMove(RectCenter(submenuItem->rect)) });
|
||||||
|
ASSERT_EQ(frame.request.popupRequests.size(), 2u);
|
||||||
|
|
||||||
|
model.resolvedMenuModel.menus[0].items[0].children.clear();
|
||||||
|
frame = UpdateUIEditorShellInteraction(
|
||||||
|
state,
|
||||||
|
controller,
|
||||||
|
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
|
||||||
|
model,
|
||||||
|
{});
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.consumed);
|
||||||
|
EXPECT_TRUE(frame.result.menuMutation.changed);
|
||||||
|
EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::Programmatic);
|
||||||
|
EXPECT_FALSE(state.menuSession.HasOpenMenu());
|
||||||
|
EXPECT_TRUE(frame.request.popupRequests.empty());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user