diff --git a/Perspex.Input/KeyboardNavigationHandler.cs b/Perspex.Input/KeyboardNavigationHandler.cs index 1825af71e0..196cdab107 100644 --- a/Perspex.Input/KeyboardNavigationHandler.cs +++ b/Perspex.Input/KeyboardNavigationHandler.cs @@ -7,9 +7,7 @@ namespace Perspex.Input { using System; - using System.Collections.Generic; - using System.Linq; - using Perspex.VisualTree; + using Perspex.Input.Navigation; /// /// Handles keyboard navigation for a window. @@ -57,37 +55,13 @@ namespace Perspex.Input { Contract.Requires(element != null); - var container = element.GetVisualParent(); - - if (container != null) + if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) { - var tab = direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous; - var forward = direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Last || - direction == FocusNavigationDirection.Right || - direction == FocusNavigationDirection.Down; - var mode = tab ? - KeyboardNavigation.GetTabNavigation((InputElement)container) : - KeyboardNavigation.GetDirectionalNavigation((InputElement)container); - - switch (mode) - { - case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, forward); - case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction) ?? - GetFocusableDescendent(container, forward); - case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction); - default: - return tab ? GetFirstInNextContainer(container, forward) : null; - } + return TabNavigation.GetNextInTabOrder(element, direction); } else { - return GetFocusableDescendents(element).FirstOrDefault(); + return DirectionalNavigation.GetNext(element, direction); } } @@ -108,184 +82,6 @@ namespace Perspex.Input } } - /// - /// 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 GetFocusableDescendent(IInputElement container, bool forward) - { - return forward ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(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 GetFocusableDescendents(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 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.Next || direction == FocusNavigationDirection.Down) - { - var descendent = GetFocusableDescendents(element).FirstOrDefault(); - - if (descendent != null) - { - return descendent; - } - } - - 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 ? - GetFocusableDescendents(sibling).FirstOrDefault() : - GetFocusableDescendents(sibling).LastOrDefault(); - } - } - - if (next == null) - { - next = GetFirstInNextContainer(parent, forward); - } - } - else - { - next = forward ? - GetFocusableDescendents(container).FirstOrDefault() : - GetFocusableDescendents(container).LastOrDefault(); - } - - return next; - } - /// /// Handles the Tab key being pressed in the window. /// diff --git a/Perspex.Input/Navigation/DirectionalNavigation.cs b/Perspex.Input/Navigation/DirectionalNavigation.cs new file mode 100644 index 0000000000..2248a09ee3 --- /dev/null +++ b/Perspex.Input/Navigation/DirectionalNavigation.cs @@ -0,0 +1,233 @@ +// ----------------------------------------------------------------------- +// +// 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; + + // TODO: Do a spatial search here if the container doesn't implement + // INavigableContainer. + if (navigable != null) + { + while (element != null) + { + element = navigable.GetControl(direction, element); + + if (element != null && element.CanFocus()) + { + break; + } + } + } + + 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) + { + 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; + } + } +} diff --git a/Perspex.Input/Navigation/FocusExtensions.cs b/Perspex.Input/Navigation/FocusExtensions.cs new file mode 100644 index 0000000000..995f9e9b41 --- /dev/null +++ b/Perspex.Input/Navigation/FocusExtensions.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Input.Navigation +{ + /// + /// Provides extension methods relating to control focus. + /// + internal static class FocusExtensions + { + /// + /// Checks if the specified element can be focused. + /// + /// The element. + /// True if the element can be focused. + public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + + /// + /// Checks if descendents of the specified element can be focused. + /// + /// The element. + /// True if descendents of the element can be focused. + public static bool CanFocusDescendents(this IInputElement e) => e.IsEnabledCore && e.IsVisible; + } +} diff --git a/Perspex.Input/Navigation/TabNavigation.cs b/Perspex.Input/Navigation/TabNavigation.cs new file mode 100644 index 0000000000..9bb77bbfc8 --- /dev/null +++ b/Perspex.Input/Navigation/TabNavigation.cs @@ -0,0 +1,239 @@ +// ----------------------------------------------------------------------- +// +// 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 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. + /// + /// The next element in the specified direction, or null if + /// was the last in the requested direction. + /// + public static IInputElement GetNextInTabOrder( + IInputElement element, + FocusNavigationDirection direction) + { + Contract.Requires(element != null); + Contract.Requires( + direction == FocusNavigationDirection.Next || + direction == FocusNavigationDirection.Previous); + + var container = element.GetVisualParent(); + + if (container != null) + { + var mode = KeyboardNavigation.GetTabNavigation((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 GetFirstInNextContainer(container, direction); + } + } + else + { + return GetFocusableDescendents(element).FirstOrDefault(); + } + } + + /// + /// 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 direction == FocusNavigationDirection.Next ? + 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.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 (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.Next) + { + var descendent = GetFocusableDescendents(element).FirstOrDefault(); + + if (descendent != null) + { + return descendent; + } + } + + 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); + + if (element != null && element.CanFocus()) + { + break; + } + } + } + + if (element != null && direction == FocusNavigationDirection.Previous) + { + 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(); + IInputElement next = null; + + if (parent != null) + { + var siblings = parent.GetVisualChildren() + .OfType() + .Where(FocusExtensions.CanFocusDescendents); + IInputElement sibling; + + if (direction == FocusNavigationDirection.Next) + { + 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 = direction == FocusNavigationDirection.Next ? + GetFocusableDescendents(sibling).FirstOrDefault() : + GetFocusableDescendents(sibling).LastOrDefault(); + } + } + + if (next == null) + { + next = GetFirstInNextContainer(parent, direction); + } + } + else + { + next = direction == FocusNavigationDirection.Next ? + GetFocusableDescendents(container).FirstOrDefault() : + GetFocusableDescendents(container).LastOrDefault(); + } + + return next; + } + } +} diff --git a/Perspex.Input/Perspex.Input.csproj b/Perspex.Input/Perspex.Input.csproj index ab2e219cf1..ee7ea83dd7 100644 --- a/Perspex.Input/Perspex.Input.csproj +++ b/Perspex.Input/Perspex.Input.csproj @@ -85,6 +85,9 @@ + + + diff --git a/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Arrows.cs index 05c1fbccdc..0bd1b0460e 100644 --- a/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Arrows.cs @@ -148,6 +148,7 @@ namespace Perspex.Input.UnitTests }, new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, Children = new Controls { (next = new Button { Name = "Button4" }), @@ -191,6 +192,7 @@ namespace Perspex.Input.UnitTests var top = new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, Children = new Controls { new StackPanel @@ -327,6 +329,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, @@ -365,6 +368,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, @@ -402,6 +406,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, @@ -440,6 +445,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, @@ -538,11 +544,11 @@ namespace Perspex.Input.UnitTests { new StackPanel { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, Children = new Controls { (container = new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, Children = new Controls { new Button { Name = "Button1" }, @@ -599,6 +605,7 @@ namespace Perspex.Input.UnitTests }, new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, Children = new Controls { new Button { Name = "Button4" }, @@ -637,6 +644,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, Children = new Controls { new Button { Name = "Button4" }, @@ -675,6 +683,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, Children = new Controls { new Button { Name = "Button4" }, @@ -713,6 +722,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, @@ -750,6 +760,7 @@ namespace Perspex.Input.UnitTests }), new StackPanel { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, Children = new Controls { new Button { Name = "Button4" }, diff --git a/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 606e6c9d9f..3214ac245c 100644 --- a/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -84,6 +84,49 @@ namespace Perspex.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Continue_Doesnt_Enter_Panel_With_TabNavigation_None() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + (next = new Button { Name = "Button1" }), + new Button { Name = "Button2" }, + (current = new Button { Name = "Button3" }), + } + }), + new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, + Children = new Controls + { + new StackPanel + { + Children = new Controls + { + new Button { Name = "Button4" }, + new Button { Name = "Button5" }, + new Button { Name = "Button6" }, + } + }, + } + } + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + + Assert.Equal(next, result); + } [Fact] public void Next_Continue_Returns_Next_Sibling()