// ----------------------------------------------------------------------- // // Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Input.Navigation { using System; using System.Collections.Generic; using System.Linq; using Perspex.VisualTree; /// /// The implementation for default directional navigation. /// public static class DirectionalNavigation { /// /// 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, FocusNavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( direction != FocusNavigationDirection.Next && direction != FocusNavigationDirection.Previous); var container = element.GetVisualParent(); if (container != null) { var isForward = IsForward(direction); var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); switch (mode) { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container, direction) ?? GetFirstInNextContainer(element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? GetFocusableDescendent(container, direction); case KeyboardNavigationMode.Contained: return GetNextInContainer(element, container, direction); default: return null; } } else { return GetFocusableDescendents(element).FirstOrDefault(); } } /// /// Returns a value indicting whether the specified direction is forward. /// /// The direction. /// True if the direction is forward. private static bool IsForward(FocusNavigationDirection direction) { return direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Last || direction == FocusNavigationDirection.Right || direction == FocusNavigationDirection.Down; } /// /// Gets the first or last focusable descendent of the specified element. /// /// The element. /// The direction to search. /// The element or null if not found.## private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) { return IsForward(direction) ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } /// /// Gets the focusable descendents of the specified element. /// /// The element. /// The element's focusable descendents. private static IEnumerable GetFocusableDescendents(IInputElement element) { var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)element); var children = element.GetVisualChildren().OfType(); foreach (var child in children) { if (child.CanFocus()) { yield return child; } if (child.CanFocusDescendents()) { foreach (var descendent in GetFocusableDescendents(child)) { yield return descendent; } } } } /// /// Gets the next item that should be focused in the specified container. /// /// The starting element/ /// The container. /// The direction. /// The next element, or null if the element is the last. private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, FocusNavigationDirection direction) { if (direction == FocusNavigationDirection.Down) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); if (descendent != null) { return descendent; } } if (container != null) { var navigable = container as INavigableContainer; if (navigable != null) { while (element != null) { element = navigable.GetControl(direction, element); 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 == FocusNavigationDirection.Up) { var descendent = GetFocusableDescendents(element).LastOrDefault(); if (descendent != null) { return descendent; } } return element; } return null; } /// /// Gets the first item that should be focused in the next container. /// /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, FocusNavigationDirection direction) { var parent = container.GetVisualParent(); var isForward = IsForward(direction); IInputElement next = null; if (parent != null) { if (!isForward && parent.CanFocus()) { return parent; } var siblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendents); IInputElement sibling; if (isForward) { sibling = siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault(); } else { sibling = siblings.TakeWhile(x => x != container).LastOrDefault(); } if (sibling != null) { if (sibling.CanFocus()) { next = sibling; } else { next = isForward ? GetFocusableDescendents(sibling).FirstOrDefault() : GetFocusableDescendents(sibling).LastOrDefault(); } } if (next == null) { next = GetFirstInNextContainer(parent, direction); } } else { next = isForward ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } return next; } } }