csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
674 lines
24 KiB
674 lines
24 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Input.Navigation
|
|
{
|
|
/// <summary>
|
|
/// The implementation for default tab navigation.
|
|
/// </summary>
|
|
internal static class TabNavigation
|
|
{
|
|
public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly)
|
|
{
|
|
return GetNextTab(e, GetGroupParent(e), goDownOnly);
|
|
}
|
|
|
|
public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly)
|
|
{
|
|
var tabbingType = GetKeyNavigationMode(container);
|
|
|
|
if (e == null)
|
|
{
|
|
if (IsTabStop(container))
|
|
return container;
|
|
|
|
// Using ActiveElement if set
|
|
var activeElement = GetActiveElement(container);
|
|
if (activeElement != null)
|
|
return GetNextTab(null, activeElement, true);
|
|
}
|
|
else
|
|
{
|
|
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
|
|
{
|
|
if (container != e)
|
|
{
|
|
if (goDownOnly)
|
|
return null;
|
|
var parentContainer = GetGroupParent(container);
|
|
return GetNextTab(container, parentContainer, goDownOnly);
|
|
}
|
|
}
|
|
}
|
|
|
|
// All groups
|
|
IInputElement? loopStartElement = null;
|
|
var nextTabElement = e;
|
|
var currentTabbingType = tabbingType;
|
|
|
|
// Search down inside the container
|
|
while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e)
|
|
{
|
|
if (e is IInputElement container)
|
|
{
|
|
var last = GetLastInTree(container);
|
|
|
|
if (last is object)
|
|
return GetNextTab(last, false);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
|
|
{
|
|
if (e is null && container is null)
|
|
throw new InvalidOperationException("Either 'e' or 'container' must be non-null.");
|
|
|
|
if (container is null)
|
|
container = GetGroupParent(e!);
|
|
|
|
KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container);
|
|
|
|
if (e == null)
|
|
{
|
|
// Using ActiveElement if set
|
|
var activeElement = GetActiveElement(container);
|
|
if (activeElement != null)
|
|
return GetPrevTab(null, activeElement, true);
|
|
else
|
|
{
|
|
// 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
|
|
{
|
|
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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 (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 (!IsFocusScope(e))
|
|
{
|
|
// 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;
|
|
|
|
// Return the first visible element.
|
|
var uiElement = e as InputElement;
|
|
|
|
if (uiElement is null || IsVisibleAndEnabled(uiElement))
|
|
{
|
|
if (e is IVisual elementAsVisual)
|
|
{
|
|
var children = elementAsVisual.VisualChildren;
|
|
var count = children.Count;
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (children[i] is InputElement ie)
|
|
{
|
|
if (IsVisibleAndEnabled(ie))
|
|
return ie;
|
|
else
|
|
{
|
|
var firstChild = GetFirstChild(ie);
|
|
if (firstChild != null)
|
|
return firstChild;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static IInputElement? GetLastChild(IInputElement e)
|
|
{
|
|
// If the element has a FocusedElement it should be its last child
|
|
if (FocusedElement(e) is IInputElement focusedElement)
|
|
return focusedElement;
|
|
|
|
// Return the last visible element.
|
|
var uiElement = e as InputElement;
|
|
|
|
if (uiElement == null || IsVisibleAndEnabled(uiElement))
|
|
{
|
|
var elementAsVisual = e as IVisual;
|
|
|
|
if (elementAsVisual != null)
|
|
{
|
|
var children = elementAsVisual.VisualChildren;
|
|
var count = children.Count;
|
|
|
|
for (int i = count - 1; i >= 0; i--)
|
|
{
|
|
if (children[i] is InputElement ie)
|
|
{
|
|
if (IsVisibleAndEnabled(ie))
|
|
return ie;
|
|
else
|
|
{
|
|
var lastChild = GetLastChild(ie);
|
|
if (lastChild != null)
|
|
return lastChild;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
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;
|
|
}
|
|
|
|
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))
|
|
{
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement);
|
|
|
|
if (currPriority > maxIndexFirstTab || lastTabElement == null)
|
|
{
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
|
|
{
|
|
// None groups: Tab navigation is not supported
|
|
if (tabbingType == KeyboardNavigationMode.None)
|
|
return null;
|
|
|
|
// e == null or e == container -> return the first TabStopOrGroup
|
|
if (e == null || e == container)
|
|
{
|
|
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 currElement;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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))
|
|
{
|
|
int currPriority = KeyboardNavigation.GetTabIndex(currElement);
|
|
if (currPriority > elementTabPriority)
|
|
{
|
|
if (currPriority < minIndex || nextTabElement == null)
|
|
{
|
|
minIndex = currPriority;
|
|
nextTabElement = currElement;
|
|
}
|
|
}
|
|
|
|
if (currPriority < minIndexFirstTab || firstTabElement == null)
|
|
{
|
|
minIndexFirstTab = currPriority;
|
|
firstTabElement = currElement;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cycle groups: if not found - return first element
|
|
if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
|
|
nextTabElement = firstTabElement;
|
|
|
|
return nextTabElement;
|
|
}
|
|
|
|
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)
|
|
{
|
|
maxIndexFirstTab = currPriority;
|
|
lastTabElement = currElement;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
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 is IInputElement ie && IsVisibleAndEnabled(ie))
|
|
prev = ie;
|
|
}
|
|
return prev;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static IInputElement? GetActiveElement(IInputElement e)
|
|
{
|
|
return ((AvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty);
|
|
}
|
|
|
|
private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false);
|
|
|
|
private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent)
|
|
{
|
|
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)
|
|
{
|
|
if (IsGroup(e))
|
|
return e;
|
|
|
|
result = e;
|
|
e = GetParent(e);
|
|
}
|
|
|
|
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 ((AvaloniaObject)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);
|
|
private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled;
|
|
}
|
|
}
|
|
|