// 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 { /// /// The implementation for default tab navigation. /// internal static class TabNavigation { /// /// Gets the next control in the specified tab direction. /// /// The element. /// The tab direction. Must be Next or Previous. /// /// If true will not descend into to find next control. /// /// /// The next element in the specified direction, or null if /// was the last in the requested direction. /// public static IInputElement GetNextInTabOrder( IInputElement element, NavigationDirection direction, bool outsideElement = false) { Contract.Requires(element != null); Contract.Requires( direction == NavigationDirection.Next || direction == NavigationDirection.Previous); var container = element.GetVisualParent(); if (container != null) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); switch (mode) { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container, direction, outsideElement) ?? GetFirstInNextContainer(element, element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction, outsideElement) ?? GetFocusableDescendant(container, direction); case KeyboardNavigationMode.Contained: return GetNextInContainer(element, container, direction, outsideElement); default: return GetFirstInNextContainer(element, container, direction); } } else { return GetFocusableDescendants(element, direction).FirstOrDefault(); } } /// /// Gets the first or last focusable descendant of the specified element. /// /// The element. /// The direction to search. /// The element or null if not found.## private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) { return direction == NavigationDirection.Next ? GetFocusableDescendants(container, direction).FirstOrDefault() : GetFocusableDescendants(container, direction).LastOrDefault(); } /// /// Gets the focusable descendants of the specified element. /// /// The element. /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); if (mode == KeyboardNavigationMode.None) { yield break; } var children = element.GetVisualChildren().OfType(); if (mode == KeyboardNavigationMode.Once) { var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element); if (active != null) { yield return active; yield break; } else { children = children.Take(1); } } foreach (var child in children) { var customNext = GetCustomNext(child, direction); if (customNext.handled) { yield return customNext.next; } else { if (child.CanFocus()) { yield return child; } if (child.CanFocusDescendants()) { foreach (var descendant in GetFocusableDescendants(child, direction)) { yield return descendant; } } } } } /// /// Gets the next item that should be focused in the specified container. /// /// The starting element/ /// The container. /// The direction. /// /// If true will not descend into to find next control. /// /// The next element, or null if the element is the last. private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, NavigationDirection direction, bool outsideElement) { if (direction == NavigationDirection.Next && !outsideElement) { var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); if (descendant != null) { return descendant; } } if (container != null) { var navigable = container as INavigableContainer; // TODO: Do a spatial search here if the container doesn't implement // INavigableContainer. if (navigable != null) { while (element != null) { element = navigable.GetControl(direction, element, false); 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.Previous) { var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); if (descendant != null) { return descendant; } } return element; } return null; } /// /// Gets the first item that should be focused in the next container. /// /// The element being navigated away from. /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement element, IInputElement container, NavigationDirection direction) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { if (direction == NavigationDirection.Previous && parent.CanFocus()) { return parent; } var allSiblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendants); var siblings = direction == NavigationDirection.Next ? allSiblings.SkipWhile(x => x != container).Skip(1) : allSiblings.TakeWhile(x => x != container).Reverse(); foreach (var sibling in siblings) { var customNext = GetCustomNext(sibling, direction); if (customNext.handled) { return customNext.next; } if (sibling.CanFocus()) { return sibling; } else { next = direction == NavigationDirection.Next ? GetFocusableDescendants(sibling, direction).FirstOrDefault() : GetFocusableDescendants(sibling, direction).LastOrDefault(); if(next != null) { return next; } } } if (next == null) { next = GetFirstInNextContainer(element, parent, direction); } } else { next = direction == NavigationDirection.Next ? GetFocusableDescendants(container, direction).FirstOrDefault() : GetFocusableDescendants(container, direction).LastOrDefault(); } return next; } private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) { return custom.GetNext(element, direction); } return (false, null); } } }