using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Input.Navigation;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
///
/// Handles keyboard navigation for a window.
///
public class KeyboardNavigationHandler : IKeyboardNavigationHandler
{
///
/// The window to which the handler belongs.
///
private IInputRoot? _owner;
///
/// Sets the owner of the keyboard navigation handler.
///
/// The owner.
///
/// This method can only be called once, typically by the owner itself on creation.
///
public void SetOwner(IInputRoot owner)
{
if (_owner != null)
{
throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
}
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
_owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown);
}
///
/// Gets the next control in the specified navigation direction.
///
/// The element.
/// The navigation direction.
///
/// The next element in the specified direction, or null if
/// was the last in the requested direction.
///
public static IInputElement? GetNext(
IInputElement element,
NavigationDirection direction)
{
element = element ?? throw new ArgumentNullException(nameof(element));
// If there's a custom keyboard navigation handler as an ancestor, use that.
var custom = (element as Visual)?.FindAncestorOfType(true);
if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce))
return ce;
var result = direction switch
{
NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
_ => throw new NotSupportedException(),
};
// 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(element, result, direction, out ce))
return ce;
return result;
}
///
/// Moves the focus in the specified direction.
///
/// The current element.
/// The direction to move.
/// Any key modifiers active at the time of focus.
public void Move(
IInputElement element,
NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None)
{
element = element ?? throw new ArgumentNullException(nameof(element));
var next = GetNext(element, direction);
if (next != null)
{
var method = direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous ?
NavigationMethod.Tab : NavigationMethod.Directional;
FocusManager.Instance?.Focus(next, method, keyModifiers);
}
}
///
/// Handles the Tab key being pressed in the window.
///
/// The event sender.
/// The event args.
protected virtual void OnKeyDown(object? sender, KeyEventArgs e)
{
var current = FocusManager.Instance?.Current;
if (current != null && e.Key == Key.Tab)
{
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
}
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(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;
}
}
}