21 changed files with 1447 additions and 338 deletions
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a <see cref="Menu"/> or <see cref="ContextMenu"/>.
|
|||
/// </summary>
|
|||
public interface IMenu : IMenuElement |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the menu interaction handler.
|
|||
/// </summary>
|
|||
IMenuInteractionHandler InteractionHandler { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the menu is open.
|
|||
/// </summary>
|
|||
bool IsOpen { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an <see cref="IMenu"/> or <see cref="IMenuItem"/>.
|
|||
/// </summary>
|
|||
public interface IMenuElement : IControl |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the currently selected submenu item.
|
|||
/// </summary>
|
|||
IMenuItem SelectedItem { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the submenu items.
|
|||
/// </summary>
|
|||
IEnumerable<IMenuItem> SubItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Opens the menu or menu item.
|
|||
/// </summary>
|
|||
void Open(); |
|||
|
|||
/// <summary>
|
|||
/// Closes the menu or menu item.
|
|||
/// </summary>
|
|||
void Close(); |
|||
|
|||
/// <summary>
|
|||
/// Moves the submenu selection in the specified direction.
|
|||
/// </summary>
|
|||
/// <param name="direction">The direction.</param>
|
|||
/// <param name="wrap">Whether to wrap after the first or last item.</param>
|
|||
/// <returns>True if the selection was moved; otherwise false.</returns>
|
|||
bool MoveSelection(NavigationDirection direction, bool wrap); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a <see cref="MenuItem"/>.
|
|||
/// </summary>
|
|||
public interface IMenuItem : IMenuElement |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates whether the item has a submenu.
|
|||
/// </summary>
|
|||
bool HasSubMenu { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the mouse is currently over the menu item's submenu.
|
|||
/// </summary>
|
|||
bool IsPointerOverSubMenu { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
|
|||
/// open.
|
|||
/// </summary>
|
|||
bool IsSubMenuOpen { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
|
|||
/// </summary>
|
|||
bool IsTopLevel { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the parent <see cref="IMenuElement"/>.
|
|||
/// </summary>
|
|||
new IMenuElement Parent { get; } |
|||
|
|||
/// <summary>
|
|||
/// Raises a click event on the menu item.
|
|||
/// </summary>
|
|||
void RaiseClick(); |
|||
} |
|||
} |
|||
@ -0,0 +1,457 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
/// <summary>
|
|||
/// Provides the default keyboard and pointer interaction for menus.
|
|||
/// </summary>
|
|||
public class DefaultMenuInteractionHandler : IMenuInteractionHandler |
|||
{ |
|||
private IDisposable _inputManagerSubscription; |
|||
private IRenderRoot _root; |
|||
|
|||
public DefaultMenuInteractionHandler() |
|||
: this(Input.InputManager.Instance, DefaultDelayRun) |
|||
{ |
|||
} |
|||
|
|||
public DefaultMenuInteractionHandler( |
|||
IInputManager inputManager, |
|||
Action<Action, TimeSpan> delayRun) |
|||
{ |
|||
InputManager = inputManager; |
|||
DelayRun = delayRun; |
|||
} |
|||
|
|||
public virtual void Attach(IMenu menu) |
|||
{ |
|||
if (Menu != null) |
|||
{ |
|||
throw new NotSupportedException("DefaultMenuInteractionHandler is already attached."); |
|||
} |
|||
|
|||
Menu = menu; |
|||
Menu.GotFocus += GotFocus; |
|||
Menu.LostFocus += LostFocus; |
|||
Menu.KeyDown += KeyDown; |
|||
Menu.PointerPressed += PointerPressed; |
|||
Menu.PointerReleased += PointerReleased; |
|||
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); |
|||
Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); |
|||
Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); |
|||
Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); |
|||
|
|||
_root = Menu.VisualRoot; |
|||
|
|||
if (_root is InputElement inputRoot) |
|||
{ |
|||
inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel); |
|||
} |
|||
|
|||
if (_root is WindowBase window) |
|||
{ |
|||
window.Deactivated += WindowDeactivated; |
|||
} |
|||
|
|||
_inputManagerSubscription = InputManager.Process.Subscribe(RawInput); |
|||
} |
|||
|
|||
public virtual void Detach(IMenu menu) |
|||
{ |
|||
if (Menu != menu) |
|||
{ |
|||
throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu."); |
|||
} |
|||
|
|||
Menu.GotFocus -= GotFocus; |
|||
Menu.LostFocus -= LostFocus; |
|||
Menu.KeyDown -= KeyDown; |
|||
Menu.PointerPressed -= PointerPressed; |
|||
Menu.PointerReleased -= PointerReleased; |
|||
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); |
|||
Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); |
|||
Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); |
|||
Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); |
|||
|
|||
if (_root is InputElement inputRoot) |
|||
{ |
|||
inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed); |
|||
} |
|||
|
|||
if (_root is WindowBase root) |
|||
{ |
|||
root.Deactivated -= WindowDeactivated; |
|||
} |
|||
|
|||
_inputManagerSubscription.Dispose(); |
|||
|
|||
Menu = null; |
|||
_root = null; |
|||
} |
|||
|
|||
protected Action<Action, TimeSpan> DelayRun { get; } |
|||
|
|||
protected IInputManager InputManager { get; } |
|||
|
|||
protected IMenu Menu { get; private set; } |
|||
|
|||
protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); |
|||
|
|||
protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item?.Parent != null) |
|||
{ |
|||
item.SelectedItem = item; |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void LostFocus(object sender, RoutedEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item != null) |
|||
{ |
|||
item.SelectedItem = null; |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void KeyDown(object sender, KeyEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item != null) |
|||
{ |
|||
KeyDown(item, e); |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) |
|||
{ |
|||
switch (e.Key) |
|||
{ |
|||
case Key.Up: |
|||
case Key.Down: |
|||
if (item.IsTopLevel) |
|||
{ |
|||
if (item.HasSubMenu && !item.IsSubMenuOpen) |
|||
{ |
|||
Open(item, true); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
goto default; |
|||
} |
|||
break; |
|||
|
|||
case Key.Left: |
|||
if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) |
|||
{ |
|||
parent.Close(); |
|||
parent.Focus(); |
|||
e.Handled = true; |
|||
} |
|||
else |
|||
{ |
|||
goto default; |
|||
} |
|||
break; |
|||
|
|||
case Key.Right: |
|||
if (!item.IsTopLevel && item.HasSubMenu) |
|||
{ |
|||
Open(item, true); |
|||
e.Handled = true; |
|||
} |
|||
else |
|||
{ |
|||
goto default; |
|||
} |
|||
break; |
|||
|
|||
case Key.Enter: |
|||
if (!item.HasSubMenu) |
|||
{ |
|||
Click(item); |
|||
} |
|||
else |
|||
{ |
|||
Open(item, true); |
|||
} |
|||
|
|||
e.Handled = true; |
|||
break; |
|||
|
|||
case Key.Escape: |
|||
if (item.Parent != null) |
|||
{ |
|||
item.Parent.Close(); |
|||
item.Parent.Focus(); |
|||
e.Handled = true; |
|||
} |
|||
break; |
|||
|
|||
default: |
|||
var direction = e.Key.ToNavigationDirection(); |
|||
|
|||
if (direction.HasValue) |
|||
{ |
|||
if (item.Parent?.MoveSelection(direction.Value, true) == true) |
|||
{ |
|||
// If the the parent is an IMenu which successfully moved its selection,
|
|||
// and the current menu is open then close the current menu and open the
|
|||
// new menu.
|
|||
if (item.IsSubMenuOpen && item.Parent is IMenu) |
|||
{ |
|||
item.Close(); |
|||
Open(item.Parent.SelectedItem, true); |
|||
} |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
if (!e.Handled && item.Parent is IMenuItem parentItem) |
|||
{ |
|||
KeyDown(parentItem, e); |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void AccessKeyPressed(object sender, RoutedEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (item.HasSubMenu) |
|||
{ |
|||
Open(item, true); |
|||
} |
|||
else |
|||
{ |
|||
Click(item); |
|||
} |
|||
|
|||
e.Handled = true; |
|||
} |
|||
|
|||
protected internal virtual void PointerEnter(object sender, PointerEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item?.Parent == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (item.IsTopLevel) |
|||
{ |
|||
if (item.Parent.SelectedItem?.IsSubMenuOpen == true) |
|||
{ |
|||
item.Parent.SelectedItem.Close(); |
|||
SelectItemAndAncestors(item); |
|||
Open(item, false); |
|||
} |
|||
else |
|||
{ |
|||
SelectItemAndAncestors(item); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
SelectItemAndAncestors(item); |
|||
|
|||
if (item.HasSubMenu) |
|||
{ |
|||
OpenWithDelay(item); |
|||
} |
|||
else if (item.Parent != null) |
|||
{ |
|||
foreach (var sibling in item.Parent.SubItems) |
|||
{ |
|||
if (sibling.IsSubMenuOpen) |
|||
{ |
|||
CloseWithDelay(sibling); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void PointerLeave(object sender, PointerEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (item?.Parent == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (item.IsTopLevel) |
|||
{ |
|||
if (!((IMenu)item.Parent).IsOpen && item.Parent.SelectedItem == item) |
|||
{ |
|||
item.Parent.SelectedItem = null; |
|||
} |
|||
} |
|||
else if (!item.HasSubMenu) |
|||
{ |
|||
item.Parent.SelectedItem = null; |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == true) |
|||
{ |
|||
Open(item, false); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void PointerReleased(object sender, PointerReleasedEventArgs e) |
|||
{ |
|||
var item = GetMenuItem(e.Source as IControl); |
|||
|
|||
if (e.MouseButton == MouseButton.Left && item.HasSubMenu == false) |
|||
{ |
|||
Click(item); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void MenuOpened(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (e.Source == Menu) |
|||
{ |
|||
Menu.MoveSelection(NavigationDirection.First, true); |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void RawInput(RawInputEventArgs e) |
|||
{ |
|||
var mouse = e as RawMouseEventArgs; |
|||
|
|||
if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) |
|||
{ |
|||
Menu.Close(); |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void RootPointerPressed(object sender, PointerPressedEventArgs e) |
|||
{ |
|||
if (Menu?.IsOpen == true) |
|||
{ |
|||
var control = e.Source as ILogical; |
|||
|
|||
if (!Menu.IsLogicalParentOf(control)) |
|||
{ |
|||
Menu.Close(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected internal virtual void WindowDeactivated(object sender, EventArgs e) |
|||
{ |
|||
Menu.Close(); |
|||
} |
|||
|
|||
protected void Click(IMenuItem item) |
|||
{ |
|||
item.RaiseClick(); |
|||
CloseMenu(item); |
|||
} |
|||
|
|||
protected void CloseMenu(IMenuItem item) |
|||
{ |
|||
var current = (IMenuElement)item; |
|||
|
|||
while (current != null && !(current is IMenu)) |
|||
{ |
|||
current = (current as IMenuItem).Parent; |
|||
} |
|||
|
|||
current?.Close(); |
|||
} |
|||
|
|||
protected void CloseWithDelay(IMenuItem item) |
|||
{ |
|||
void Execute() |
|||
{ |
|||
if (item.Parent?.SelectedItem != item) |
|||
{ |
|||
item.Close(); |
|||
} |
|||
} |
|||
|
|||
DelayRun(Execute, MenuShowDelay); |
|||
} |
|||
|
|||
protected void Open(IMenuItem item, bool selectFirst) |
|||
{ |
|||
item.Open(); |
|||
|
|||
if (selectFirst) |
|||
{ |
|||
item.MoveSelection(NavigationDirection.First, true); |
|||
} |
|||
} |
|||
|
|||
protected void OpenWithDelay(IMenuItem item) |
|||
{ |
|||
void Execute() |
|||
{ |
|||
if (item.Parent?.SelectedItem == item) |
|||
{ |
|||
Open(item, false); |
|||
} |
|||
} |
|||
|
|||
DelayRun(Execute, MenuShowDelay); |
|||
} |
|||
|
|||
protected void SelectItemAndAncestors(IMenuItem item) |
|||
{ |
|||
var current = item; |
|||
|
|||
while (current?.Parent != null) |
|||
{ |
|||
current.Parent.SelectedItem = current; |
|||
current = current.Parent as IMenuItem; |
|||
} |
|||
} |
|||
|
|||
protected static IMenuItem GetMenuItem(IControl item) |
|||
{ |
|||
while (true) |
|||
{ |
|||
if (item == null) |
|||
return null; |
|||
if (item is IMenuItem menuItem) |
|||
return menuItem; |
|||
item = item.Parent; |
|||
} |
|||
} |
|||
|
|||
private static void DefaultDelayRun(Action action, TimeSpan timeSpan) |
|||
{ |
|||
DispatcherTimer.RunOnce(action, timeSpan); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
/// <summary>
|
|||
/// Handles user interaction for menus.
|
|||
/// </summary>
|
|||
public interface IMenuInteractionHandler |
|||
{ |
|||
/// <summary>
|
|||
/// Attaches the interaction handler to a menu.
|
|||
/// </summary>
|
|||
/// <param name="menu">The menu.</param>
|
|||
void Attach(IMenu menu); |
|||
|
|||
/// <summary>
|
|||
/// Detaches the interaction handler from the attached menu.
|
|||
/// </summary>
|
|||
void Detach(IMenu menu); |
|||
} |
|||
} |
|||
@ -0,0 +1,489 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests.Platform |
|||
{ |
|||
public class DefaultMenuInteractionHandlerTests |
|||
{ |
|||
public class TopLevel |
|||
{ |
|||
[Fact] |
|||
public void Up_Opens_MenuItem_With_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var e = new KeyEventArgs { Key = Key.Up, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Down_Opens_MenuItem_With_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var e = new KeyEventArgs { Key = Key.Down, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Right_Selects_Next_MenuItem() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(x => x.MoveSelection(NavigationDirection.Right, true) == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu); |
|||
var e = new KeyEventArgs { Key = Key.Right, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Right, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Left_Selects_Previous_MenuItem() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(x => x.MoveSelection(NavigationDirection.Left, true) == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu); |
|||
var e = new KeyEventArgs { Key = Key.Left, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Left, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enter_On_Item_With_No_SubMenu_Causes_Click() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu); |
|||
var e = new KeyEventArgs { Key = Key.Enter, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.RaiseClick()); |
|||
Mock.Get(menu).Verify(x => x.Close()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enter_On_Item_With_SubMenu_Opens_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var e = new KeyEventArgs { Key = Key.Enter, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Escape_Closes_Parent_Menu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu); |
|||
var e = new KeyEventArgs { Key = Key.Escape, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(menu).Verify(x => x.Close()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerEnter_Opens_Item_When_Old_Item_Is_Open() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = new Mock<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => |
|||
x.IsSubMenuOpen == true && |
|||
x.IsTopLevel == true && |
|||
x.HasSubMenu == true && |
|||
x.Parent == menu.Object); |
|||
var nextItem = Mock.Of<IMenuItem>(x => |
|||
x.IsTopLevel == true && |
|||
x.HasSubMenu == true && |
|||
x.Parent == menu.Object); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; |
|||
|
|||
menu.SetupGet(x => x.SelectedItem).Returns(item); |
|||
|
|||
target.PointerEnter(nextItem, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Close()); |
|||
menu.VerifySet(x => x.SelectedItem = nextItem); |
|||
Mock.Get(nextItem).Verify(x => x.Open()); |
|||
Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); |
|||
Assert.False(e.Handled); |
|||
|
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerLeave_Deselects_Item_When_Menu_Not_Open() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = new Mock<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; |
|||
|
|||
menu.SetupGet(x => x.SelectedItem).Returns(item); |
|||
target.PointerLeave(item, e); |
|||
|
|||
menu.VerifySet(x => x.SelectedItem = null); |
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = new Mock<IMenu>(); |
|||
var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; |
|||
|
|||
menu.SetupGet(x => x.IsOpen).Returns(true); |
|||
menu.SetupGet(x => x.SelectedItem).Returns(item); |
|||
target.PointerLeave(item, e); |
|||
|
|||
menu.VerifySet(x => x.SelectedItem = null, Times.Never); |
|||
Assert.False(e.Handled); |
|||
} |
|||
} |
|||
|
|||
public class NonTopLevel |
|||
{ |
|||
[Fact] |
|||
public void Up_Selects_Previous_MenuItem() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Up, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Up, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Down_Selects_Next_MenuItem() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Down, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Down, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Left_Closes_Parent_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.HasSubMenu == true && x.IsSubMenuOpen == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Left, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(parentItem).Verify(x => x.Close()); |
|||
Mock.Get(parentItem).Verify(x => x.Focus()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Right_With_SubMenu_Items_Opens_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true); |
|||
var e = new KeyEventArgs { Key = Key.Right, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = new Mock<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => |
|||
x.IsSubMenuOpen == true && |
|||
x.IsTopLevel == true && |
|||
x.HasSubMenu == true && |
|||
x.Parent == menu.Object); |
|||
var nextItem = Mock.Of<IMenuItem>(x => |
|||
x.IsTopLevel == true && |
|||
x.HasSubMenu == true && |
|||
x.Parent == menu.Object); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Right, Source = item }; |
|||
|
|||
menu.Setup(x => x.MoveSelection(NavigationDirection.Right, true)) |
|||
.Callback(() => menu.SetupGet(x => x.SelectedItem).Returns(nextItem)) |
|||
.Returns(true); |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
menu.Verify(x => x.MoveSelection(NavigationDirection.Right, true)); |
|||
Mock.Get(parentItem).Verify(x => x.Close()); |
|||
Mock.Get(nextItem).Verify(x => x.Open()); |
|||
Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enter_On_Item_With_No_SubMenu_Causes_Click() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Enter, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.RaiseClick()); |
|||
Mock.Get(menu).Verify(x => x.Close()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enter_On_Item_With_SubMenu_Opens_SubMenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true); |
|||
var e = new KeyEventArgs { Key = Key.Enter, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Escape_Closes_Parent_MenuItem() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new KeyEventArgs { Key = Key.Escape, Source = item }; |
|||
|
|||
target.KeyDown(item, e); |
|||
|
|||
Mock.Get(parentItem).Verify(x => x.Close()); |
|||
Mock.Get(parentItem).Verify(x => x.Focus()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerEnter_Selects_Item() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; |
|||
|
|||
target.PointerEnter(item, e); |
|||
|
|||
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); |
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerEnter_Opens_Submenu_After_Delay() |
|||
{ |
|||
var timer = new TestTimer(); |
|||
var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; |
|||
|
|||
target.PointerEnter(item, e); |
|||
Mock.Get(item).Verify(x => x.Open(), Times.Never); |
|||
|
|||
timer.Pulse(); |
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
|
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerEnter_Closes_Sibling_Submenu_After_Delay() |
|||
{ |
|||
var timer = new TestTimer(); |
|||
var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; |
|||
|
|||
Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); |
|||
|
|||
target.PointerEnter(item, e); |
|||
Mock.Get(sibling).Verify(x => x.Close(), Times.Never); |
|||
|
|||
timer.Pulse(); |
|||
Mock.Get(sibling).Verify(x => x.Close()); |
|||
|
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerLeave_Deselects_Item() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; |
|||
|
|||
target.PointerLeave(item, e); |
|||
|
|||
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null); |
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); |
|||
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; |
|||
|
|||
target.PointerLeave(item, e); |
|||
|
|||
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never); |
|||
Assert.False(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem); |
|||
var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; |
|||
|
|||
target.PointerReleased(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.RaiseClick()); |
|||
Mock.Get(menu).Verify(x => x.Close()); |
|||
Assert.True(e.Handled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem() |
|||
{ |
|||
var timer = new TestTimer(); |
|||
var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true); |
|||
var childItem = Mock.Of<IMenuItem>(x => x.Parent == item); |
|||
var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; |
|||
var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; |
|||
|
|||
// Pointer enters item; item is selected.
|
|||
target.PointerEnter(item, enter); |
|||
Assert.True(timer.ActionIsQueued); |
|||
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); |
|||
Mock.Get(parentItem).ResetCalls(); |
|||
|
|||
// SubMenu shown after a delay.
|
|||
timer.Pulse(); |
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).SetupGet(x => x.IsSubMenuOpen).Returns(true); |
|||
Mock.Get(item).ResetCalls(); |
|||
|
|||
// Pointer briefly exits item, but submenu remains open.
|
|||
target.PointerLeave(item, leave); |
|||
Mock.Get(item).Verify(x => x.Close(), Times.Never); |
|||
Mock.Get(item).ResetCalls(); |
|||
|
|||
// Pointer enters child item; is selected.
|
|||
enter.Source = childItem; |
|||
target.PointerEnter(childItem, enter); |
|||
Mock.Get(item).VerifySet(x => x.SelectedItem = childItem); |
|||
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); |
|||
Mock.Get(item).ResetCalls(); |
|||
Mock.Get(parentItem).ResetCalls(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu() |
|||
{ |
|||
var target = new DefaultMenuInteractionHandler(); |
|||
var menu = Mock.Of<IMenu>(); |
|||
var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); |
|||
var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true); |
|||
var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; |
|||
|
|||
target.PointerPressed(item, e); |
|||
|
|||
Mock.Get(item).Verify(x => x.Open()); |
|||
Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); |
|||
Assert.True(e.Handled); |
|||
} |
|||
} |
|||
|
|||
private class TestTimer |
|||
{ |
|||
private Action _action; |
|||
|
|||
public bool ActionIsQueued => _action != null; |
|||
|
|||
public void Pulse() |
|||
{ |
|||
_action(); |
|||
_action = null; |
|||
} |
|||
|
|||
public void RunOnce(Action action, TimeSpan timeSpan) |
|||
{ |
|||
if (_action != null) |
|||
{ |
|||
throw new NotSupportedException("Action already set."); |
|||
} |
|||
|
|||
_action = action; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue