csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
220 lines
8.4 KiB
220 lines
8.4 KiB
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Avalonia.Input.Navigation;
|
|
using Avalonia.Input;
|
|
using Avalonia.Metadata;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Input
|
|
{
|
|
/// <summary>
|
|
/// Handles keyboard navigation for a window.
|
|
/// </summary>
|
|
[Unstable]
|
|
public sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler
|
|
{
|
|
/// <summary>
|
|
/// The window to which the handler belongs.
|
|
/// </summary>
|
|
private IInputRoot? _owner;
|
|
|
|
/// <summary>
|
|
/// Sets the owner of the keyboard navigation handler.
|
|
/// </summary>
|
|
/// <param name="owner">The owner.</param>
|
|
/// <remarks>
|
|
/// This method can only be called once, typically by the owner itself on creation.
|
|
/// </remarks>
|
|
[PrivateApi]
|
|
public void SetOwner(IInputRoot owner)
|
|
{
|
|
if (_owner != null)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(KeyboardNavigationHandler)} owner has already been set.");
|
|
}
|
|
|
|
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
|
_owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown);
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
element = element ?? throw new ArgumentNullException(nameof(element));
|
|
return GetNextPrivate(element, null, direction, null);
|
|
}
|
|
|
|
private static IInputElement? GetNextPrivate(
|
|
IInputElement? element,
|
|
IInputRoot? owner,
|
|
NavigationDirection direction,
|
|
KeyDeviceType? keyDeviceType)
|
|
{
|
|
var elementOrOwner = element ?? owner ?? throw new ArgumentNullException(nameof(owner));
|
|
|
|
// If there's a custom keyboard navigation handler as an ancestor, use that.
|
|
var custom = (element as Visual)?.FindAncestorOfType<ICustomKeyboardNavigation>(true);
|
|
if (custom is not null && HandlePreCustomNavigation(custom, elementOrOwner, direction, out var ce))
|
|
return ce;
|
|
|
|
IInputElement? result;
|
|
if (direction is NavigationDirection.Next)
|
|
{
|
|
result = TabNavigation.GetNextTab(elementOrOwner, false);
|
|
}
|
|
else if (direction is NavigationDirection.Previous)
|
|
{
|
|
result = TabNavigation.GetPrevTab(elementOrOwner, null, false);
|
|
}
|
|
else if (direction is NavigationDirection.Up or NavigationDirection.Down
|
|
or NavigationDirection.Left or NavigationDirection.Right)
|
|
{
|
|
// HACK: a window should always have some element focused,
|
|
// it seems to be a difference between UWP and Avalonia focus manager implementations.
|
|
result = element is null
|
|
? TabNavigation.GetNextTab(elementOrOwner, true)
|
|
: XYFocus.TryDirectionalFocus(direction, element, owner, null, keyDeviceType);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
|
|
}
|
|
|
|
// If there wasn't a custom navigation handler as an ancestor of the current element,
|
|
// but there is one as an ancestor of the new element, use that.
|
|
if (custom is null && HandlePostCustomNavigation(elementOrOwner, result, direction, out ce))
|
|
return ce;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the focus in the specified direction.
|
|
/// </summary>
|
|
/// <param name="element">The current element.</param>
|
|
/// <param name="direction">The direction to move.</param>
|
|
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
|
|
public void Move(
|
|
IInputElement? element,
|
|
NavigationDirection direction,
|
|
KeyModifiers keyModifiers = KeyModifiers.None)
|
|
{
|
|
MovePrivate(element, direction, keyModifiers, null);
|
|
}
|
|
|
|
// TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler.
|
|
private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType)
|
|
{
|
|
var next = GetNextPrivate(element, _owner, direction, deviceType);
|
|
|
|
if (next != null)
|
|
{
|
|
var method = direction == NavigationDirection.Next ||
|
|
direction == NavigationDirection.Previous ?
|
|
NavigationMethod.Tab : NavigationMethod.Directional;
|
|
return next.Focus(method, keyModifiers);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the Tab key being pressed in the window.
|
|
/// </summary>
|
|
/// <param name="sender">The event sender.</param>
|
|
/// <param name="e">The event args.</param>
|
|
void OnKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Tab)
|
|
{
|
|
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
|
|
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
|
|
NavigationDirection.Next : NavigationDirection.Previous;
|
|
e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
|
|
}
|
|
else if (e.Key is Key.Left or Key.Right or Key.Up or Key.Down)
|
|
{
|
|
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
|
|
var direction = e.Key switch
|
|
{
|
|
Key.Left => NavigationDirection.Left,
|
|
Key.Right => NavigationDirection.Right,
|
|
Key.Up => NavigationDirection.Up,
|
|
Key.Down => NavigationDirection.Down,
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
};
|
|
e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
|
|
}
|
|
}
|
|
|
|
private static bool HandlePreCustomNavigation(
|
|
ICustomKeyboardNavigation customHandler,
|
|
IInputElement element,
|
|
NavigationDirection direction,
|
|
[NotNullWhen(true)] out IInputElement? result)
|
|
{
|
|
var (handled, next) = customHandler.GetNext(element, direction);
|
|
|
|
if (handled)
|
|
{
|
|
if (next is not null)
|
|
{
|
|
result = next;
|
|
return true;
|
|
}
|
|
|
|
var r = direction switch
|
|
{
|
|
NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
|
|
NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
|
|
_ => null
|
|
};
|
|
|
|
if (r is not null)
|
|
{
|
|
result = r;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
private static bool HandlePostCustomNavigation(
|
|
IInputElement element,
|
|
IInputElement? newElement,
|
|
NavigationDirection direction,
|
|
[NotNullWhen(true)] out IInputElement? result)
|
|
{
|
|
if (newElement is Visual v)
|
|
{
|
|
var customHandler = v.FindAncestorOfType<ICustomKeyboardNavigation>(true);
|
|
|
|
if (customHandler is object)
|
|
{
|
|
var (handled, next) = customHandler.GetNext(element, direction);
|
|
|
|
if (handled && next is object)
|
|
{
|
|
result = next;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|