|
|
|
@ -10,277 +10,663 @@ namespace Avalonia.Input.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>
|
|
|
|
/// <param name="outsideElement">
|
|
|
|
/// If true will not descend into <paramref name="element"/> to find next control.
|
|
|
|
/// </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, |
|
|
|
NavigationDirection direction, |
|
|
|
bool outsideElement = false) |
|
|
|
public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly) |
|
|
|
{ |
|
|
|
element = element ?? throw new ArgumentNullException(nameof(element)); |
|
|
|
|
|
|
|
if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous) |
|
|
|
{ |
|
|
|
throw new ArgumentException("Invalid direction: must be Next or Previous."); |
|
|
|
} |
|
|
|
return GetNextTab(e, GetGroupParent(e), goDownOnly); |
|
|
|
} |
|
|
|
|
|
|
|
var container = element.GetVisualParent<IInputElement>(); |
|
|
|
public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly) |
|
|
|
{ |
|
|
|
var tabbingType = GetKeyNavigationMode(container); |
|
|
|
|
|
|
|
if (container != null) |
|
|
|
if (e == null) |
|
|
|
{ |
|
|
|
var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); |
|
|
|
if (IsTabStop(container)) |
|
|
|
return container; |
|
|
|
|
|
|
|
switch (mode) |
|
|
|
// Using ActiveElement if set
|
|
|
|
var activeElement = GetActiveElement(container); |
|
|
|
if (activeElement != null) |
|
|
|
return GetNextTab(null, activeElement, true); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) |
|
|
|
{ |
|
|
|
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); |
|
|
|
if (container != e) |
|
|
|
{ |
|
|
|
if (goDownOnly) |
|
|
|
return null; |
|
|
|
var parentContainer = GetGroupParent(container); |
|
|
|
return GetNextTab(container, parentContainer, goDownOnly); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
// All groups
|
|
|
|
IInputElement? loopStartElement = null; |
|
|
|
var nextTabElement = e; |
|
|
|
var currentTabbingType = tabbingType; |
|
|
|
|
|
|
|
// Search down inside the container
|
|
|
|
while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null) |
|
|
|
{ |
|
|
|
return GetFocusableDescendants(element, direction).FirstOrDefault(); |
|
|
|
// Avoid the endless loop here for Cycle groups
|
|
|
|
if (loopStartElement == nextTabElement) |
|
|
|
break; |
|
|
|
if (loopStartElement == null) |
|
|
|
loopStartElement = nextTabElement; |
|
|
|
|
|
|
|
var firstTabElementInside = GetNextTab(null, nextTabElement, true); |
|
|
|
if (firstTabElementInside != null) |
|
|
|
return firstTabElementInside; |
|
|
|
|
|
|
|
// If we want to continue searching inside the Once groups, we should change the navigation mode
|
|
|
|
if (currentTabbingType == KeyboardNavigationMode.Once) |
|
|
|
currentTabbingType = KeyboardNavigationMode.Contained; |
|
|
|
} |
|
|
|
|
|
|
|
// If there is no next element in the group (nextTabElement == null)
|
|
|
|
|
|
|
|
// Search up in the tree if allowed
|
|
|
|
// consider: Use original tabbingType instead of currentTabbingType
|
|
|
|
if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null) |
|
|
|
{ |
|
|
|
return GetNextTab(container, GetGroupParent(container), false); |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the first or last focusable descendant 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 GetFocusableDescendant(IInputElement container, NavigationDirection direction) |
|
|
|
public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) |
|
|
|
{ |
|
|
|
return direction == NavigationDirection.Next ? |
|
|
|
GetFocusableDescendants(container, direction).FirstOrDefault() : |
|
|
|
GetFocusableDescendants(container, direction).LastOrDefault(); |
|
|
|
if (e is IInputElement container) |
|
|
|
{ |
|
|
|
var last = GetLastInTree(container); |
|
|
|
|
|
|
|
if (last is object) |
|
|
|
return GetNextTab(last, false); |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the focusable descendants of the specified element.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="element">The element.</param>
|
|
|
|
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
|
|
|
|
/// <returns>The element's focusable descendants.</returns>
|
|
|
|
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element, |
|
|
|
NavigationDirection direction) |
|
|
|
public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) |
|
|
|
{ |
|
|
|
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); |
|
|
|
if (e is null && container is null) |
|
|
|
throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); |
|
|
|
|
|
|
|
if (mode == KeyboardNavigationMode.None) |
|
|
|
{ |
|
|
|
yield break; |
|
|
|
} |
|
|
|
if (container is null) |
|
|
|
container = GetGroupParent(e!); |
|
|
|
|
|
|
|
var children = element.GetVisualChildren().OfType<IInputElement>(); |
|
|
|
KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); |
|
|
|
|
|
|
|
if (mode == KeyboardNavigationMode.Once) |
|
|
|
if (e == null) |
|
|
|
{ |
|
|
|
var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element); |
|
|
|
|
|
|
|
if (active != null) |
|
|
|
// Using ActiveElement if set
|
|
|
|
var activeElement = GetActiveElement(container); |
|
|
|
if (activeElement != null) |
|
|
|
return GetPrevTab(null, activeElement, true); |
|
|
|
else |
|
|
|
{ |
|
|
|
yield return active; |
|
|
|
yield break; |
|
|
|
// If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null
|
|
|
|
// then we want to go to the first item (not last) within the container
|
|
|
|
if (tabbingType == KeyboardNavigationMode.Once) |
|
|
|
{ |
|
|
|
var firstTabElement = GetNextTabInGroup(null, container, tabbingType); |
|
|
|
if (firstTabElement == null) |
|
|
|
{ |
|
|
|
if (IsTabStop(container)) |
|
|
|
return container; |
|
|
|
if (goDownOnly) |
|
|
|
return null; |
|
|
|
|
|
|
|
return GetPrevTab(container, null, false); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
return GetPrevTab(null, firstTabElement, true); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) |
|
|
|
{ |
|
|
|
children = children.Take(1); |
|
|
|
if (goDownOnly || container == e) |
|
|
|
return null; |
|
|
|
|
|
|
|
// FocusedElement should not be e otherwise we will delegate focus to the same element
|
|
|
|
if (IsTabStop(container)) |
|
|
|
return container; |
|
|
|
|
|
|
|
return GetPrevTab(container, null, false); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var child in children) |
|
|
|
// All groups (except Once) - continue
|
|
|
|
IInputElement? loopStartElement = null; |
|
|
|
IInputElement? nextTabElement = e; |
|
|
|
|
|
|
|
// Look for element with the same TabIndex before the current element
|
|
|
|
while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) |
|
|
|
{ |
|
|
|
var customNext = GetCustomNext(child, direction); |
|
|
|
if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local) |
|
|
|
break; |
|
|
|
|
|
|
|
// At this point nextTabElement is TabStop or TabGroup
|
|
|
|
// In case it is a TabStop only return the element
|
|
|
|
if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) |
|
|
|
return nextTabElement; |
|
|
|
|
|
|
|
// Avoid the endless loop here
|
|
|
|
if (loopStartElement == nextTabElement) |
|
|
|
break; |
|
|
|
if (loopStartElement == null) |
|
|
|
loopStartElement = nextTabElement; |
|
|
|
|
|
|
|
// At this point nextTabElement is TabGroup
|
|
|
|
var lastTabElementInside = GetPrevTab(null, nextTabElement, true); |
|
|
|
if (lastTabElementInside != null) |
|
|
|
return lastTabElementInside; |
|
|
|
} |
|
|
|
|
|
|
|
if (customNext.handled) |
|
|
|
{ |
|
|
|
yield return customNext.next!; |
|
|
|
} |
|
|
|
else |
|
|
|
if (tabbingType == KeyboardNavigationMode.Contained) |
|
|
|
return null; |
|
|
|
|
|
|
|
if (e != container && IsTabStop(container)) |
|
|
|
return container; |
|
|
|
|
|
|
|
// If end of the subtree is reached or there no other elements above
|
|
|
|
if (!goDownOnly && GetParent(container) != null) |
|
|
|
{ |
|
|
|
return GetPrevTab(container, null, false); |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) |
|
|
|
{ |
|
|
|
if (e is IInputElement container) |
|
|
|
{ |
|
|
|
var first = GetFirstChild(container); |
|
|
|
|
|
|
|
if (first is object) |
|
|
|
return GetPrevTab(first, null, false); |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? FocusedElement(IInputElement e) |
|
|
|
{ |
|
|
|
var iie = e; |
|
|
|
// Focus delegation is enabled only if keyboard focus is outside the container
|
|
|
|
if (iie != null && !iie.IsKeyboardFocusWithin) |
|
|
|
{ |
|
|
|
var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); |
|
|
|
if (focusedElement != null) |
|
|
|
{ |
|
|
|
if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) |
|
|
|
if (!IsFocusScope(e)) |
|
|
|
{ |
|
|
|
yield return child; |
|
|
|
// Verify if focusedElement is a visual descendant of e
|
|
|
|
if (focusedElement is IVisual visualFocusedElement && |
|
|
|
visualFocusedElement != e && |
|
|
|
e.IsVisualAncestorOf(visualFocusedElement)) |
|
|
|
{ |
|
|
|
return focusedElement; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetFirstChild(IInputElement e) |
|
|
|
{ |
|
|
|
// If the element has a FocusedElement it should be its first child
|
|
|
|
if (FocusedElement(e) is IInputElement focusedElement) |
|
|
|
return focusedElement; |
|
|
|
|
|
|
|
if (child.CanFocusDescendants()) |
|
|
|
// Return the first visible element.
|
|
|
|
var uiElement = e as InputElement; |
|
|
|
|
|
|
|
if (uiElement is null || uiElement.IsVisible) |
|
|
|
{ |
|
|
|
if (e is IVisual elementAsVisual) |
|
|
|
{ |
|
|
|
var children = elementAsVisual.VisualChildren; |
|
|
|
var count = children.Count; |
|
|
|
|
|
|
|
for (int i = 0; i < count; i++) |
|
|
|
{ |
|
|
|
foreach (var descendant in GetFocusableDescendants(child, direction)) |
|
|
|
if (children[i] is InputElement ie) |
|
|
|
{ |
|
|
|
if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) |
|
|
|
if (ie.IsVisible) |
|
|
|
return ie; |
|
|
|
else |
|
|
|
{ |
|
|
|
yield return descendant; |
|
|
|
var firstChild = GetFirstChild(ie); |
|
|
|
if (firstChild != null) |
|
|
|
return firstChild; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <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>
|
|
|
|
/// <param name="outsideElement">
|
|
|
|
/// If true will not descend into <paramref name="element"/> to find next control.
|
|
|
|
/// </param>
|
|
|
|
/// <returns>The next element, or null if the element is the last.</returns>
|
|
|
|
private static IInputElement? GetNextInContainer( |
|
|
|
IInputElement element, |
|
|
|
IInputElement container, |
|
|
|
NavigationDirection direction, |
|
|
|
bool outsideElement) |
|
|
|
private static IInputElement? GetLastChild(IInputElement e) |
|
|
|
{ |
|
|
|
IInputElement? e = element; |
|
|
|
// If the element has a FocusedElement it should be its last child
|
|
|
|
if (FocusedElement(e) is IInputElement focusedElement) |
|
|
|
return focusedElement; |
|
|
|
|
|
|
|
if (direction == NavigationDirection.Next && !outsideElement) |
|
|
|
{ |
|
|
|
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); |
|
|
|
// Return the last visible element.
|
|
|
|
var uiElement = e as InputElement; |
|
|
|
|
|
|
|
if (descendant != null) |
|
|
|
{ |
|
|
|
return descendant; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (container != null) |
|
|
|
if (uiElement == null || uiElement.IsVisible) |
|
|
|
{ |
|
|
|
var navigable = container as INavigableContainer; |
|
|
|
var elementAsVisual = e as IVisual; |
|
|
|
|
|
|
|
// TODO: Do a spatial search here if the container doesn't implement
|
|
|
|
// INavigableContainer.
|
|
|
|
if (navigable != null) |
|
|
|
if (elementAsVisual != null) |
|
|
|
{ |
|
|
|
while (e != null) |
|
|
|
{ |
|
|
|
e = navigable.GetControl(direction, e, false); |
|
|
|
var children = elementAsVisual.VisualChildren; |
|
|
|
var count = children.Count; |
|
|
|
|
|
|
|
if (e != null && |
|
|
|
e.CanFocus() && |
|
|
|
KeyboardNavigation.GetIsTabStop((InputElement)e)) |
|
|
|
for (int i = count - 1; i >= 0; i--) |
|
|
|
{ |
|
|
|
if (children[i] is InputElement ie) |
|
|
|
{ |
|
|
|
break; |
|
|
|
if (ie.IsVisible) |
|
|
|
return ie; |
|
|
|
else |
|
|
|
{ |
|
|
|
var lastChild = GetLastChild(ie); |
|
|
|
if (lastChild != null) |
|
|
|
return lastChild; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetFirstTabInGroup(IInputElement container) |
|
|
|
{ |
|
|
|
IInputElement? firstTabElement = null; |
|
|
|
int minIndexFirstTab = int.MinValue; |
|
|
|
|
|
|
|
var currElement = container; |
|
|
|
while ((currElement = GetNextInTree(currElement, container)) != null) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement)) |
|
|
|
{ |
|
|
|
// TODO: Do a spatial search here if the container doesn't implement
|
|
|
|
// INavigableContainer.
|
|
|
|
e = null; |
|
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement); |
|
|
|
|
|
|
|
if (currPriority < minIndexFirstTab || firstTabElement == null) |
|
|
|
{ |
|
|
|
minIndexFirstTab = currPriority; |
|
|
|
firstTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return firstTabElement; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetLastInTree(IInputElement container) |
|
|
|
{ |
|
|
|
IInputElement? result; |
|
|
|
IInputElement? c = container; |
|
|
|
|
|
|
|
do |
|
|
|
{ |
|
|
|
result = c; |
|
|
|
c = GetLastChild(c); |
|
|
|
} while (c != null && !IsGroup(c)); |
|
|
|
|
|
|
|
if (c != null) |
|
|
|
return c; |
|
|
|
|
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
if (e != null && direction == NavigationDirection.Previous) |
|
|
|
private static IInputElement? GetLastTabInGroup(IInputElement container) |
|
|
|
{ |
|
|
|
IInputElement? lastTabElement = null; |
|
|
|
int maxIndexFirstTab = int.MaxValue; |
|
|
|
var currElement = GetLastInTree(container); |
|
|
|
while (currElement != null && currElement != container) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement)) |
|
|
|
{ |
|
|
|
var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); |
|
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement); |
|
|
|
|
|
|
|
if (descendant != null) |
|
|
|
if (currPriority > maxIndexFirstTab || lastTabElement == null) |
|
|
|
{ |
|
|
|
return descendant; |
|
|
|
maxIndexFirstTab = currPriority; |
|
|
|
lastTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
currElement = GetPreviousInTree(currElement, container); |
|
|
|
} |
|
|
|
return lastTabElement; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetNextInTree(IInputElement e, IInputElement container) |
|
|
|
{ |
|
|
|
IInputElement? result = null; |
|
|
|
|
|
|
|
if (e == container || !IsGroup(e)) |
|
|
|
result = GetFirstChild(e); |
|
|
|
|
|
|
|
if (result != null || e == container) |
|
|
|
return result; |
|
|
|
|
|
|
|
IInputElement? parent = e; |
|
|
|
do |
|
|
|
{ |
|
|
|
var sibling = GetNextSibling(parent); |
|
|
|
if (sibling != null) |
|
|
|
return sibling; |
|
|
|
|
|
|
|
return e; |
|
|
|
parent = GetParent(parent); |
|
|
|
} while (parent != null && parent != container); |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetNextSibling(IInputElement e) |
|
|
|
{ |
|
|
|
if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) |
|
|
|
{ |
|
|
|
var children = parentAsVisual.VisualChildren; |
|
|
|
var count = children.Count; |
|
|
|
var i = 0; |
|
|
|
|
|
|
|
//go till itself
|
|
|
|
for (; i < count; i++) |
|
|
|
{ |
|
|
|
var vchild = children[i]; |
|
|
|
if (vchild == elementAsVisual) |
|
|
|
break; |
|
|
|
} |
|
|
|
i++; |
|
|
|
//search ahead
|
|
|
|
for (; i < count; i++) |
|
|
|
{ |
|
|
|
var visual = children[i]; |
|
|
|
if (visual is IInputElement ie) |
|
|
|
return ie; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the first item that should be focused in the next container.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="element">The element being navigated away from.</param>
|
|
|
|
/// <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 element, |
|
|
|
IInputElement container, |
|
|
|
NavigationDirection direction) |
|
|
|
private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) |
|
|
|
{ |
|
|
|
var parent = container.GetVisualParent<IInputElement>(); |
|
|
|
IInputElement? next = null; |
|
|
|
// None groups: Tab navigation is not supported
|
|
|
|
if (tabbingType == KeyboardNavigationMode.None) |
|
|
|
return null; |
|
|
|
|
|
|
|
if (parent != null) |
|
|
|
// e == null or e == container -> return the first TabStopOrGroup
|
|
|
|
if (e == null || e == container) |
|
|
|
{ |
|
|
|
if (direction == NavigationDirection.Previous && |
|
|
|
parent.CanFocus() && |
|
|
|
KeyboardNavigation.GetIsTabStop((InputElement) parent)) |
|
|
|
return GetFirstTabInGroup(container); |
|
|
|
} |
|
|
|
|
|
|
|
if (tabbingType == KeyboardNavigationMode.Once) |
|
|
|
return null; |
|
|
|
|
|
|
|
var nextTabElement = GetNextTabWithSameIndex(e, container); |
|
|
|
if (nextTabElement != null) |
|
|
|
return nextTabElement; |
|
|
|
|
|
|
|
return GetNextTabWithNextIndex(e, container, tabbingType); |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container) |
|
|
|
{ |
|
|
|
var elementTabPriority = KeyboardNavigation.GetTabIndex(e); |
|
|
|
var currElement = e; |
|
|
|
while ((currElement = GetNextInTree(currElement, container)) != null) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority) |
|
|
|
{ |
|
|
|
return parent; |
|
|
|
return currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var allSiblings = parent.GetVisualChildren() |
|
|
|
.OfType<IInputElement>() |
|
|
|
.Where(FocusExtensions.CanFocusDescendants); |
|
|
|
var siblings = direction == NavigationDirection.Next ? |
|
|
|
allSiblings.SkipWhile(x => x != container).Skip(1) : |
|
|
|
allSiblings.TakeWhile(x => x != container).Reverse(); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
foreach (var sibling in siblings) |
|
|
|
private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) |
|
|
|
{ |
|
|
|
// Find the next min index in the tree
|
|
|
|
// min (index>currentTabIndex)
|
|
|
|
IInputElement? nextTabElement = null; |
|
|
|
IInputElement? firstTabElement = null; |
|
|
|
int minIndexFirstTab = int.MinValue; |
|
|
|
int minIndex = int.MinValue; |
|
|
|
int elementTabPriority = KeyboardNavigation.GetTabIndex(e); |
|
|
|
|
|
|
|
IInputElement? currElement = container; |
|
|
|
while ((currElement = GetNextInTree(currElement, container)) != null) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement)) |
|
|
|
{ |
|
|
|
var customNext = GetCustomNext(sibling, direction); |
|
|
|
if (customNext.handled) |
|
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement); |
|
|
|
if (currPriority > elementTabPriority) |
|
|
|
{ |
|
|
|
return customNext.next; |
|
|
|
if (currPriority < minIndex || nextTabElement == null) |
|
|
|
{ |
|
|
|
minIndex = currPriority; |
|
|
|
nextTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) |
|
|
|
if (currPriority < minIndexFirstTab || firstTabElement == null) |
|
|
|
{ |
|
|
|
return sibling; |
|
|
|
minIndexFirstTab = currPriority; |
|
|
|
firstTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
next = direction == NavigationDirection.Next ? |
|
|
|
GetFocusableDescendants(sibling, direction).FirstOrDefault() : |
|
|
|
GetFocusableDescendants(sibling, direction).LastOrDefault(); |
|
|
|
// Cycle groups: if not found - return first element
|
|
|
|
if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) |
|
|
|
nextTabElement = firstTabElement; |
|
|
|
|
|
|
|
return nextTabElement; |
|
|
|
} |
|
|
|
|
|
|
|
if (next != null) |
|
|
|
private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) |
|
|
|
{ |
|
|
|
// None groups: Tab navigation is not supported
|
|
|
|
if (tabbingType == KeyboardNavigationMode.None) |
|
|
|
return null; |
|
|
|
|
|
|
|
// Search the last index inside the group
|
|
|
|
if (e == null) |
|
|
|
{ |
|
|
|
return GetLastTabInGroup(container); |
|
|
|
} |
|
|
|
|
|
|
|
if (tabbingType == KeyboardNavigationMode.Once) |
|
|
|
return null; |
|
|
|
|
|
|
|
if (e == container) |
|
|
|
return null; |
|
|
|
|
|
|
|
var nextTabElement = GetPrevTabWithSameIndex(e, container); |
|
|
|
if (nextTabElement != null) |
|
|
|
return nextTabElement; |
|
|
|
|
|
|
|
return GetPrevTabWithPrevIndex(e, container, tabbingType); |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container) |
|
|
|
{ |
|
|
|
int elementTabPriority = KeyboardNavigation.GetTabIndex(e); |
|
|
|
var currElement = GetPreviousInTree(e, container); |
|
|
|
while (currElement != null) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container) |
|
|
|
{ |
|
|
|
return currElement; |
|
|
|
} |
|
|
|
currElement = GetPreviousInTree(currElement, container); |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) |
|
|
|
{ |
|
|
|
// Find the next max index in the tree
|
|
|
|
// max (index<currentTabIndex)
|
|
|
|
IInputElement? lastTabElement = null; |
|
|
|
IInputElement? nextTabElement = null; |
|
|
|
int elementTabPriority = KeyboardNavigation.GetTabIndex(e); |
|
|
|
int maxIndexFirstTab = Int32.MaxValue; |
|
|
|
int maxIndex = Int32.MaxValue; |
|
|
|
var currElement = GetLastInTree(container); |
|
|
|
while (currElement != null) |
|
|
|
{ |
|
|
|
if (IsTabStopOrGroup(currElement) && currElement != container) |
|
|
|
{ |
|
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement); |
|
|
|
if (currPriority < elementTabPriority) |
|
|
|
{ |
|
|
|
if (currPriority > maxIndex || nextTabElement == null) |
|
|
|
{ |
|
|
|
maxIndex = currPriority; |
|
|
|
nextTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (currPriority > maxIndexFirstTab || lastTabElement == null) |
|
|
|
{ |
|
|
|
return next; |
|
|
|
maxIndexFirstTab = currPriority; |
|
|
|
lastTabElement = currElement; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
next = GetFirstInNextContainer(element, parent, direction); |
|
|
|
currElement = GetPreviousInTree(currElement, container); |
|
|
|
} |
|
|
|
|
|
|
|
// Cycle groups: if not found - return first element
|
|
|
|
if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) |
|
|
|
nextTabElement = lastTabElement; |
|
|
|
|
|
|
|
return nextTabElement; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container) |
|
|
|
{ |
|
|
|
if (e == container) |
|
|
|
return null; |
|
|
|
|
|
|
|
var result = GetPreviousSibling(e); |
|
|
|
|
|
|
|
if (result != null) |
|
|
|
{ |
|
|
|
if (IsGroup(result)) |
|
|
|
return result; |
|
|
|
else |
|
|
|
return GetLastInTree(result); |
|
|
|
} |
|
|
|
else |
|
|
|
return GetParent(e); |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetPreviousSibling(IInputElement e) |
|
|
|
{ |
|
|
|
if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) |
|
|
|
{ |
|
|
|
next = direction == NavigationDirection.Next ? |
|
|
|
GetFocusableDescendants(container, direction).FirstOrDefault() : |
|
|
|
GetFocusableDescendants(container, direction).LastOrDefault(); |
|
|
|
var children = parentAsVisual.VisualChildren; |
|
|
|
var count = children.Count; |
|
|
|
IInputElement? prev = null; |
|
|
|
|
|
|
|
for (int i = 0; i < count; i++) |
|
|
|
{ |
|
|
|
var vchild = children[i]; |
|
|
|
if (vchild == elementAsVisual) |
|
|
|
break; |
|
|
|
if (vchild.IsVisible == true && vchild is IInputElement ie) |
|
|
|
prev = ie; |
|
|
|
} |
|
|
|
return prev; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
return next; |
|
|
|
private static IInputElement? GetActiveElement(IInputElement e) |
|
|
|
{ |
|
|
|
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); |
|
|
|
} |
|
|
|
|
|
|
|
private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, |
|
|
|
NavigationDirection direction) |
|
|
|
private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); |
|
|
|
|
|
|
|
private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent) |
|
|
|
{ |
|
|
|
if (element is ICustomKeyboardNavigation custom) |
|
|
|
var result = element; // Keep the last non null element
|
|
|
|
var e = element; |
|
|
|
|
|
|
|
// If we don't want to include the current element,
|
|
|
|
// start at the parent of the element. If the element
|
|
|
|
// is the root, then just return it as the group parent.
|
|
|
|
if (!includeCurrent) |
|
|
|
{ |
|
|
|
result = e; |
|
|
|
e = GetParent(e); |
|
|
|
if (e == null) |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
while (e != null) |
|
|
|
{ |
|
|
|
return custom.GetNext(element, direction); |
|
|
|
if (IsGroup(e)) |
|
|
|
return e; |
|
|
|
|
|
|
|
result = e; |
|
|
|
e = GetParent(e); |
|
|
|
} |
|
|
|
|
|
|
|
return (false, null); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
private static IInputElement? GetParent(IInputElement e) |
|
|
|
{ |
|
|
|
// For Visual - go up the visual parent chain until we find Visual.
|
|
|
|
if (e is IVisual v) |
|
|
|
return v.FindAncestorOfType<IInputElement>(); |
|
|
|
|
|
|
|
// This will need to be implemented when we have non-visual input elements.
|
|
|
|
throw new NotSupportedException(); |
|
|
|
} |
|
|
|
|
|
|
|
private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) |
|
|
|
{ |
|
|
|
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); |
|
|
|
} |
|
|
|
private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; |
|
|
|
private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue; |
|
|
|
|
|
|
|
private static bool IsTabStop(IInputElement e) |
|
|
|
{ |
|
|
|
if (e is InputElement ie) |
|
|
|
return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); |
|
|
|
} |
|
|
|
} |
|
|
|
|