using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Input.Navigation; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Handles keyboard navigation for a window. /// internal sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler { /// /// The window to which the handler belongs. /// private InputElement? _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. /// [PrivateApi] public void SetOwner(InputElement 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); } /// /// 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)); return GetNextPrivate(element, null, direction, null); } private static IInputElement? GetNextPrivate( IInputElement? element, InputElement? 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(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; } /// public bool Move( IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None, KeyDeviceType? deviceType = null) { 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; } /// /// Handles the Tab key being pressed in the window. /// /// The event sender. /// The event args. 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 = Move(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 = Move(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(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; } } }