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