// ----------------------------------------------------------------------- // // 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 element in tab order. /// /// The element. /// The next element in tab order. public static IInputElement GetNextInTabOrder(IInputElement element) { Contract.Requires(element != null); var container = element.GetVisualParent(); if (container != null) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); switch (mode) { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container) ?? GetFirstInNextContainer(container); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container) ?? GetDescendents(container).FirstOrDefault(); default: return GetFirstInNextContainer(container); } } else { return GetDescendents(element).FirstOrDefault(); } } /// /// Gets the next element in tab order. /// /// The element. /// The next element in tab order. public static IInputElement GetPreviousInTabOrder(IInputElement element) { Contract.Requires(element != null); var container = element.GetVisualParent(); if (container != null) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); switch (mode) { case KeyboardNavigationMode.Continue: return GetPreviousInContainer(element, container) ?? GetLastInPreviousContainer(element); case KeyboardNavigationMode.Cycle: return GetPreviousInContainer(element, container) ?? GetDescendents(container).LastOrDefault(); default: return GetLastInPreviousContainer(container); } } else { return GetDescendents(element).LastOrDefault(); } } /// /// Moves the focus to the next control in tab order. /// /// The current element. public void TabNext(IInputElement element) { Contract.Requires(element != null); var next = GetNextInTabOrder(element); 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 = GetPreviousInTabOrder(element); 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 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.Never) { 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 next element, or null if the element is the last. private static IInputElement GetNextInContainer(IInputElement element, IInputElement container) { var descendent = GetDescendents(element).FirstOrDefault(); if (descendent != null) { return descendent; } else if (container != null) { var sibling = container.GetVisualChildren() .OfType() .Where(CanFocus) .SkipWhile(x => x != element) .Skip(1) .FirstOrDefault(); if (sibling != null) { return sibling; } } return null; } /// /// Gets the previous item that should be focused in the specified container. /// /// The starting element/ /// The container. /// The previous element, or null if the element is the first. private static IInputElement GetPreviousInContainer(IInputElement element, IInputElement container) { return container.GetVisualChildren() .OfType() .Where(CanFocus) .TakeWhile(x => x != element) .LastOrDefault(); } /// /// Gets the first item that should be focused in the next container. /// /// The container. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer(IInputElement container) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { var sibling = parent.GetVisualChildren() .OfType() .Where(CanFocusDescendent) .SkipWhile(x => x != container) .Skip(1) .FirstOrDefault(); if (sibling != null) { if (CanFocus(sibling)) { next = sibling; } else { next = GetDescendents(sibling).FirstOrDefault(); } } if (next == null) { next = GetFirstInNextContainer(parent); } } else { next = GetDescendents(container).FirstOrDefault(); } return next; } /// /// Gets the last item that should be focused in the previous container. /// /// The container. /// The next element, or null if there are no more elements. private static IInputElement GetLastInPreviousContainer(IInputElement container) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { var sibling = parent.GetVisualChildren() .OfType() .Where(CanFocusDescendent) .TakeWhile(x => x != container) .LastOrDefault(); if (sibling != null) { if (CanFocus(sibling)) { next = sibling; } else { next = GetDescendents(sibling).LastOrDefault(); } } if (next == null) { next = GetLastInPreviousContainer(parent); } } else { next = 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); } } } } }