Browse Source

Merge branch 'master' into 1733-cleaning-up-theme-resources

pull/1831/head
William David Cossey 8 years ago
committed by GitHub
parent
commit
faab4e85e7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/Avalonia.Controls/Canvas.cs
  2. 148
      src/Avalonia.Controls/ContextMenu.cs
  3. 21
      src/Avalonia.Controls/IMenu.cs
  4. 40
      src/Avalonia.Controls/IMenuElement.cs
  5. 41
      src/Avalonia.Controls/IMenuItem.cs
  6. 87
      src/Avalonia.Controls/ItemsControl.cs
  7. 250
      src/Avalonia.Controls/Menu.cs
  8. 301
      src/Avalonia.Controls/MenuItem.cs
  9. 459
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  10. 22
      src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs
  11. 9
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  12. 36
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  13. 58
      src/Avalonia.Controls/StackPanel.cs
  14. 86
      src/Avalonia.Controls/TreeView.cs
  15. 7
      src/Avalonia.Controls/TreeViewItem.cs
  16. 5
      src/Avalonia.Controls/WrapPanel.cs
  17. 37
      src/Avalonia.Input/AccessKeyHandler.cs
  18. 2
      src/Avalonia.Input/IInputElement.cs
  19. 7
      src/Avalonia.Input/IMainMenu.cs
  20. 3
      src/Avalonia.Input/INavigableContainer.cs
  21. 2
      src/Avalonia.Input/InputElement.cs
  22. 33
      src/Avalonia.Input/KeyboardNavigation.cs
  23. 47
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  24. 242
      src/Avalonia.Input/Navigation/DirectionalNavigation.cs
  25. 2
      src/Avalonia.Input/Navigation/TabNavigation.cs
  26. 70
      src/Avalonia.Input/NavigationDirection.cs
  27. 7
      src/Avalonia.Themes.Default/MenuItem.xaml
  28. 3
      src/Avalonia.Themes.Default/Separator.xaml
  29. 72
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  30. 507
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  31. 2
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  32. 799
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs
  33. 31
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs

3
src/Avalonia.Controls/Canvas.cs

@ -136,8 +136,9 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap)
{
// TODO: Implement this
return null;

148
src/Avalonia.Controls/ContextMenu.cs

@ -1,16 +1,18 @@
using System;
using System.Reactive.Linq;
using System.Linq;
using System.ComponentModel;
using Avalonia.Controls.Platform;
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
using Input;
using Interactivity;
using LogicalTree;
using Primitives;
using System;
using System.Reactive.Linq;
using System.Linq;
using System.ComponentModel;
public class ContextMenu : SelectingItemsControl
public class ContextMenu : SelectingItemsControl, IMenu
{
private readonly IMenuInteractionHandler _interaction;
private bool _isOpen;
private Popup _popup;
@ -20,6 +22,25 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary>
public ContextMenu()
{
_interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
}
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary>
/// <param name="interactionHandler">The menu iteraction handler.</param>
public ContextMenu(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);
_interaction = interactionHandler;
}
/// <summary>
/// Initializes static members of the <see cref="ContextMenu"/> class.
@ -27,8 +48,6 @@ namespace Avalonia.Controls
static ContextMenu()
{
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
}
/// <summary>
@ -36,6 +55,36 @@ namespace Avalonia.Controls
/// </summary>
public bool IsOpen => _isOpen;
/// <inheritdoc/>
IMenuInteractionHandler IMenu.InteractionHandler => _interaction;
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ?
(IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
set
{
SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
}
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
@ -50,7 +99,6 @@ namespace Avalonia.Controls
/// </summary>
public event CancelEventHandler ContextMenuClosing;
/// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
/// </summary>
@ -71,62 +119,53 @@ namespace Avalonia.Controls
}
/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// Opens the menu.
/// </summary>
/// <param name="e">The event args.</param>
private void OnContextMenuClick(RoutedEventArgs e)
{
Hide();
FocusManager.Instance.Focus(null);
e.Handled = true;
}
public void Open() => Open(null);
/// <summary>
/// Closes the menu.
/// Opens a context menu on the specified control.
/// </summary>
public void Hide()
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (_popup != null && _popup.IsVisible)
if (_popup == null)
{
_popup.IsOpen = false;
_popup = new Popup()
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
StaysOpen = false,
ObeyScreenEdges = true
};
_popup.Closed += PopupClosed;
_interaction.Attach(this);
}
SelectedIndex = -1;
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
SetAndRaise(IsOpenProperty, ref _isOpen, false);
SetAndRaise(IsOpenProperty, ref _isOpen, true);
}
/// <summary>
/// Shows a context menu for the specified control.
/// Closes the menu.
/// </summary>
/// <param name="control">The control.</param>
private void Show(Control control)
public void Close()
{
if (control != null)
if (_popup != null && _popup.IsVisible)
{
if (_popup == null)
{
_popup = new Popup()
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
StaysOpen = false,
ObeyScreenEdges = true
};
_popup.Closed += PopupClosed;
}
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = false;
}
_popup.IsOpen = true;
SelectedIndex = -1;
SetAndRaise(IsOpenProperty, ref _isOpen, true);
}
SetAndRaise(IsOpenProperty, ref _isOpen, false);
}
private static void PopupClosed(object sender, EventArgs e)
private void PopupClosed(object sender, EventArgs e)
{
var contextMenu = (sender as Popup)?.Child as ContextMenu;
@ -152,7 +191,7 @@ namespace Avalonia.Controls
if (contextMenu.CancelClosing())
return;
control.ContextMenu.Hide();
control.ContextMenu.Close();
e.Handled = true;
}
@ -161,7 +200,7 @@ namespace Avalonia.Controls
if (contextMenu.CancelOpening())
return;
contextMenu.Show(control);
contextMenu.Open(control);
e.Handled = true;
}
}
@ -179,5 +218,10 @@ namespace Avalonia.Controls
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
{
throw new NotImplementedException();
}
}
}

21
src/Avalonia.Controls/IMenu.cs

@ -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; }
}
}

40
src/Avalonia.Controls/IMenuElement.cs

@ -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);
}
}

41
src/Avalonia.Controls/IMenuItem.cs

@ -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();
}
}

87
src/Avalonia.Controls/ItemsControl.cs

@ -15,6 +15,7 @@ using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -323,6 +324,46 @@ namespace Avalonia.Controls
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
/// Handles directional navigation within the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="e">The key events.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
if (!e.Handled)
{
var focus = FocusManager.Instance;
var direction = e.Key.ToNavigationDirection();
var container = Presenter?.Panel as INavigableContainer;
if (container == null ||
focus.Current == null ||
direction == null ||
direction.Value.IsTab())
{
return;
}
var current = focus.Current
.GetSelfAndVisualAncestors()
.OfType<IInputElement>()
.FirstOrDefault(x => x.VisualParent == container);
if (current != null)
{
var next = GetNextControl(container, direction.Value, current, false);
if (next != null)
{
focus.Focus(next, NavigationMethod.Directional);
e.Handled = true;
}
}
}
base.OnKeyDown(e);
}
/// <summary>
/// Caled when the <see cref="Items"/> property changes.
/// </summary>
@ -335,6 +376,7 @@ namespace Avalonia.Controls
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
SubscribeToItems(newValue);
@ -358,10 +400,8 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
int? count = (Items as IList)?.Count;
if (count != null)
ItemCount = (int)count;
UpdateItemCount();
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);
@ -445,5 +485,44 @@ namespace Avalonia.Controls
// TODO: Rebuild the item containers.
}
}
private void UpdateItemCount()
{
if (Items == null)
{
ItemCount = 0;
}
else if (Items is IList list)
{
ItemCount = list.Count;
}
else
{
ItemCount = Items.Count();
}
}
protected static IInputElement GetNextControl(
INavigableContainer container,
NavigationDirection direction,
IInputElement from,
bool wrap)
{
IInputElement result;
do
{
result = container.GetControl(direction, from, wrap);
if (result?.Focusable == true)
{
return result;
}
from = result;
} while (from != null);
return null;
}
}
}

250
src/Avalonia.Controls/Menu.cs

@ -2,30 +2,23 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Rendering;
namespace Avalonia.Controls
{
/// <summary>
/// A top-level menu control.
/// </summary>
public class Menu : SelectingItemsControl, IFocusScope, IMainMenu
public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu
{
/// <summary>
/// Defines the default items panel used by a <see cref="Menu"/>.
/// </summary>
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
@ -34,12 +27,42 @@ namespace Avalonia.Controls
nameof(IsOpen),
o => o.IsOpen);
/// <summary>
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
private readonly IMenuInteractionHandler _interaction;
private bool _isOpen;
/// <summary>
/// Tracks event handlers added to the root of the visual tree.
/// Initializes a new instance of the <see cref="Menu"/> class.
/// </summary>
public Menu()
{
_interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
}
/// <summary>
/// Initializes a new instance of the <see cref="Menu"/> class.
/// </summary>
private IDisposable _subscription;
/// <param name="interactionHandler">The menu iteraction handler.</param>
public Menu(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);
_interaction = interactionHandler;
}
/// <summary>
/// Initializes static members of the <see cref="Menu"/> class.
@ -47,7 +70,6 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick, handledEventsToo: true);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
}
@ -60,18 +82,52 @@ namespace Avalonia.Controls
private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <summary>
/// Gets the selected <see cref="MenuItem"/> container.
/// </summary>
private MenuItem SelectedMenuItem
/// <inheritdoc/>
IMenuInteractionHandler IMenu.InteractionHandler => _interaction;
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ?
(MenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
(IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
set
{
SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
}
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
/// <summary>
/// Occurs when a <see cref="Menu"/> is opened.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuOpened
{
add { AddHandler(MenuOpenedEvent, value); }
remove { RemoveHandler(MenuOpenedEvent, value); }
}
/// <summary>
/// Occurs when a <see cref="Menu"/> is closed.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuClosed
{
add { AddHandler(MenuClosedEvent, value); }
remove { RemoveHandler(MenuClosedEvent, value); }
}
/// <summary>
@ -79,13 +135,22 @@ namespace Avalonia.Controls
/// </summary>
public void Close()
{
foreach (MenuItem i in this.GetLogicalChildren())
if (IsOpen)
{
i.IsSubMenuOpen = false;
}
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
IsOpen = false;
SelectedIndex = -1;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
}
/// <summary>
@ -93,9 +158,25 @@ namespace Avalonia.Controls
/// </summary>
public void Open()
{
SelectedIndex = 0;
SelectedMenuItem.Focus();
IsOpen = true;
if (!IsOpen)
{
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
}
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <inheritdoc/>
@ -103,79 +184,27 @@ namespace Avalonia.Controls
{
base.OnAttachedToVisualTree(e);
var topLevel = (TopLevel)e.Root;
var window = e.Root as Window;
if (window != null)
window.Deactivated += Deactivated;
var pointerPress = topLevel.AddHandler(
PointerPressedEvent,
TopLevelPreviewPointerPress,
RoutingStrategies.Tunnel);
_subscription = new CompositeDisposable(
pointerPress,
Disposable.Create(() =>
{
if (window != null)
window.Deactivated -= Deactivated;
}),
InputManager.Instance.Process.Subscribe(ListenForNonClientClick));
var inputRoot = e.Root as IInputRoot;
if (inputRoot?.AccessKeyHandler != null)
{
inputRoot.AccessKeyHandler.MainMenu = this;
}
_interaction.Attach(this);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_subscription.Dispose();
_interaction.Detach(this);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <summary>
/// Called when a key is pressed within the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
bool menuWasOpen = SelectedMenuItem?.IsSubMenuOpen ?? false;
base.OnKeyDown(e);
if (menuWasOpen)
{
// If a menu item was open and we navigate to a new one with the arrow keys, open
// that menu and select the first item.
var selection = SelectedMenuItem;
if (selection != null && !selection.IsSubMenuOpen)
{
selection.IsSubMenuOpen = true;
selection.SelectedIndex = 0;
}
}
}
/// <summary>
/// Called when the menu loses focus.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
SelectedItem = null;
// Don't handle here: let the interaction handler handle it.
}
/// <summary>
@ -184,9 +213,7 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
var menuItem = e.Source as MenuItem;
if (menuItem != null && menuItem.Parent == this)
if (e.Source is MenuItem menuItem && menuItem.Parent == this)
{
foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
{
@ -199,58 +226,5 @@ namespace Avalonia.Controls
IsOpen = true;
}
/// <summary>
/// Called when the top-level window is deactivated.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void Deactivated(object sender, EventArgs e)
{
Close();
}
/// <summary>
/// Listens for non-client clicks and closes the menu when one is detected.
/// </summary>
/// <param name="e">The raw event.</param>
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
{
Close();
}
}
/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
private void OnMenuClick(RoutedEventArgs e)
{
Close();
FocusManager.Instance.Focus(null);
e.Handled = true;
}
/// <summary>
/// Called when the pointer is pressed anywhere on the window.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void TopLevelPreviewPointerPress(object sender, PointerPressedEventArgs e)
{
if (IsOpen)
{
var control = e.Source as ILogical;
if (!this.IsLogicalParentOf(control))
{
Close();
}
}
}
}
}

301
src/Avalonia.Controls/MenuItem.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls.Generators;
@ -11,14 +12,13 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : HeaderedSelectingItemsControl, ISelectable
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
{
/// <summary>
/// Defines the <see cref="Command"/> property.
@ -62,6 +62,18 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerEnterItem"/> event.
/// </summary>
public static readonly RoutedEvent<PointerEventArgs> PointerEnterItemEvent =
RoutedEvent.Register<InputElement, PointerEventArgs>(nameof(PointerEnterItem), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerLeaveItem"/> event.
/// </summary>
public static readonly RoutedEvent<PointerEventArgs> PointerLeaveItemEvent =
RoutedEvent.Register<InputElement, PointerEventArgs>(nameof(PointerLeaveItem), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="SubmenuOpened"/> event.
/// </summary>
@ -72,15 +84,7 @@ namespace Avalonia.Controls
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
});
/// <summary>
/// The timer used to display submenus.
/// </summary>
private IDisposable _submenuTimer;
new FuncTemplate<IPanel>(() => new StackPanel());
/// <summary>
/// The submenu popup.
@ -96,16 +100,15 @@ namespace Avalonia.Controls
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true);
IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
IsSelectedProperty.Changed.AddClassHandler<MenuItem>(x => x.IsSelectedChanged);
ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.AddClassHandler<MenuItem>(x => x.SubMenuOpenChanged);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(x => x.AccessKeyPressed);
}
public MenuItem()
{
}
/// <summary>
@ -117,6 +120,30 @@ namespace Avalonia.Controls
remove { RemoveHandler(ClickEvent, value); }
}
/// <summary>
/// Occurs when the pointer enters a menu item.
/// </summary>
/// <remarks>
/// A bubbling version of the <see cref="InputElement.PointerEnter"/> event for menu items.
/// </remarks>
public event EventHandler<PointerEventArgs> PointerEnterItem
{
add { AddHandler(PointerEnterItemEvent, value); }
remove { RemoveHandler(PointerEnterItemEvent, value); }
}
/// <summary>
/// Raised when the pointer leaves a menu item.
/// </summary>
/// <remarks>
/// A bubbling version of the <see cref="InputElement.PointerLeave"/> event for menu items.
/// </remarks>
public event EventHandler<PointerEventArgs> PointerLeaveItem
{
add { AddHandler(PointerLeaveItemEvent, value); }
remove { RemoveHandler(PointerLeaveItemEvent, value); }
}
/// <summary>
/// Occurs when a <see cref="MenuItem"/>'s submenu is opened.
/// </summary>
@ -188,10 +215,71 @@ namespace Avalonia.Controls
public bool HasSubMenu => !Classes.Contains(":empty");
/// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level menu item.
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
/// </summary>
public bool IsTopLevel => Parent is Menu;
/// <inheritdoc/>
bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ?
(IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
set
{
SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
}
}
/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
/// <summary>
/// Opens the submenu.
/// </summary>
/// <remarks>
/// This has the same effect as setting <see cref="IsSubMenuOpen"/> to true.
/// </remarks>
public void Open() => IsSubMenuOpen = true;
/// <summary>
/// Closes the submenu.
/// </summary>
/// <remarks>
/// This has the same effect as setting <see cref="IsSubMenuOpen"/> to false.
/// </remarks>
public void Close() => IsSubMenuOpen = false;
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
/// <summary>
/// Called when the <see cref="MenuItem"/> is clicked.
/// </summary>
@ -205,163 +293,43 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when the <see cref="MenuItem"/> recieves focus.
/// </summary>
/// <param name="e">The event args.</param>
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
IsSelected = true;
e.Handled = UpdateSelectionFromEventSource(e.Source, true);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
/// <summary>
/// Called when a key is pressed in the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
// Some keypresses we want to pass straight to the parent MenuItem/Menu without giving
// this MenuItem the chance to handle them. This is usually e.g. when the submenu is
// closed so passing them to the base would try to move the selection in a hidden
// submenu.
var passStraightToParent = true;
switch (e.Key)
{
case Key.Left:
if (!IsTopLevel && IsSubMenuOpen)
{
IsSubMenuOpen = false;
e.Handled = true;
}
passStraightToParent = IsTopLevel || !IsSubMenuOpen;
break;
case Key.Right:
if (!IsTopLevel && HasSubMenu && !IsSubMenuOpen)
{
SelectedIndex = 0;
IsSubMenuOpen = true;
e.Handled = true;
}
passStraightToParent = IsTopLevel || !IsSubMenuOpen;
break;
case Key.Enter:
if (HasSubMenu)
{
goto case Key.Right;
}
else
{
RaiseEvent(new RoutedEventArgs(ClickEvent));
e.Handled = true;
}
break;
case Key.Escape:
if (IsSubMenuOpen)
{
IsSubMenuOpen = false;
e.Handled = true;
}
break;
}
if (!passStraightToParent)
{
base.OnKeyDown(e);
}
// Don't handle here: let event bubble up to menu.
}
/// <summary>
/// Called when the pointer enters the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
/// <inheritdoc/>
protected override void OnPointerEnter(PointerEventArgs e)
{
base.OnPointerEnter(e);
var menu = Parent as Menu;
if (menu != null)
{
if (menu.IsOpen)
{
IsSubMenuOpen = true;
}
}
else if (HasSubMenu && !IsSubMenuOpen)
RaiseEvent(new PointerEventArgs
{
_submenuTimer = DispatcherTimer.Run(
() => IsSubMenuOpen = true,
TimeSpan.FromMilliseconds(400));
}
else
{
var parentItem = Parent as MenuItem;
if (parentItem != null)
{
foreach (var sibling in parentItem.Items
.OfType<MenuItem>()
.Where(x => x != this && x.IsSubMenuOpen))
{
sibling.CloseSubmenus();
sibling.IsSubMenuOpen = false;
sibling.IsSelected = false;
}
}
}
Device = e.Device,
RoutedEvent = PointerEnterItemEvent,
Source = this,
});
}
/// <summary>
/// Called when the pointer leaves the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
/// <inheritdoc/>
protected override void OnPointerLeave(PointerEventArgs e)
{
base.OnPointerLeave(e);
if (_submenuTimer != null)
RaiseEvent(new PointerEventArgs
{
_submenuTimer.Dispose();
_submenuTimer = null;
}
}
/// <summary>
/// Called when the pointer is pressed over the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (!HasSubMenu)
{
RaiseEvent(new RoutedEventArgs(ClickEvent));
}
else if (IsTopLevel)
{
IsSubMenuOpen = !IsSubMenuOpen;
}
else
{
IsSubMenuOpen = true;
}
e.Handled = true;
Device = e.Device,
RoutedEvent = PointerLeaveItemEvent,
Source = this,
});
}
/// <summary>
@ -374,7 +342,7 @@ namespace Avalonia.Controls
if (menuItem != null && menuItem.Parent == this)
{
foreach (var child in Items.OfType<MenuItem>())
foreach (var child in ((IMenuItem)this).SubItems)
{
if (child != menuItem && child.IsSubMenuOpen)
{
@ -395,31 +363,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
/// <summary>
/// Called when the menu item's access key is pressed.
/// </summary>
/// <param name="e">The event args.</param>
private void AccessKeyPressed(RoutedEventArgs e)
{
if (HasSubMenu)
{
SelectedIndex = 0;
IsSubMenuOpen = true;
}
else
{
RaiseEvent(new RoutedEventArgs(ClickEvent));
}
e.Handled = true;
}
/// <summary>
/// Closes all submenus of the menu item.
/// </summary>
private void CloseSubmenus()
{
foreach (var child in Items.OfType<MenuItem>())
foreach (var child in ((IMenuItem)this).SubItems)
{
child.IsSubMenuOpen = false;
}
@ -479,6 +428,18 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when the <see cref="IsSelected"/> property changes.
/// </summary>
/// <param name="e">The property change event.</param>
private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
Focus();
}
}
/// <summary>
/// Called when the <see cref="IsSubMenuOpen"/> property changes.
/// </summary>

459
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -0,0 +1,459 @@
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)
{
Contract.Requires<ArgumentNullException>(item != null);
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 && 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.Parent.SelectedItem == item)
{
if (item.IsTopLevel)
{
if (!((IMenu)item.Parent).IsOpen)
{
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);
}
}
}

22
src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs

@ -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);
}
}

9
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -143,13 +143,6 @@ namespace Avalonia.Controls.Presenters
Virtualizer = ItemVirtualizer.Create(this);
((ILogicalScrollable)this).InvalidateScroll?.Invoke();
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{
KeyboardNavigation.SetDirectionalNavigation(
(InputElement)Panel,
KeyboardNavigationMode.Contained);
}
KeyboardNavigation.SetTabNavigation(
(InputElement)Panel,
KeyboardNavigation.GetTabNavigation(this));
@ -175,4 +168,4 @@ namespace Avalonia.Controls.Presenters
((ILogicalScrollable)this).InvalidateScroll?.Invoke();
}
}
}
}

36
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -457,6 +457,42 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Moves the selection in the specified direction relative to the current selection.
/// </summary>
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(NavigationDirection direction, bool wrap)
{
var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null;
return MoveSelection(from, direction, wrap);
}
/// <summary>
/// Moves the selection in the specified direction relative to the specified container.
/// </summary>
/// <param name="from">The container which serves as a starting point for the movement.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is IControl next)
{
var index = ItemContainerGenerator.IndexFromContainer(next);
if (index != -1)
{
SelectedIndex = index;
return true;
}
}
return false;
}
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>

58
src/Avalonia.Controls/StackPanel.cs

@ -56,11 +56,49 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap)
{
var fromControl = from as IControl;
return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null;
var result = GetControlInDirection(direction, from as IControl);
if (result == null && wrap)
{
if (Orientation == Orientation.Vertical)
{
switch (direction)
{
case NavigationDirection.Up:
case NavigationDirection.Previous:
case NavigationDirection.PageUp:
result = GetControlInDirection(NavigationDirection.Last, null);
break;
case NavigationDirection.Down:
case NavigationDirection.Next:
case NavigationDirection.PageDown:
result = GetControlInDirection(NavigationDirection.First, null);
break;
}
}
else
{
switch (direction)
{
case NavigationDirection.Left:
case NavigationDirection.Previous:
case NavigationDirection.PageUp:
result = GetControlInDirection(NavigationDirection.Last, null);
break;
case NavigationDirection.Right:
case NavigationDirection.Next:
case NavigationDirection.PageDown:
result = GetControlInDirection(NavigationDirection.First, null);
break;
}
}
}
return result;
}
/// <summary>
@ -72,7 +110,7 @@ namespace Avalonia.Controls
protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
int index = from != null ? Children.IndexOf(from) : -1;
switch (direction)
{
@ -83,22 +121,22 @@ namespace Avalonia.Controls
index = Children.Count - 1;
break;
case NavigationDirection.Next:
++index;
if (index != -1) ++index;
break;
case NavigationDirection.Previous:
--index;
if (index != -1) --index;
break;
case NavigationDirection.Left:
index = horiz ? index - 1 : -1;
if (index != -1) index = horiz ? index - 1 : -1;
break;
case NavigationDirection.Right:
index = horiz ? index + 1 : -1;
if (index != -1) index = horiz ? index + 1 : -1;
break;
case NavigationDirection.Up:
index = horiz ? -1 : index - 1;
if (index != -1) index = horiz ? -1 : index - 1;
break;
case NavigationDirection.Down:
index = horiz ? -1 : index + 1;
if (index != -1) index = horiz ? -1 : index + 1;
break;
default:
index = -1;

86
src/Avalonia.Controls/TreeView.cs

@ -136,6 +136,92 @@ namespace Avalonia.Controls
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
var direction = e.Key.ToNavigationDirection();
if (direction?.IsDirectional() == true && !e.Handled)
{
if (SelectedItem != null)
{
var next = GetContainerInDirection(
GetContainerFromEventSource(e.Source) as TreeViewItem,
direction.Value,
true);
if (next != null)
{
FocusManager.Instance.Focus(next, NavigationMethod.Directional);
e.Handled = true;
}
}
else
{
SelectedItem = ElementAt(Items, 0);
}
}
}
private TreeViewItem GetContainerInDirection(
TreeViewItem from,
NavigationDirection direction,
bool intoChildren)
{
IItemContainerGenerator parentGenerator;
if (from?.Parent is TreeView treeView)
{
parentGenerator = treeView.ItemContainerGenerator;
}
else if (from?.Parent is TreeViewItem item)
{
parentGenerator = item.ItemContainerGenerator;
}
else
{
return null;
}
var index = parentGenerator.IndexFromContainer(from);
var parent = from.Parent as ItemsControl;
TreeViewItem result = null;
switch (direction)
{
case NavigationDirection.Up:
if (index > 0)
{
var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1);
result = previous.IsExpanded ?
(TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) :
previous;
}
else
{
result = from.Parent as TreeViewItem;
}
break;
case NavigationDirection.Down:
if (from.IsExpanded && intoChildren)
{
result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0);
}
else if (index < parent?.ItemCount - 1)
{
result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1);
}
else if (parent is TreeViewItem parentItem)
{
return GetContainerInDirection(parentItem, direction, false);
}
break;
}
return result;
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{

7
src/Avalonia.Controls/TreeViewItem.cs

@ -32,10 +32,7 @@ namespace Avalonia.Controls
ListBoxItem.IsSelectedProperty.AddOwner<TreeViewItem>();
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
});
new FuncTemplate<IPanel>(() => new StackPanel());
private TreeView _treeView;
private bool _isExpanded;
@ -127,7 +124,7 @@ namespace Avalonia.Controls
}
}
base.OnKeyDown(e);
// Don't call base.OnKeyDown - let events bubble up to containing TreeView.
}
}
}

5
src/Avalonia.Controls/WrapPanel.cs

@ -47,8 +47,9 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);
@ -250,4 +251,4 @@ namespace Avalonia.Controls
}
}
}
}
}

37
src/Avalonia.Input/AccessKeyHandler.cs

@ -53,10 +53,32 @@ namespace Avalonia.Input
/// </summary>
private IInputElement _restoreFocusElement;
/// <summary>
/// The window's main menu.
/// </summary>
private IMainMenu _mainMenu;
/// <summary>
/// Gets or sets the window's main menu.
/// </summary>
public IMainMenu MainMenu { get; set; }
public IMainMenu MainMenu
{
get => _mainMenu;
set
{
if (_mainMenu != null)
{
_mainMenu.MenuClosed -= MainMenuClosed;
}
_mainMenu = value;
if (_mainMenu != null)
{
_mainMenu.MenuClosed += MainMenuClosed;
}
}
}
/// <summary>
/// Sets the owner of the access key handler.
@ -160,13 +182,7 @@ namespace Avalonia.Input
{
bool menuIsOpen = MainMenu?.IsOpen == true;
if (e.Key == Key.Escape && menuIsOpen)
{
// When the Escape key is pressed with the main menu open, close it.
CloseMenu();
e.Handled = true;
}
else if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen)
if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen)
{
// If any other key is pressed with the Alt key held down, or the main menu is open,
// find all controls who have registered that access key.
@ -245,5 +261,10 @@ namespace Avalonia.Input
MainMenu.Close();
_owner.ShowAccessKeys = _showingAccessKeys = false;
}
private void MainMenuClosed(object sender, EventArgs e)
{
_owner.ShowAccessKeys = false;
}
}
}

2
src/Avalonia.Input/IInputElement.cs

@ -15,7 +15,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when the control receives focus.
/// </summary>
event EventHandler<RoutedEventArgs> GotFocus;
event EventHandler<GotFocusEventArgs> GotFocus;
/// <summary>
/// Occurs when the control loses focus.

7
src/Avalonia.Input/IMainMenu.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -24,5 +26,10 @@ namespace Avalonia.Input
/// Opens the menu in response to the Alt/F10 key.
/// </summary>
void Open();
/// <summary>
/// Occurs when the main menu closes.
/// </summary>
event EventHandler<RoutedEventArgs> MenuClosed;
}
}

3
src/Avalonia.Input/INavigableContainer.cs

@ -13,7 +13,8 @@ namespace Avalonia.Input
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement GetControl(NavigationDirection direction, IInputElement from);
IInputElement GetControl(NavigationDirection direction, IInputElement from, bool wrap);
}
}

2
src/Avalonia.Input/InputElement.cs

@ -177,7 +177,7 @@ namespace Avalonia.Input
/// <summary>
/// Occurs when the control receives focus.
/// </summary>
public event EventHandler<RoutedEventArgs> GotFocus
public event EventHandler<GotFocusEventArgs> GotFocus
{
add { AddHandler(GotFocusEvent, value); }
remove { RemoveHandler(GotFocusEvent, value); }

33
src/Avalonia.Input/KeyboardNavigation.cs

@ -8,19 +8,6 @@ namespace Avalonia.Input
/// </summary>
public static class KeyboardNavigation
{
/// <summary>
/// Defines the DirectionalNavigation attached property.
/// </summary>
/// <remarks>
/// The DirectionalNavigation attached property defines how pressing arrow keys causes
/// focus to be navigated between the children of the container.
/// </remarks>
public static readonly AttachedProperty<KeyboardNavigationMode> DirectionalNavigationProperty =
AvaloniaProperty.RegisterAttached<InputElement, KeyboardNavigationMode>(
"DirectionalNavigation",
typeof(KeyboardNavigation),
KeyboardNavigationMode.None);
/// <summary>
/// Defines the TabNavigation attached property.
/// </summary>
@ -46,26 +33,6 @@ namespace Avalonia.Input
"TabOnceActiveElement",
typeof(KeyboardNavigation));
/// <summary>
/// Gets the <see cref="DirectionalNavigationProperty"/> for a container.
/// </summary>
/// <param name="element">The container.</param>
/// <returns>The <see cref="KeyboardNavigationMode"/> for the container.</returns>
public static KeyboardNavigationMode GetDirectionalNavigation(InputElement element)
{
return element.GetValue(DirectionalNavigationProperty);
}
/// <summary>
/// Sets the <see cref="DirectionalNavigationProperty"/> for a container.
/// </summary>
/// <param name="element">The container.</param>
/// <param name="value">The <see cref="KeyboardNavigationMode"/> for the container.</param>
public static void SetDirectionalNavigation(InputElement element, KeyboardNavigationMode value)
{
element.SetValue(DirectionalNavigationProperty, value);
}
/// <summary>
/// Gets the <see cref="TabNavigationProperty"/> for a container.
/// </summary>

47
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -85,7 +85,7 @@ namespace Avalonia.Input
}
else
{
return DirectionalNavigation.GetNext(element, direction);
throw new NotSupportedException();
}
}
@ -122,47 +122,12 @@ namespace Avalonia.Input
{
var current = FocusManager.Instance.Current;
if (current != null)
if (current != null && e.Key == Key.Tab)
{
NavigationDirection? direction = null;
switch (e.Key)
{
case Key.Tab:
direction = (e.Modifiers & InputModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
break;
case Key.Up:
direction = NavigationDirection.Up;
break;
case Key.Down:
direction = NavigationDirection.Down;
break;
case Key.Left:
direction = NavigationDirection.Left;
break;
case Key.Right:
direction = NavigationDirection.Right;
break;
case Key.PageUp:
direction = NavigationDirection.PageUp;
break;
case Key.PageDown:
direction = NavigationDirection.PageDown;
break;
case Key.Home:
direction = NavigationDirection.First;
break;
case Key.End:
direction = NavigationDirection.Last;
break;
}
if (direction.HasValue)
{
Move(current, direction.Value, e.Modifiers);
e.Handled = true;
}
var direction = (e.Modifiers & InputModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.Modifiers);
e.Handled = true;
}
}
}

242
src/Avalonia.Input/Navigation/DirectionalNavigation.cs

@ -1,242 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Input.Navigation
{
/// <summary>
/// The implementation for default directional navigation.
/// </summary>
public static class DirectionalNavigation
{
/// <summary>
/// Gets the next control in the specified navigation direction.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The navigation direction.</param>
/// <returns>
/// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement GetNext(
IInputElement element,
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction != NavigationDirection.Next &&
direction != NavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
if (container != null)
{
var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container);
switch (mode)
{
case KeyboardNavigationMode.Continue:
return GetNextInContainer(element, container, direction) ??
GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
return GetNextInContainer(element, container, direction) ??
GetFocusableDescendant(container, direction);
case KeyboardNavigationMode.Contained:
return GetNextInContainer(element, container, direction);
default:
return null;
}
}
else
{
return GetFocusableDescendants(element).FirstOrDefault();
}
}
/// <summary>
/// Returns a value indicting whether the specified direction is forward.
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>True if the direction is forward.</returns>
private static bool IsForward(NavigationDirection direction)
{
return direction == NavigationDirection.Next ||
direction == NavigationDirection.Last ||
direction == NavigationDirection.Right ||
direction == NavigationDirection.Down;
}
/// <summary>
/// Gets the first or last focusable descendant of the specified element.
/// </summary>
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
{
return IsForward(direction) ?
GetFocusableDescendants(container).FirstOrDefault() :
GetFocusableDescendants(container).LastOrDefault();
}
/// <summary>
/// Gets the focusable descendants of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The element's focusable descendants.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element)
{
var children = element.GetVisualChildren().OfType<IInputElement>();
foreach (var child in children)
{
if (child.CanFocus())
{
yield return child;
}
if (child.CanFocusDescendants())
{
foreach (var descendant in GetFocusableDescendants(child))
{
yield return descendant;
}
}
}
}
/// <summary>
/// Gets the next item that should be focused in the specified container.
/// </summary>
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <returns>The next element, or null if the element is the last.</returns>
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
if (direction == NavigationDirection.Down)
{
var descendant = GetFocusableDescendants(element).FirstOrDefault();
if (descendant != null)
{
return descendant;
}
}
if (container != null)
{
var navigable = container as INavigableContainer;
if (navigable != null)
{
while (element != null)
{
element = navigable.GetControl(direction, element);
if (element != null && element.CanFocus())
{
break;
}
}
}
else
{
// TODO: Do a spatial search here if the container doesn't implement
// INavigableContainer.
element = null;
}
if (element != null && direction == NavigationDirection.Up)
{
var descendant = GetFocusableDescendants(element).LastOrDefault();
if (descendant != null)
{
return descendant;
}
}
return element;
}
return null;
}
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="element">The element being navigated away from.</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
var isForward = IsForward(direction);
IInputElement next = null;
if (parent != null)
{
if (!isForward && parent.CanFocus())
{
return parent;
}
var siblings = parent.GetVisualChildren()
.OfType<IInputElement>()
.Where(FocusExtensions.CanFocusDescendants);
var sibling = isForward ?
siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() :
siblings.TakeWhile(x => x != container).LastOrDefault();
if (sibling != null)
{
if (sibling is ICustomKeyboardNavigation custom)
{
var (handled, customNext) = custom.GetNext(element, direction);
if (handled)
{
return customNext;
}
}
if (sibling.CanFocus())
{
next = sibling;
}
else
{
next = isForward ?
GetFocusableDescendants(sibling).FirstOrDefault() :
GetFocusableDescendants(sibling).LastOrDefault();
}
}
if (next == null)
{
next = GetFirstInNextContainer(element, parent, direction);
}
}
else
{
next = isForward ?
GetFocusableDescendants(container).FirstOrDefault() :
GetFocusableDescendants(container).LastOrDefault();
}
return next;
}
}
}

2
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -168,7 +168,7 @@ namespace Avalonia.Input.Navigation
{
while (element != null)
{
element = navigable.GetControl(direction, element);
element = navigable.GetControl(direction, element, false);
if (element != null && element.CanFocus())
{

70
src/Avalonia.Input/NavigationDirection.cs

@ -58,4 +58,74 @@ namespace Avalonia.Input
/// </summary>
PageDown,
}
public static class NavigationDirectionExtensions
{
/// <summary>
/// Checks whether a <see cref="NavigationDirection"/> represents a tab movement.
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>
/// True if the direction represents a tab movement (<see cref="NavigationDirection.Next"/>
/// or <see cref="NavigationDirection.Previous"/>); otherwise false.
/// </returns>
public static bool IsTab(this NavigationDirection direction)
{
return direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous;
}
/// <summary>
/// Checks whether a <see cref="NavigationDirection"/> represents a directional movement.
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>
/// True if the direction represents a directional movement (any value except
/// <see cref="NavigationDirection.Next"/> and <see cref="NavigationDirection.Previous"/>);
/// otherwise false.
/// </returns>
public static bool IsDirectional(this NavigationDirection direction)
{
return direction > NavigationDirection.Previous ||
direction <= NavigationDirection.PageDown;
}
/// <summary>
/// Converts a keypress into a <see cref="NavigationDirection"/>.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="modifiers">The keyboard modifiers.</param>
/// <returns>
/// A <see cref="NavigationDirection"/> if the keypress represents a navigation keypress.
/// </returns>
public static NavigationDirection? ToNavigationDirection(
this Key key,
InputModifiers modifiers = InputModifiers.None)
{
switch (key)
{
case Key.Tab:
return (modifiers & InputModifiers.Shift) != 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
case Key.Up:
return NavigationDirection.Up;
case Key.Down:
return NavigationDirection.Down;
case Key.Left:
return NavigationDirection.Left;
case Key.Right:
return NavigationDirection.Right;
case Key.Home:
return NavigationDirection.First;
case Key.End:
return NavigationDirection.Last;
case Key.PageUp:
return NavigationDirection.PageUp;
case Key.PageDown:
return NavigationDirection.PageDown;
default:
return null;
}
}
}
}

7
src/Avalonia.Themes.Default/MenuItem.xaml

@ -127,11 +127,6 @@
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:pointerover /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
</Style>
<Style Selector="MenuItem:empty /template/ Path#rightArrow">
<Setter Property="IsVisible" Value="False"/>
</Style>
@ -139,4 +134,4 @@
<Style Selector="MenuItem:disabled">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
</Styles>
</Styles>

3
src/Avalonia.Themes.Default/Separator.xaml

@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="Separator">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}"
@ -22,4 +23,4 @@
<Setter Property="Height" Value="1"/>
</Style>
</Styles>
</Styles>

72
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -11,6 +11,7 @@ using Avalonia.VisualTree;
using Xunit;
using System.Collections.ObjectModel;
using Avalonia.UnitTests;
using Avalonia.Input;
namespace Avalonia.Controls.UnitTests
{
@ -494,6 +495,77 @@ namespace Avalonia.Controls.UnitTests
Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child));
}
[Fact]
public void Focuses_Next_Item_On_Key_Down()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var items = new object[]
{
new Button(),
new Button(),
};
var target = new ItemsControl
{
Template = GetTemplate(),
Items = items,
};
var root = new TestRoot { Child = target };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Presenter.Panel.Children[0].Focus();
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Down,
});
Assert.Equal(
target.Presenter.Panel.Children[1],
FocusManager.Instance.Current);
}
}
[Fact]
public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var items = new object[]
{
new Button(),
new Button { Focusable = false },
new Button(),
};
var target = new ItemsControl
{
Template = GetTemplate(),
Items = items,
};
var root = new TestRoot { Child = target };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.Presenter.Panel.Children[0].Focus();
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Down,
});
Assert.Equal(
target.Presenter.Panel.Children[2],
FocusManager.Instance.Current);
}
}
private class Item
{
public Item(string value)

507
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -0,0 +1,507 @@
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 };
Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item);
target.PointerLeave(item, e);
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null);
Assert.False(e.Handled);
}
[Fact]
public void PointerLeave_Doesnt_Deselect_Sibling()
{
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 sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling);
target.PointerLeave(item, e);
Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never);
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;
}
}
}
}

2
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -219,7 +219,7 @@ namespace Avalonia.Controls.UnitTests
scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
((ISetLogicalParent)target).SetParent(presenter.Object);
((INavigableContainer)target).GetControl(NavigationDirection.Next, from);
((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false);
scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from));
}

799
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs

@ -1,799 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class KeyboardNavigationTests_Arrows
{
[Fact]
public void Down_Continue_Returns_Down_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
(next = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Continue_Returns_First_Control_In_Down_Sibling_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(current = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Continue_Returns_Down_Sibling()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(current = new Button { Name = "Button3" }),
}
},
(next = new Button { Name = "Button4" }),
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Continue_Returns_First_Control_In_Down_Uncle_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(current = new Button { Name = "Button3" }),
}
},
},
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(next = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Continue_Returns_Child_Of_Top_Level()
{
Button next;
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(next = new Button { Name = "Button1" }),
}
};
var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Continue_Wraps()
{
Button current;
Button next;
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
new Button { Name = "Button3" },
}
},
},
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
(current = new Button { Name = "Button6" }),
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Cycle_Returns_Down_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
(next = new Button { Name = "Button3" }),
}
},
new StackPanel
{
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Cycle_Wraps_To_First()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(next = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
(current = new Button { Name = "Button3" }),
}
},
new StackPanel
{
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Contained_Returns_Down_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
(next = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Equal(next, result);
}
[Fact]
public void Down_Contained_Stops_At_End()
{
Button current;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(current = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Null(result);
}
[Fact]
public void Down_None_Does_Nothing()
{
Button current;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.None,
Children =
{
new Button { Name = "Button1" },
(current = new Button { Name = "Button2" }),
new Button { Name = "Button3" },
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down);
Assert.Null(result);
}
[Fact]
public void Up_Continue_Returns_Up_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
(next = new Button { Name = "Button2" }),
(current = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Continue_Returns_Last_Control_In_Up_Sibling_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(next = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Continue_Returns_Last_Child_Of_Sibling()
{
Button current;
Button next;
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(next = new Button { Name = "Button3" }),
}
},
(current = new Button { Name = "Button4" }),
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Continue_Returns_Last_Control_In_Up_Nephew_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button1" },
new Button { Name = "Button2" },
(next = new Button { Name = "Button3" }),
}
},
},
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(current = new Button { Name = "Button4" }),
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Continue_Wraps()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
new Button { Name = "Button3" },
}
},
},
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
(next = new Button { Name = "Button6" }),
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Continue_Returns_Parent()
{
Button current;
var top = new Decorator
{
Focusable = true,
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
Child = current = new Button
{
Name = "Button",
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(top, result);
}
[Fact]
public void Up_Cycle_Returns_Up_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
new Button { Name = "Button3" },
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Cycle_Wraps_To_Last()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
(next = new Button { Name = "Button3" }),
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Contained_Returns_Up_Control_In_Container()
{
Button current;
Button next;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
(next = new Button { Name = "Button1" }),
(current = new Button { Name = "Button2" }),
new Button { Name = "Button3" },
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Equal(next, result);
}
[Fact]
public void Up_Contained_Stops_At_Beginning()
{
Button current;
var top = new StackPanel
{
Children =
{
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
(current = new Button { Name = "Button1" }),
new Button { Name = "Button2" },
new Button { Name = "Button3" },
}
},
new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
new Button { Name = "Button4" },
new Button { Name = "Button5" },
new Button { Name = "Button6" },
}
},
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Null(result);
}
[Fact]
public void Up_Contained_Doesnt_Return_Child_Control()
{
Decorator current;
var top = new StackPanel
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained,
Children =
{
(current = new Decorator
{
Focusable = true,
Child = new Button(),
})
}
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up);
Assert.Null(result);
}
}
}

31
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs

@ -140,37 +140,6 @@ namespace Avalonia.Input.UnitTests
Assert.Same(next, result);
}
[Fact]
public void Right_Should_Custom_Navigate_From_Outside()
{
Button current;
Button next;
var target = new CustomNavigatingStackPanel
{
Children =
{
new Button { Content = "Button 1" },
new Button { Content = "Button 2" },
(next = new Button { Content = "Button 3" }),
},
NextControl = next,
};
var root = new StackPanel
{
Children =
{
(current = new Button { Content = "Outside" }),
target,
},
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
};
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right);
Assert.Same(next, result);
}
[Fact]
public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next()
{

Loading…
Cancel
Save