// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Input.Navigation
{
///
/// 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.
///
/// If true will not descend into to find next control.
///
///
/// The next element in the specified direction, or null if
/// was the last in the requested direction.
///
public static IInputElement GetNextInTabOrder(
IInputElement element,
NavigationDirection direction,
bool outsideElement = false)
{
Contract.Requires(element != null);
Contract.Requires(
direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous);
var container = element.GetVisualParent();
if (container != null)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)container);
switch (mode)
{
case KeyboardNavigationMode.Continue:
return GetNextInContainer(element, container, direction, outsideElement) ??
GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
return GetNextInContainer(element, container, direction, outsideElement) ??
GetFocusableDescendant(container, direction);
case KeyboardNavigationMode.Contained:
return GetNextInContainer(element, container, direction, outsideElement);
default:
return GetFirstInNextContainer(element, container, direction);
}
}
else
{
return GetFocusableDescendants(element, direction).FirstOrDefault();
}
}
///
/// Gets the first or last focusable descendant of the specified element.
///
/// The element.
/// The direction to search.
/// The element or null if not found.##
private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
{
return direction == NavigationDirection.Next ?
GetFocusableDescendants(container, direction).FirstOrDefault() :
GetFocusableDescendants(container, direction).LastOrDefault();
}
///
/// Gets the focusable descendants of the specified element.
///
/// The element.
/// The tab direction. Must be Next or Previous.
/// The element's focusable descendants.
private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction)
{
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)
{
var customNext = GetCustomNext(child, direction);
if (customNext.handled)
{
yield return customNext.next;
}
else
{
if (child.CanFocus())
{
yield return child;
}
if (child.CanFocusDescendants())
{
foreach (var descendant in GetFocusableDescendants(child, direction))
{
yield return descendant;
}
}
}
}
}
///
/// Gets the next item that should be focused in the specified container.
///
/// The starting element/
/// The container.
/// The direction.
///
/// If true will not descend into to find next control.
///
/// The next element, or null if the element is the last.
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction,
bool outsideElement)
{
if (direction == NavigationDirection.Next && !outsideElement)
{
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
if (descendant != null)
{
return descendant;
}
}
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, false);
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 == NavigationDirection.Previous)
{
var descendant = GetFocusableDescendants(element, direction).LastOrDefault();
if (descendant != null)
{
return descendant;
}
}
return element;
}
return null;
}
///
/// Gets the first item that should be focused in the next container.
///
/// The element being navigated away from.
/// The container.
/// The direction of the search.
/// The first element, or null if there are no more elements.
private static IInputElement GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
var parent = container.GetVisualParent();
IInputElement next = null;
if (parent != null)
{
if (direction == NavigationDirection.Previous && parent.CanFocus())
{
return parent;
}
var allSiblings = parent.GetVisualChildren()
.OfType()
.Where(FocusExtensions.CanFocusDescendants);
var siblings = direction == NavigationDirection.Next ?
allSiblings.SkipWhile(x => x != container).Skip(1) :
allSiblings.TakeWhile(x => x != container).Reverse();
foreach (var sibling in siblings)
{
var customNext = GetCustomNext(sibling, direction);
if (customNext.handled)
{
return customNext.next;
}
if (sibling.CanFocus())
{
return sibling;
}
else
{
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(sibling, direction).FirstOrDefault() :
GetFocusableDescendants(sibling, direction).LastOrDefault();
if(next != null)
{
return next;
}
}
}
if (next == null)
{
next = GetFirstInNextContainer(element, parent, direction);
}
}
else
{
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(container, direction).FirstOrDefault() :
GetFocusableDescendants(container, direction).LastOrDefault();
}
return next;
}
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction)
{
if (element is ICustomKeyboardNavigation custom)
{
return custom.GetNext(element, direction);
}
return (false, null);
}
}
}