// -----------------------------------------------------------------------
//
// 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;
}
}
}