// ----------------------------------------------------------------------- // // Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Input { using System; using System.Collections.Generic; using System.Linq; using Perspex.VisualTree; /// /// 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) { Contract.Requires(owner != null); if (this.owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } this.owner = owner; this.owner.AddHandler(InputElement.KeyDownEvent, this.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 therequested direction. /// public static IInputElement GetNext( IInputElement element, FocusNavigationDirection direction) { Contract.Requires(element != null); var container = element.GetVisualParent(); if (container != null) { KeyboardNavigationMode mode; if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) { mode = KeyboardNavigation.GetTabNavigation((InputElement)container); } else { mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); } bool forward = direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Last || direction == FocusNavigationDirection.Right || direction == FocusNavigationDirection.Down; switch (mode) { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container, direction) ?? GetFirstInNextContainer(element, forward); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? GetDescendent(container, forward); case KeyboardNavigationMode.Contained: return GetNextInContainer(element, container, direction); default: return GetFirstInNextContainer(container, forward); } } else { return GetDescendents(element).FirstOrDefault(); } } /// /// Moves the focus to the next control in tab order. /// /// The current element. public void TabNext(IInputElement element) { Contract.Requires(element != null); var next = GetNext(element, FocusNavigationDirection.Next); if (next != null) { FocusManager.Instance.Focus(next, true); } } /// /// Moves the focus to the previous control in tab order. /// /// The current element. public void TabPrevious(IInputElement element) { Contract.Requires(element != null); var next = GetNext(element, FocusNavigationDirection.Previous); if (next != null) { FocusManager.Instance.Focus(next, true); } } /// /// Checks if the specified element can be focused. /// /// The element. /// True if the element can be focused. private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; /// /// Checks if a descendent of the specified element can be focused. /// /// The element. /// True if a descendent of the element can be focused. private static bool CanFocusDescendent(IInputElement e) => e.IsEnabledCore && e.IsVisible; /// /// Gets the first or last focusable descendent of the specified element. /// /// The element. /// Whether to search forward or backwards. /// The element or null if not found.## private static IInputElement GetDescendent(IInputElement container, bool forward) { return forward ? GetDescendents(container).FirstOrDefault() : GetDescendents(container).LastOrDefault(); } /// /// Gets the focusable descendents of the specified element, depending on the element's /// . /// /// The element. /// The element's focusable descendents. private static IEnumerable GetDescendents(IInputElement element) { 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) { if (CanFocus(child)) { yield return child; } if (CanFocusDescendent(child)) { foreach (var descendent in GetDescendents(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) { var descendent = GetDescendents(element).FirstOrDefault(); if (descendent != null) { return descendent; } else if (container != null) { var navigable = container as INavigableContainer; // TODO: Do a spatial search here. if (navigable != null) { while (element != null) { var sibling = navigable.GetControl(direction, element); if (sibling != null && CanFocus(sibling)) { return sibling; } element = sibling; } } } return null; } /// /// Gets the first item that should be focused in the next container. /// /// The container. /// Whether to search forward or backwards. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer(IInputElement container, bool forward) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { var siblings = parent.GetVisualChildren() .OfType() .Where(CanFocusDescendent); IInputElement sibling; if (forward) { sibling = siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault(); } else { sibling = siblings.TakeWhile(x => x != container).LastOrDefault(); } if (sibling != null) { if (CanFocus(sibling)) { next = sibling; } else { next = forward ? GetDescendents(sibling).FirstOrDefault() : GetDescendents(sibling).LastOrDefault(); } } if (next == null) { next = GetFirstInNextContainer(parent, forward); } } else { next = forward ? GetDescendents(container).FirstOrDefault() : GetDescendents(container).LastOrDefault(); } return next; } /// /// 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 (e.Key == Key.Tab && current != null) { if ((KeyboardDevice.Instance.Modifiers & ModifierKeys.Shift) == 0) { this.TabNext(current); } else { this.TabPrevious(current); } } } } }