Browse Source

More work on tab/arrow key navigation.

Getting there, but still not quite right.
pull/72/merge
Steven Kirk 11 years ago
parent
commit
a3219f9170
  1. 212
      Perspex.Input/KeyboardNavigationHandler.cs
  2. 233
      Perspex.Input/Navigation/DirectionalNavigation.cs
  3. 28
      Perspex.Input/Navigation/FocusExtensions.cs
  4. 239
      Perspex.Input/Navigation/TabNavigation.cs
  5. 3
      Perspex.Input/Perspex.Input.csproj
  6. 13
      Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Arrows.cs
  7. 43
      Tests/Perspex.Input.UnitTests/KeyboardNavigationTests_Tab.cs

212
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;
/// <summary>
/// Handles keyboard navigation for a window.
@ -57,37 +55,13 @@ namespace Perspex.Input
{
Contract.Requires<ArgumentNullException>(element != null);
var container = element.GetVisualParent<IInputElement>();
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
}
}
/// <summary>
/// Checks if the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
/// <summary>
/// Checks if a descendent of the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if a descendent of the element can be focused.</returns>
private static bool CanFocusDescendent(IInputElement e) => e.IsEnabledCore && e.IsVisible;
/// <summary>
/// Gets the first or last focusable descendent of the specified element.
/// </summary>
/// <param name="container">The element.</param>
/// <param name="forward">Whether to search forward or backwards.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendent(IInputElement container, bool forward)
{
return forward ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
/// <summary>
/// Gets the focusable descendents of the specified element, depending on the element's
/// <see cref="KeyboardNavigation.TabNavigationProperty"/>.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The element's focusable descendents.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendents(IInputElement element)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
if (mode == KeyboardNavigationMode.None)
{
yield break;
}
var children = element.GetVisualChildren().OfType<IInputElement>();
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;
}
}
}
}
/// <summary>
/// Gets the next item that should be focused in the specified container.
/// </summary>
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <returns>The next element, or null if the element is the last.</returns>
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;
}
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="forward">Whether to search forward or backwards.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(IInputElement container, bool forward)
{
var parent = container.GetVisualParent<IInputElement>();
IInputElement next = null;
if (parent != null)
{
var siblings = parent.GetVisualChildren()
.OfType<IInputElement>()
.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;
}
/// <summary>
/// Handles the Tab key being pressed in the window.
/// </summary>

233
Perspex.Input/Navigation/DirectionalNavigation.cs

@ -0,0 +1,233 @@
// -----------------------------------------------------------------------
// <copyright file="DirectionalNavigation.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Input.Navigation
{
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.VisualTree;
/// <summary>
/// The implementation for default directional navigation.
/// </summary>
public static class DirectionalNavigation
{
/// <summary>
/// Gets the next control in the specified navigation direction.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The navigation direction.</param>
/// <returns>
/// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement GetNext(
IInputElement element,
FocusNavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction != FocusNavigationDirection.Next &&
direction != FocusNavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
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();
}
}
/// <summary>
/// Returns a value indicting whether the specified direction is forward.
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>True if the direction is forward.</returns>
private static bool IsForward(FocusNavigationDirection direction)
{
return direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Last ||
direction == FocusNavigationDirection.Right ||
direction == FocusNavigationDirection.Down;
}
/// <summary>
/// Gets the first or last focusable descendent of the specified element.
/// </summary>
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
{
return IsForward(direction) ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
/// <summary>
/// Gets the focusable descendents of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The element's focusable descendents.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendents(IInputElement element)
{
var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)element);
var children = element.GetVisualChildren().OfType<IInputElement>();
foreach (var child in children)
{
if (child.CanFocus())
{
yield return child;
}
if (child.CanFocusDescendents())
{
foreach (var descendent in GetFocusableDescendents(child))
{
yield return descendent;
}
}
}
}
/// <summary>
/// Gets the next item that should be focused in the specified container.
/// </summary>
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <returns>The next element, or null if the element is the last.</returns>
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;
}
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
var isForward = IsForward(direction);
IInputElement next = null;
if (parent != null)
{
var siblings = parent.GetVisualChildren()
.OfType<IInputElement>()
.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;
}
}
}

28
Perspex.Input/Navigation/FocusExtensions.cs

@ -0,0 +1,28 @@
// -----------------------------------------------------------------------
// <copyright file="FocusExtensions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Input.Navigation
{
/// <summary>
/// Provides extension methods relating to control focus.
/// </summary>
internal static class FocusExtensions
{
/// <summary>
/// Checks if the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
/// <summary>
/// Checks if descendents of the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if descendents of the element can be focused.</returns>
public static bool CanFocusDescendents(this IInputElement e) => e.IsEnabledCore && e.IsVisible;
}
}

239
Perspex.Input/Navigation/TabNavigation.cs

@ -0,0 +1,239 @@
// -----------------------------------------------------------------------
// <copyright file="TabNavigation.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Input.Navigation
{
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.VisualTree;
/// <summary>
/// The implementation for default tab navigation.
/// </summary>
internal static class TabNavigation
{
/// <summary>
/// Gets the next control in the specified tab direction.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <returns>
/// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement GetNextInTabOrder(
IInputElement element,
FocusNavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
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();
}
}
/// <summary>
/// Gets the first or last focusable descendent of the specified element.
/// </summary>
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction)
{
return direction == FocusNavigationDirection.Next ?
GetFocusableDescendents(container).FirstOrDefault() :
GetFocusableDescendents(container).LastOrDefault();
}
/// <summary>
/// Gets the focusable descendents of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The element's focusable descendents.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendents(IInputElement element)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
if (mode == KeyboardNavigationMode.None)
{
yield break;
}
var children = element.GetVisualChildren().OfType<IInputElement>();
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;
}
}
}
}
/// <summary>
/// Gets the next item that should be focused in the specified container.
/// </summary>
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <returns>The next element, or null if the element is the last.</returns>
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;
}
/// <summary>
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
IInputElement next = null;
if (parent != null)
{
var siblings = parent.GetVisualChildren()
.OfType<IInputElement>()
.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;
}
}
}

3
Perspex.Input/Perspex.Input.csproj

@ -85,6 +85,9 @@
<Compile Include="KeyboardNavigation.cs" />
<Compile Include="KeyboardNavigationHandler.cs" />
<Compile Include="GotFocusEventArgs.cs" />
<Compile Include="Navigation\FocusExtensions.cs" />
<Compile Include="Navigation\DirectionalNavigation.cs" />
<Compile Include="Navigation\TabNavigation.cs" />
<Compile Include="Platform\IPlatformSettings.cs" />
<Compile Include="PointerPressEventArgs.cs" />
<Compile Include="PointerWheelEventArgs.cs" />

13
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" },

43
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()

Loading…
Cancel
Save