mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 06:21:52 +08:00
814 lines
27 KiB
C#
814 lines
27 KiB
C#
using System.Reflection;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuInteractionHandlerTests
|
|
{
|
|
static NavMenuInteractionHandlerTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Layout_Induced_Pointer_Exit_Does_Not_Schedule_Submenu_Close()
|
|
{
|
|
var fixture = CreateScrollableHorizontalMenuFixture();
|
|
var delayedRuns = new List<ScheduledRun>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (action, _) =>
|
|
{
|
|
var run = new ScheduledRun(action);
|
|
delayedRuns.Add(run);
|
|
return run;
|
|
});
|
|
|
|
try
|
|
{
|
|
fixture.Window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
ReplaceInteractionHandler(fixture.Menu, handler);
|
|
|
|
var item = fixture.Menu.ContainerFromItem(fixture.Submenu).ShouldBeOfType<NavMenuItem>();
|
|
item.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
item.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
fixture.ScrollViewer.Offset = new Vector(0, 20);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
RaisePointerExited(item, fixture.Window, timestamp: 0);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
delayedRuns.Count.ShouldBe(0);
|
|
item.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
fixture.Window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Move_Exit_Schedules_Submenu_Close()
|
|
{
|
|
var fixture = CreateScrollableHorizontalMenuFixture();
|
|
var delayedRuns = new List<ScheduledRun>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (action, _) =>
|
|
{
|
|
var run = new ScheduledRun(action);
|
|
delayedRuns.Add(run);
|
|
return run;
|
|
});
|
|
|
|
try
|
|
{
|
|
fixture.Window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
ReplaceInteractionHandler(fixture.Menu, handler);
|
|
|
|
var item = fixture.Menu.ContainerFromItem(fixture.Submenu).ShouldBeOfType<NavMenuItem>();
|
|
item.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
item.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
RaisePointerExited(item, fixture.Window, timestamp: 42);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
delayedRuns.Count.ShouldBe(1);
|
|
delayedRuns[0].Run();
|
|
Dispatcher.UIThread.RunJobs();
|
|
item.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
fixture.Window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_An_Item_Disposes_Its_Pending_Delayed_Open()
|
|
{
|
|
var fixture = CreateScrollableHorizontalMenuFixture();
|
|
var delayedRuns = new List<ScheduledRun>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (action, _) =>
|
|
{
|
|
var run = new ScheduledRun(action);
|
|
delayedRuns.Add(run);
|
|
return run;
|
|
});
|
|
|
|
try
|
|
{
|
|
fixture.Window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
ReplaceInteractionHandler(fixture.Menu, handler);
|
|
|
|
var item = fixture.Menu.ContainerFromItem(fixture.Submenu).ShouldBeOfType<NavMenuItem>();
|
|
RaisePointerEntered(item, fixture.Window, timestamp: 42);
|
|
Dispatcher.UIThread.RunJobs();
|
|
delayedRuns.Count.ShouldBe(1);
|
|
delayedRuns[0].IsDisposed.ShouldBeFalse();
|
|
|
|
fixture.Menu.Items.Remove(fixture.Submenu);
|
|
Dispatcher.UIThread.RunJobs();
|
|
fixture.Window.UpdateLayout();
|
|
|
|
delayedRuns[0].IsDisposed.ShouldBeTrue(
|
|
"A delayed hover action must not retain or later act on a recycled container.");
|
|
}
|
|
finally
|
|
{
|
|
fixture.Window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_An_Item_Disposes_Its_Pending_Delayed_Close()
|
|
{
|
|
var fixture = CreateScrollableHorizontalMenuFixture();
|
|
var delayedRuns = new List<ScheduledRun>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (action, _) =>
|
|
{
|
|
var run = new ScheduledRun(action);
|
|
delayedRuns.Add(run);
|
|
return run;
|
|
});
|
|
|
|
try
|
|
{
|
|
fixture.Window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
ReplaceInteractionHandler(fixture.Menu, handler);
|
|
|
|
var item = fixture.Menu.ContainerFromItem(fixture.Submenu).ShouldBeOfType<NavMenuItem>();
|
|
item.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
RaisePointerExited(item, fixture.Window, timestamp: 42);
|
|
Dispatcher.UIThread.RunJobs();
|
|
delayedRuns.Count.ShouldBe(1);
|
|
delayedRuns[0].IsDisposed.ShouldBeFalse();
|
|
|
|
fixture.Menu.Items.Remove(fixture.Submenu);
|
|
Dispatcher.UIThread.RunJobs();
|
|
fixture.Window.UpdateLayout();
|
|
|
|
delayedRuns[0].IsDisposed.ShouldBeTrue(
|
|
"A delayed close action must not retain or later act on a recycled container.");
|
|
}
|
|
finally
|
|
{
|
|
fixture.Window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Enter_On_A_Grouped_Leaf_Closes_An_Open_Item_In_Another_Root_Group()
|
|
{
|
|
var openNode = CreateSubmenuNode("Open");
|
|
var leaf = new NavMenuNode { Header = "Leaf" };
|
|
var firstGroup = new NavMenuGroup { Header = "First group" };
|
|
firstGroup.Entries.Add(openNode);
|
|
var secondGroup = new NavMenuGroup { Header = "Second group" };
|
|
secondGroup.Entries.Add(leaf);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(firstGroup);
|
|
menu.Items.Add(secondGroup);
|
|
var overlay = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(overlay);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = overlay };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var openContainer = menu.ContainerFromItem(firstGroup)
|
|
.ShouldBeOfType<NavMenuGroupItem>()
|
|
.ContainerFromItem(openNode)
|
|
.ShouldBeOfType<NavMenuItem>();
|
|
var leafContainer = menu.ContainerFromItem(secondGroup)
|
|
.ShouldBeOfType<NavMenuGroupItem>()
|
|
.ContainerFromItem(leaf)
|
|
.ShouldBeOfType<NavMenuItem>();
|
|
|
|
openContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
openContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
RaisePointerEntered(leafContainer, window, timestamp: 42);
|
|
RunDispatcherJobsUntil(() => !openContainer.IsSubMenuOpen);
|
|
|
|
openContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Layout_Induced_Pointer_Enter_Does_Not_Schedule_Submenu_Open()
|
|
{
|
|
var fixture = CreateScrollableHorizontalMenuFixture();
|
|
var delayedRuns = new List<ScheduledRun>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (action, _) =>
|
|
{
|
|
var run = new ScheduledRun(action);
|
|
delayedRuns.Add(run);
|
|
return run;
|
|
});
|
|
|
|
try
|
|
{
|
|
fixture.Window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
ReplaceInteractionHandler(fixture.Menu, handler);
|
|
|
|
var item = fixture.Menu.ContainerFromItem(fixture.Submenu).ShouldBeOfType<NavMenuItem>();
|
|
item.IsSubMenuOpen.ShouldBeFalse();
|
|
|
|
fixture.ScrollViewer.Offset = new Vector(0, 20);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
RaisePointerEntered(item, fixture.Window, timestamp: 0);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
delayedRuns.Count.ShouldBe(0);
|
|
item.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
fixture.Window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Default_Handler_Detach_Disposes_Pending_Delayed_Open()
|
|
{
|
|
var scheduledRuns = new List<TrackingDisposable>();
|
|
var handler = new DefaultNavMenuInteractionHandler(null, (_, _) =>
|
|
{
|
|
var disposable = new TrackingDisposable();
|
|
scheduledRuns.Add(disposable);
|
|
return disposable;
|
|
});
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu();
|
|
|
|
handler.AttachCore(menu);
|
|
handler.OpenWithDelay(new NavMenuItem());
|
|
|
|
scheduledRuns.Count.ShouldBe(1);
|
|
scheduledRuns[0].IsDisposed.ShouldBeFalse();
|
|
|
|
handler.DetachCore(menu);
|
|
|
|
scheduledRuns[0].IsDisposed.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_The_Pressed_Item_Invalidates_It_Before_Pointer_Release()
|
|
{
|
|
var first = new NavMenuNode { Header = "First", ItemKey = "first" };
|
|
var second = new NavMenuNode { Header = "Second", ItemKey = "second" };
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
var clickedItems = new List<INavMenuItem>();
|
|
menu.NavMenuItemClick += (_, args) => clickedItems.Add(args.NavMenuItem);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
var header = firstContainer.ItemHeader.ShouldNotBeNull();
|
|
var point = header.TranslatePoint(
|
|
new Point(header.Bounds.Width / 2, header.Bounds.Height / 2),
|
|
window).ShouldNotBeNull();
|
|
|
|
window.MouseMove(point);
|
|
window.MouseDown(point, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.Items.Remove(first);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
window.MouseUp(point, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
clickedItems.ShouldBeEmpty(
|
|
"Releasing after the pressed container was recycled must not click the removed entry.");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Accordion_Closes_A_Previously_Open_Top_Level_Item_Through_Groups()
|
|
{
|
|
var first = CreateSubmenuNode("First");
|
|
var second = CreateSubmenuNode("Second");
|
|
var firstGroup = new NavMenuGroup { Header = "First group" };
|
|
firstGroup.Entries.Add(first);
|
|
var secondGroup = new NavMenuGroup { Header = "Second group" };
|
|
secondGroup.Entries.Add(second);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsAccordionMode = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(firstGroup);
|
|
menu.Items.Add(new NavMenuDivider());
|
|
menu.Items.Add(secondGroup);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = menu };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var firstContainer = menu.ContainerFromItem(firstGroup)
|
|
.ShouldBeOfType<NavMenuGroupItem>()
|
|
.ContainerFromItem(first)
|
|
.ShouldBeOfType<NavMenuItem>();
|
|
var secondContainer = menu.ContainerFromItem(secondGroup)
|
|
.ShouldBeOfType<NavMenuGroupItem>()
|
|
.ContainerFromItem(second)
|
|
.ShouldBeOfType<NavMenuItem>();
|
|
|
|
firstContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
firstContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
secondContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
secondContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
firstContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Vertical_Grouped_Sibling_Submenus_Remain_Mutually_Exclusive()
|
|
{
|
|
var first = CreateSubmenuNode("First");
|
|
var second = CreateSubmenuNode("Second");
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(first);
|
|
group.Entries.Add(second);
|
|
var parent = new NavMenuNode { Header = "Parent" };
|
|
parent.Entries.Add(group);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
var overlay = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(overlay);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = overlay };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ExecutePendingContainerLayout(parentContainer);
|
|
var groupContainer = parentContainer.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var firstContainer = groupContainer.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
var secondContainer = groupContainer.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
|
|
|
|
firstContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
firstContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
secondContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
secondContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
firstContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Close_Recursively_Closes_Grouped_Descendant_Submenus()
|
|
{
|
|
var child = CreateSubmenuNode("Child");
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(child);
|
|
var parent = new NavMenuNode { Header = "Parent" };
|
|
parent.Entries.Add(group);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
var visualLayerManager = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = visualLayerManager };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var groupContainer = parentContainer.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var childContainer = groupContainer.ContainerFromItem(child).ShouldBeOfType<NavMenuItem>();
|
|
childContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
childContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
parentContainer.Close();
|
|
RunDispatcherJobsUntil(() => !parentContainer.IsSubMenuOpen);
|
|
|
|
parentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
childContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void InlineCollapsed_TopLevel_Lost_Platform_Focus_Closes_Popups_Without_Clearing_Selection()
|
|
{
|
|
var leaf = new NavMenuNode { Header = "Leaf", ItemKey = "leaf" };
|
|
var child = new NavMenuNode { Header = "Child", ItemKey = "child" };
|
|
child.Children.Add(leaf);
|
|
var parent = new NavMenuNode { Header = "Parent", ItemKey = "parent" };
|
|
parent.Children.Add(child);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsInlineCollapsed = true,
|
|
IsMotionEnabled = false,
|
|
Width = 240
|
|
};
|
|
menu.Items.Add(parent);
|
|
var visualLayerManager = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = visualLayerManager
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ExecutePendingContainerLayout(parentContainer);
|
|
|
|
var childContainer = parentContainer.ContainerFromItem(child).ShouldBeOfType<NavMenuItem>();
|
|
childContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ExecutePendingContainerLayout(childContainer);
|
|
|
|
var leafContainer = childContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
parentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
childContainer.IsInSelectedPath.ShouldBeTrue();
|
|
|
|
var handler = menu.InteractionHandler.ShouldBeOfType<DefaultNavMenuInteractionHandler>();
|
|
var lostFocusMethod = typeof(DefaultNavMenuInteractionHandler).GetMethod(
|
|
"TopLevelLostPlatformFocus",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
lostFocusMethod.ShouldNotBeNull();
|
|
lostFocusMethod.Invoke(handler, null);
|
|
RunDispatcherJobsUntil(() => !parentContainer.IsSubMenuOpen);
|
|
|
|
parentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
parentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ExecutePendingContainerLayout(parentContainer);
|
|
|
|
var reopenedChildContainer = parentContainer.ContainerFromItem(child).ShouldBeOfType<NavMenuItem>();
|
|
reopenedChildContainer.IsInSelectedPath.ShouldBeTrue();
|
|
reopenedChildContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ExecutePendingContainerLayout(reopenedChildContainer);
|
|
|
|
var reopenedLeafContainer = reopenedChildContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
reopenedLeafContainer.IsSelected.ShouldBeTrue();
|
|
parentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Public_Close_Clears_Selection_While_Internal_Popup_Close_Does_Not()
|
|
{
|
|
var leaf = new NavMenuNode { Header = "Leaf", ItemKey = "leaf" };
|
|
var parent = new NavMenuNode { Header = "Parent", ItemKey = "parent" };
|
|
parent.Children.Add(leaf);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
var visualLayerManager = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = visualLayerManager };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var leafContainer = parentContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.Close();
|
|
RunDispatcherJobsUntil(() => !parentContainer.IsSubMenuOpen);
|
|
|
|
menu.SelectedItem.ShouldBeNull();
|
|
parentContainer.IsInSelectedPath.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Mode_Change_Closes_Popup_Without_Clearing_Selection()
|
|
{
|
|
var leaf = new NavMenuNode { Header = "Leaf", ItemKey = "leaf" };
|
|
var parent = new NavMenuNode { Header = "Parent", ItemKey = "parent" };
|
|
parent.Children.Add(leaf);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
var visualLayerManager = new VisualLayerManager { Child = menu };
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
var window = new AvaloniaWindow { Width = 320, Height = 240, Content = visualLayerManager };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var leafContainer = parentContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(leafContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.Mode = NavMenuMode.Horizontal;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(leaf);
|
|
var modeChangedParentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
modeChangedParentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
modeChangedParentContainer.IsInSelectedPath.ShouldBeTrue();
|
|
|
|
modeChangedParentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
modeChangedParentContainer.ContainerFromItem(leaf)
|
|
.ShouldBeOfType<NavMenuItem>()
|
|
.IsSelected.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
|
|
private static MenuFixture CreateScrollableHorizontalMenuFixture()
|
|
{
|
|
var submenu = new NavMenuNode
|
|
{
|
|
Header = "Parent"
|
|
};
|
|
submenu.Children.Add(new NavMenuNode
|
|
{
|
|
Header = "Child"
|
|
});
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Horizontal,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(submenu);
|
|
|
|
var stackPanel = new StackPanel();
|
|
stackPanel.Children.Add(new Border
|
|
{
|
|
Height = 80
|
|
});
|
|
stackPanel.Children.Add(menu);
|
|
stackPanel.Children.Add(new Border
|
|
{
|
|
Height = 600
|
|
});
|
|
var scrollViewer = new ScrollViewer
|
|
{
|
|
Height = 240,
|
|
Content = stackPanel
|
|
};
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = scrollViewer
|
|
};
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 240,
|
|
Content = visualLayerManager
|
|
};
|
|
|
|
return new MenuFixture(window, scrollViewer, menu, submenu);
|
|
}
|
|
|
|
private static NavMenuNode CreateSubmenuNode(string header)
|
|
{
|
|
var node = new NavMenuNode { Header = header };
|
|
node.Children.Add(new NavMenuNode { Header = $"{header} child" });
|
|
return node;
|
|
}
|
|
|
|
private static void RunDispatcherJobsUntil(Func<bool> condition, int maxPasses = 128)
|
|
{
|
|
for (var i = 0; i < maxPasses && !condition(); i++)
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
private static void ReplaceInteractionHandler(
|
|
AtomUI.Desktop.Controls.NavMenu menu,
|
|
DefaultNavMenuInteractionHandler handler)
|
|
{
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.InteractionHandler!.Detach(menu);
|
|
|
|
var property = typeof(AtomUI.Desktop.Controls.NavMenu).GetProperty(
|
|
"InteractionHandler",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(menu, handler);
|
|
handler.AttachCore(menu);
|
|
}
|
|
|
|
private static void RaisePointerExited(NavMenuItem item, AvaloniaWindow window, ulong timestamp)
|
|
{
|
|
RaisePointerTransition(InputElement.PointerExitedEvent, item, window, timestamp);
|
|
}
|
|
|
|
private static void RaisePointerEntered(NavMenuItem item, AvaloniaWindow window, ulong timestamp)
|
|
{
|
|
RaisePointerTransition(InputElement.PointerEnteredEvent, item, window, timestamp);
|
|
}
|
|
|
|
private static void RaisePointerTransition(
|
|
RoutedEvent routedEvent,
|
|
NavMenuItem item,
|
|
AvaloniaWindow window,
|
|
ulong timestamp)
|
|
{
|
|
var pointer = new Avalonia.Input.Pointer(
|
|
Avalonia.Input.Pointer.GetNextFreeId(),
|
|
PointerType.Mouse,
|
|
true);
|
|
var position = item.TranslatePoint(
|
|
new Point(item.Bounds.Width + 1, item.Bounds.Height + 1),
|
|
window) ?? default;
|
|
|
|
var args = new PointerEventArgs(
|
|
routedEvent,
|
|
item,
|
|
pointer,
|
|
window,
|
|
position,
|
|
timestamp,
|
|
PointerPointProperties.None,
|
|
KeyModifiers.None);
|
|
|
|
item.RaiseEvent(args);
|
|
}
|
|
|
|
private sealed record MenuFixture(
|
|
AvaloniaWindow Window,
|
|
ScrollViewer ScrollViewer,
|
|
AtomUI.Desktop.Controls.NavMenu Menu,
|
|
NavMenuNode Submenu);
|
|
|
|
private sealed class ScheduledRun : IDisposable
|
|
{
|
|
private readonly Action _action;
|
|
|
|
public ScheduledRun(Action action)
|
|
{
|
|
_action = action;
|
|
}
|
|
|
|
public bool IsDisposed { get; private set; }
|
|
|
|
public void Run()
|
|
{
|
|
if (!IsDisposed)
|
|
{
|
|
_action();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
IsDisposed = true;
|
|
}
|
|
}
|
|
|
|
private sealed class TrackingDisposable : IDisposable
|
|
{
|
|
public bool IsDisposed { get; private set; }
|
|
|
|
public void Dispose()
|
|
{
|
|
IsDisposed = true;
|
|
}
|
|
}
|
|
}
|