|
@@ -1,6 +1,10 @@
|
|
|
using System;
|
|
|
+using System.Diagnostics;
|
|
|
+using System.Linq;
|
|
|
+using Avalonia.Input.Navigation;
|
|
|
using Avalonia.Interactivity;
|
|
|
using Avalonia.Metadata;
|
|
|
+using Avalonia.Reactive;
|
|
|
using Avalonia.VisualTree;
|
|
|
|
|
|
namespace Avalonia.Input
|
|
@@ -34,8 +38,22 @@ namespace Avalonia.Input
|
|
|
RoutingStrategies.Tunnel);
|
|
|
}
|
|
|
|
|
|
+ public FocusManager()
|
|
|
+ {
|
|
|
+ _contentRoot = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public FocusManager(IInputElement contentRoot)
|
|
|
+ {
|
|
|
+ _contentRoot = contentRoot;
|
|
|
+ }
|
|
|
+
|
|
|
private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
|
|
|
|
|
|
+ private XYFocus _xyFocus = new();
|
|
|
+ private XYFocusOptions _xYFocusOptions = new XYFocusOptions();
|
|
|
+ private IInputElement? _contentRoot;
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Gets the currently focused <see cref="IInputElement"/>.
|
|
|
/// </summary>
|
|
@@ -48,7 +66,7 @@ namespace Avalonia.Input
|
|
|
/// <param name="method">The method by which focus was changed.</param>
|
|
|
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
|
|
|
public bool Focus(
|
|
|
- IInputElement? control,
|
|
|
+ IInputElement? control,
|
|
|
NavigationMethod method = NavigationMethod.Unspecified,
|
|
|
KeyModifiers keyModifiers = KeyModifiers.None)
|
|
|
{
|
|
@@ -69,7 +87,7 @@ namespace Avalonia.Input
|
|
|
keyboardDevice.SetFocusedElement(control, method, keyModifiers);
|
|
|
return true;
|
|
|
}
|
|
|
- else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
|
|
|
+ else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
|
|
|
restore != Current &&
|
|
|
Focus(restore))
|
|
|
{
|
|
@@ -144,21 +162,31 @@ namespace Avalonia.Input
|
|
|
// Element might not be a visual, and not attached to the root.
|
|
|
// But IFocusManager is always expected to be a FocusManager.
|
|
|
return (FocusManager?)((element as Visual)?.VisualRoot as IInputRoot)?.FocusManager
|
|
|
- // In our unit tests some elements might not have a root. Remove when we migrate to headless tests.
|
|
|
+ // In our unit tests some elements might not have a root. Remove when we migrate to headless tests.
|
|
|
?? (FocusManager?)AvaloniaLocator.Current.GetService<IFocusManager>();
|
|
|
}
|
|
|
|
|
|
- internal bool TryMoveFocus(NavigationDirection direction)
|
|
|
+ /// <summary>
|
|
|
+ /// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="direction">The direction to traverse (in tab order).</param>
|
|
|
+ /// <returns>true if focus moved; otherwise, false.</returns>
|
|
|
+ public bool TryMoveFocus(NavigationDirection direction)
|
|
|
{
|
|
|
- if (GetFocusedElement() is {} focusedElement
|
|
|
- && KeyboardNavigationHandler.GetNext(focusedElement, direction) is {} newElement)
|
|
|
- {
|
|
|
- return newElement.Focus();
|
|
|
- }
|
|
|
+ return FindAndSetNextFocus(direction, _xYFocusOptions);
|
|
|
+ }
|
|
|
|
|
|
- return false;
|
|
|
+ /// <summary>
|
|
|
+ /// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="direction">The direction to traverse (in tab order).</param>
|
|
|
+ /// <param name="options">The options to help identify the next element to receive focus with keyboard/controller/remote navigation.</param>
|
|
|
+ /// <returns>true if focus moved; otherwise, false.</returns>
|
|
|
+ public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options)
|
|
|
+ {
|
|
|
+ return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Checks if the specified element can be focused.
|
|
|
/// </summary>
|
|
@@ -233,7 +261,7 @@ namespace Avalonia.Input
|
|
|
|
|
|
break;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
element = element.VisualParent;
|
|
|
}
|
|
|
}
|
|
@@ -245,5 +273,852 @@ namespace Avalonia.Input
|
|
|
return v.IsAttachedToVisualTree && e.IsEffectivelyVisible;
|
|
|
return true;
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the first element that can receive focus.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns>The first focusable element.</returns>
|
|
|
+ public IInputElement? FindFirstFocusableElement()
|
|
|
+ {
|
|
|
+ var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
|
|
|
+ if (root == null)
|
|
|
+ return null;
|
|
|
+ return GetFirstFocusableElementFromRoot(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the first element that can receive focus based on the specified scope.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="searchScope">The root element from which to search.</param>
|
|
|
+ /// <returns>The first focusable element.</returns>
|
|
|
+ public static IInputElement? FindFirstFocusableElement(IInputElement searchScope)
|
|
|
+ {
|
|
|
+ return GetFirstFocusableElement(searchScope);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the last element that can receive focus.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns>The last focusable element.</returns>
|
|
|
+ public IInputElement? FindLastFocusableElement()
|
|
|
+ {
|
|
|
+ var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
|
|
|
+ if (root == null)
|
|
|
+ return null;
|
|
|
+ return GetFirstFocusableElementFromRoot(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the last element that can receive focus based on the specified scope.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="searchScope">The root element from which to search.</param>
|
|
|
+ /// <returns>The last focusable object.</returns>
|
|
|
+ public static IInputElement? FindLastFocusableElement(IInputElement searchScope)
|
|
|
+ {
|
|
|
+ return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the element that should receive focus based on the specified navigation direction.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="direction"></param>
|
|
|
+ /// <returns></returns>
|
|
|
+ public IInputElement? FindNextElement(NavigationDirection direction)
|
|
|
+ {
|
|
|
+ var xyOption = new XYFocusOptions()
|
|
|
+ {
|
|
|
+ UpdateManifold = false
|
|
|
+ };
|
|
|
+
|
|
|
+ return FindNextFocus(direction, xyOption);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation).
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="direction">The direction that focus moves from element to element within the app UI.</param>
|
|
|
+ /// <param name="options">The options to help identify the next element to receive focus with the provided navigation.</param>
|
|
|
+ /// <returns>The next element to receive focus.</returns>
|
|
|
+ public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options)
|
|
|
+ {
|
|
|
+ return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options)
|
|
|
+ {
|
|
|
+ if (direction is not NavigationDirection.Up
|
|
|
+ and not NavigationDirection.Down
|
|
|
+ and not NavigationDirection.Left
|
|
|
+ and not NavigationDirection.Right)
|
|
|
+ {
|
|
|
+ throw new ArgumentOutOfRangeException(nameof(direction),
|
|
|
+ $"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported");
|
|
|
+ }
|
|
|
+
|
|
|
+ return new XYFocusOptions
|
|
|
+ {
|
|
|
+ UpdateManifold = false,
|
|
|
+ SearchRoot = options.SearchRoot,
|
|
|
+ ExclusionRect = options.ExclusionRect,
|
|
|
+ FocusHintRectangle = options.FocusHintRectangle,
|
|
|
+ NavigationStrategyOverride = options.NavigationStrategyOverride,
|
|
|
+ IgnoreOcclusivity = options.IgnoreOcclusivity
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)
|
|
|
+ {
|
|
|
+ IInputElement? nextFocusedElement = null;
|
|
|
+
|
|
|
+ var currentlyFocusedElement = Current;
|
|
|
+
|
|
|
+ if (direction is NavigationDirection.Previous or NavigationDirection.Next || currentlyFocusedElement == null)
|
|
|
+ {
|
|
|
+ var isReverse = direction == NavigationDirection.Previous;
|
|
|
+ nextFocusedElement = ProcessTabStopInternal(isReverse, true);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (currentlyFocusedElement is InputElement inputElement &&
|
|
|
+ XYFocus.GetBoundsForRanking(inputElement, focusOptions.IgnoreClipping) is { } bounds)
|
|
|
+ {
|
|
|
+ focusOptions.FocusedElementBounds = bounds;
|
|
|
+ }
|
|
|
+
|
|
|
+ nextFocusedElement = _xyFocus.GetNextFocusableElement(direction,
|
|
|
+ currentlyFocusedElement as InputElement,
|
|
|
+ null,
|
|
|
+ updateManifolds,
|
|
|
+ focusOptions);
|
|
|
+ }
|
|
|
+
|
|
|
+ return nextFocusedElement;
|
|
|
+ }
|
|
|
+
|
|
|
+ internal static IInputElement? GetFirstFocusableElementInternal(IInputElement searchStart, IInputElement? focusCandidate = null)
|
|
|
+ {
|
|
|
+ IInputElement? firstFocusableFromCallback = null;
|
|
|
+ var useFirstFocusableFromCallback = false;
|
|
|
+ if (searchStart is InputElement inputElement)
|
|
|
+ {
|
|
|
+ firstFocusableFromCallback = inputElement.GetFirstFocusableElementOverride();
|
|
|
+
|
|
|
+ if (firstFocusableFromCallback != null)
|
|
|
+ {
|
|
|
+ useFirstFocusableFromCallback = FocusHelpers.IsFocusable(firstFocusableFromCallback) || FocusHelpers.CanHaveFocusableChildren(firstFocusableFromCallback as AvaloniaObject);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (useFirstFocusableFromCallback)
|
|
|
+ {
|
|
|
+ if (focusCandidate == null || (GetTabIndex(firstFocusableFromCallback) < GetTabIndex(focusCandidate)))
|
|
|
+ {
|
|
|
+ focusCandidate = firstFocusableFromCallback;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var children = FocusHelpers.GetInputElementChildren(searchStart as AvaloniaObject);
|
|
|
+
|
|
|
+ foreach (var child in children)
|
|
|
+ {
|
|
|
+ if (FocusHelpers.IsVisible(child))
|
|
|
+ {
|
|
|
+ var hasFocusableChildren = FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject);
|
|
|
+ if (FocusHelpers.IsPotentialTabStop(child))
|
|
|
+ {
|
|
|
+ if (focusCandidate == null && (FocusHelpers.IsFocusable(child) || hasFocusableChildren))
|
|
|
+ {
|
|
|
+ focusCandidate = child;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FocusHelpers.IsFocusable(child) || hasFocusableChildren)
|
|
|
+ {
|
|
|
+ if (focusCandidate == null || GetTabIndex(child) < GetTabIndex(focusCandidate))
|
|
|
+ {
|
|
|
+ focusCandidate = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (hasFocusableChildren)
|
|
|
+ {
|
|
|
+ focusCandidate = GetFirstFocusableElementInternal(child, focusCandidate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return focusCandidate;
|
|
|
+ }
|
|
|
+
|
|
|
+ internal static IInputElement? GetLastFocusableElementInternal(IInputElement searchStart, IInputElement? lastFocus = null)
|
|
|
+ {
|
|
|
+ IInputElement? lastFocusableFromCallback = null;
|
|
|
+ var useLastFocusableFromCallback = false;
|
|
|
+ if (searchStart is InputElement inputElement)
|
|
|
+ {
|
|
|
+ lastFocusableFromCallback = inputElement.GetLastFocusableElementOverride();
|
|
|
+
|
|
|
+ if (lastFocusableFromCallback != null)
|
|
|
+ {
|
|
|
+ useLastFocusableFromCallback = FocusHelpers.IsFocusable(lastFocusableFromCallback) || FocusHelpers.CanHaveFocusableChildren(lastFocusableFromCallback as AvaloniaObject);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (useLastFocusableFromCallback)
|
|
|
+ {
|
|
|
+ if (lastFocus == null || (GetTabIndex(lastFocusableFromCallback) > GetTabIndex(lastFocus)))
|
|
|
+ {
|
|
|
+ lastFocus = lastFocusableFromCallback;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var children = FocusHelpers.GetInputElementChildren(searchStart as AvaloniaObject);
|
|
|
+
|
|
|
+ foreach (var child in children)
|
|
|
+ {
|
|
|
+ if (FocusHelpers.IsVisible(child))
|
|
|
+ {
|
|
|
+ var hasFocusableChildren = FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject);
|
|
|
+ if (FocusHelpers.IsPotentialTabStop(child))
|
|
|
+ {
|
|
|
+ if (lastFocus == null && (FocusHelpers.IsFocusable(child) || hasFocusableChildren))
|
|
|
+ {
|
|
|
+ lastFocus = child;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FocusHelpers.IsFocusable(child) || hasFocusableChildren)
|
|
|
+ {
|
|
|
+ if (lastFocus == null || GetTabIndex(child) >= GetTabIndex(lastFocus))
|
|
|
+ {
|
|
|
+ lastFocus = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (hasFocusableChildren)
|
|
|
+ {
|
|
|
+ lastFocus = GetLastFocusableElementInternal(child, lastFocus);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return lastFocus;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? ProcessTabStopInternal(bool isReverse, bool queryOnly)
|
|
|
+ {
|
|
|
+ IInputElement? newTabStop = null;
|
|
|
+
|
|
|
+ var defaultCandidateTabStop = GetTabStopCandidateElement(isReverse, queryOnly, out var didCycleFocusAtRootVisualScope);
|
|
|
+
|
|
|
+ var isTabStopOverriden = InputElement.ProcessTabStop(_contentRoot,
|
|
|
+ Current,
|
|
|
+ defaultCandidateTabStop,
|
|
|
+ isReverse,
|
|
|
+ didCycleFocusAtRootVisualScope,
|
|
|
+ out var newTabStopFromCallback);
|
|
|
+
|
|
|
+ if (isTabStopOverriden)
|
|
|
+ {
|
|
|
+ newTabStop = newTabStopFromCallback;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isTabStopOverriden && newTabStop == null && defaultCandidateTabStop != null)
|
|
|
+ {
|
|
|
+ newTabStop = defaultCandidateTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ return newTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetTabStopCandidateElement(bool isReverse, bool queryOnly, out bool didCycleFocusAtRootVisualScope)
|
|
|
+ {
|
|
|
+ didCycleFocusAtRootVisualScope = false;
|
|
|
+ var currentFocus = Current;
|
|
|
+ IInputElement? newTabStop = null;
|
|
|
+ var root = this._contentRoot as IInputElement;
|
|
|
+
|
|
|
+ if (root == null)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ bool internalCycleWorkaround = false;
|
|
|
+
|
|
|
+ if (Current != null)
|
|
|
+ {
|
|
|
+ internalCycleWorkaround = CanProcessTabStop(isReverse);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentFocus == null)
|
|
|
+ {
|
|
|
+ if (!isReverse)
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(root, null);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(root, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ didCycleFocusAtRootVisualScope = true;
|
|
|
+ }
|
|
|
+ else if (!isReverse)
|
|
|
+ {
|
|
|
+ newTabStop = GetNextTabStop();
|
|
|
+
|
|
|
+ if (newTabStop == null && (internalCycleWorkaround || queryOnly))
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(root, null);
|
|
|
+
|
|
|
+ didCycleFocusAtRootVisualScope = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = GetPreviousTabStop();
|
|
|
+
|
|
|
+ if (newTabStop == null && (internalCycleWorkaround || queryOnly))
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(root, null);
|
|
|
+ didCycleFocusAtRootVisualScope = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetNextTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
|
|
|
+ {
|
|
|
+ var focused = currentTabStop ?? Current;
|
|
|
+ if (focused == null || _contentRoot == null)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ IInputElement? currentCompare = focused;
|
|
|
+ IInputElement? newTabStop = (focused as InputElement)?.GetNextTabStopOverride();
|
|
|
+
|
|
|
+ if (newTabStop == null && !ignoreCurrentTabStop
|
|
|
+ && (FocusHelpers.IsVisible(focused) && (FocusHelpers.CanHaveFocusableChildren(focused as AvaloniaObject) || FocusHelpers.CanHaveChildren(focused))))
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(focused, newTabStop);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (newTabStop == null)
|
|
|
+ {
|
|
|
+ var currentPassed = false;
|
|
|
+ var current = focused;
|
|
|
+ var parent = FocusHelpers.GetFocusParent(focused);
|
|
|
+ var parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
|
|
|
+
|
|
|
+ while (parent != null && !parentIsRootVisual && newTabStop == null)
|
|
|
+ {
|
|
|
+ if (IsValidTabStopSearchCandidate(current) && current is InputElement c && KeyboardNavigation.GetTabNavigation(c) == KeyboardNavigationMode.Cycle)
|
|
|
+ {
|
|
|
+ if (current == GetParentTabStopElement(focused))
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(focused, null);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(current, current);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(parent) && parent is InputElement p && KeyboardNavigation.GetTabNavigation(p) == KeyboardNavigationMode.Once)
|
|
|
+ {
|
|
|
+ current = parent;
|
|
|
+ parent = FocusHelpers.GetFocusParent(focused);
|
|
|
+ if (parent == null)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ else if (!IsValidTabStopSearchCandidate(parent))
|
|
|
+ {
|
|
|
+ var parentElement = GetParentTabStopElement(parent);
|
|
|
+ if (parentElement == null)
|
|
|
+ {
|
|
|
+ parent = GetRootOfPopupSubTree(current) as IInputElement;
|
|
|
+
|
|
|
+ if (parent != null)
|
|
|
+ {
|
|
|
+ newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, true, ref currentPassed, ref currentCompare);
|
|
|
+
|
|
|
+ if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop))
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(newTabStop, null);
|
|
|
+ }
|
|
|
+ if (newTabStop == null)
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(parent, null);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ parent = (_contentRoot as Visual)?.VisualRoot as IInputElement;
|
|
|
+ }
|
|
|
+ else if (parentElement is InputElement pIE && KeyboardNavigation.GetTabNavigation(pIE) == KeyboardNavigationMode.None)
|
|
|
+ {
|
|
|
+ current = pIE;
|
|
|
+ parent = FocusHelpers.GetFocusParent(current);
|
|
|
+ if (parent == null)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ parent = parentElement as IInputElement;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, true, ref currentPassed, ref currentCompare);
|
|
|
+
|
|
|
+ if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop) && FocusHelpers.CanHaveFocusableChildren(newTabStop as AvaloniaObject))
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(newTabStop, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (newTabStop != null)
|
|
|
+ break;
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(parent))
|
|
|
+ {
|
|
|
+ current = parent;
|
|
|
+ }
|
|
|
+
|
|
|
+ parent = FocusHelpers.GetFocusParent(parent);
|
|
|
+ currentPassed = false;
|
|
|
+
|
|
|
+ parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
|
|
|
+ {
|
|
|
+ var focused = currentTabStop ?? Current;
|
|
|
+ if (focused == null || _contentRoot == null)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ IInputElement? newTabStop = (focused as InputElement)?.GetPreviousTabStopOverride();
|
|
|
+ IInputElement? currentCompare = focused;
|
|
|
+
|
|
|
+ if (newTabStop == null)
|
|
|
+ {
|
|
|
+ var currentPassed = false;
|
|
|
+ var current = focused;
|
|
|
+ var parent = FocusHelpers.GetFocusParent(focused);
|
|
|
+ var parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
|
|
|
+
|
|
|
+ while (parent != null && !parentIsRootVisual && newTabStop == null)
|
|
|
+ {
|
|
|
+ if (IsValidTabStopSearchCandidate(current) && current is InputElement c && KeyboardNavigation.GetTabNavigation(c) == KeyboardNavigationMode.Cycle)
|
|
|
+ {
|
|
|
+ newTabStop = GetFirstFocusableElement(current, current);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(parent) && parent is InputElement p && KeyboardNavigation.GetTabNavigation(p) == KeyboardNavigationMode.Once)
|
|
|
+ {
|
|
|
+ if (FocusHelpers.IsFocusable(parent))
|
|
|
+ {
|
|
|
+ newTabStop = parent;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ current = parent;
|
|
|
+ parent = FocusHelpers.GetFocusParent(focused);
|
|
|
+ if (parent == null)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (!IsValidTabStopSearchCandidate(parent))
|
|
|
+ {
|
|
|
+ var parentElement = GetParentTabStopElement(parent);
|
|
|
+ if (parentElement == null)
|
|
|
+ {
|
|
|
+ parent = GetRootOfPopupSubTree(current) as IInputElement;
|
|
|
+
|
|
|
+ if (parent != null)
|
|
|
+ {
|
|
|
+ newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, false, ref currentPassed, ref currentCompare);
|
|
|
+
|
|
|
+ if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop))
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(newTabStop, null);
|
|
|
+ }
|
|
|
+ if (newTabStop == null)
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(parent, null);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ parent = (_contentRoot as Visual)?.VisualRoot as IInputElement;
|
|
|
+ }
|
|
|
+ else if (parentElement is InputElement pIE && KeyboardNavigation.GetTabNavigation(pIE) == KeyboardNavigationMode.None)
|
|
|
+ {
|
|
|
+ if (FocusHelpers.IsFocusable(parent))
|
|
|
+ {
|
|
|
+ newTabStop = parent;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ current = parent;
|
|
|
+ parent = FocusHelpers.GetFocusParent(focused);
|
|
|
+ if (parent == null)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ parent = parentElement as IInputElement;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, false, ref currentPassed, ref currentCompare);
|
|
|
+
|
|
|
+ if (newTabStop == null && FocusHelpers.IsPotentialTabStop(parent) && FocusHelpers.IsFocusable(parent))
|
|
|
+ {
|
|
|
+ if (parent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle)
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(parent, null);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = parent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (newTabStop != null && FocusHelpers.CanHaveFocusableChildren(newTabStop as AvaloniaObject))
|
|
|
+ {
|
|
|
+ newTabStop = GetLastFocusableElement(newTabStop, null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (newTabStop != null)
|
|
|
+ break;
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(parent))
|
|
|
+ {
|
|
|
+ current = parent;
|
|
|
+ }
|
|
|
+
|
|
|
+ parent = FocusHelpers.GetFocusParent(parent);
|
|
|
+ currentPassed = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetNextOrPreviousTabStopInternal(IInputElement? parent, IInputElement? current, IInputElement? candidate, bool findNext, ref bool currentPassed, ref IInputElement? currentCompare)
|
|
|
+ {
|
|
|
+ var newTabStop = candidate;
|
|
|
+ IInputElement? childStop = null;
|
|
|
+ int compareIndexResult = 0;
|
|
|
+ bool compareCurrentForPreviousElement = false;
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(current))
|
|
|
+ {
|
|
|
+ currentCompare = current;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (parent != null)
|
|
|
+ {
|
|
|
+ bool foundCurrent = false;
|
|
|
+ foreach (var child in FocusHelpers.GetInputElementChildren(parent as AvaloniaObject))
|
|
|
+ {
|
|
|
+ childStop = null;
|
|
|
+ compareCurrentForPreviousElement = false;
|
|
|
+ if (child == current)
|
|
|
+ {
|
|
|
+ foundCurrent = true;
|
|
|
+ currentPassed = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FocusHelpers.IsVisible(child))
|
|
|
+ {
|
|
|
+ if (child == current)
|
|
|
+ {
|
|
|
+ foundCurrent = true;
|
|
|
+ currentPassed = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (IsValidTabStopSearchCandidate(child))
|
|
|
+ {
|
|
|
+ if (!FocusHelpers.IsPotentialTabStop(child))
|
|
|
+ {
|
|
|
+ childStop = GetNextOrPreviousTabStopInternal(childStop, current, newTabStop, findNext, ref currentPassed, ref currentCompare);
|
|
|
+ compareCurrentForPreviousElement = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ childStop = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject))
|
|
|
+ {
|
|
|
+ childStop = GetNextOrPreviousTabStopInternal(child, current, newTabStop, findNext, ref currentPassed, ref currentCompare);
|
|
|
+ compareCurrentForPreviousElement = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (childStop != null && (FocusHelpers.IsFocusable(childStop) || FocusHelpers.CanHaveFocusableChildren(childStop as AvaloniaObject)))
|
|
|
+ {
|
|
|
+ compareIndexResult = CompareTabIndex(childStop, currentCompare);
|
|
|
+
|
|
|
+ if (findNext)
|
|
|
+ {
|
|
|
+ if (compareIndexResult > 0 || ((foundCurrent || currentPassed) && compareIndexResult == 0))
|
|
|
+ {
|
|
|
+ if (newTabStop != null)
|
|
|
+ {
|
|
|
+ if (CompareTabIndex(childStop, newTabStop) < 0)
|
|
|
+ {
|
|
|
+ newTabStop = childStop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = childStop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (compareIndexResult < 0 || (((foundCurrent || currentPassed) || compareCurrentForPreviousElement) && compareIndexResult == 0))
|
|
|
+ {
|
|
|
+ if (newTabStop != null)
|
|
|
+ {
|
|
|
+ if (CompareTabIndex(childStop, newTabStop) >= 0)
|
|
|
+ {
|
|
|
+ newTabStop = childStop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newTabStop = childStop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newTabStop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int CompareTabIndex(IInputElement? control1, IInputElement? control2)
|
|
|
+ {
|
|
|
+ return GetTabIndex(control1).CompareTo(GetTabIndex(control2));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int GetTabIndex(IInputElement? element)
|
|
|
+ {
|
|
|
+ if (element is InputElement inputElement)
|
|
|
+ return inputElement.TabIndex;
|
|
|
+
|
|
|
+ return int.MaxValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool CanProcessTabStop(bool isReverse)
|
|
|
+ {
|
|
|
+ bool isFocusOnFirst = false;
|
|
|
+ bool isFocusOnLast = false;
|
|
|
+ bool canProcessTab = true;
|
|
|
+ if (IsFocusedElementInPopup())
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isReverse)
|
|
|
+ {
|
|
|
+ isFocusOnFirst = IsFocusOnFirstTabStop();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ isFocusOnLast = IsFocusOnLastTabStop();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isFocusOnFirst || isFocusOnLast)
|
|
|
+ {
|
|
|
+ canProcessTab = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (canProcessTab)
|
|
|
+ {
|
|
|
+ var edge = GetFirstFocusableElementFromRoot(!isReverse);
|
|
|
+
|
|
|
+ if (edge != null)
|
|
|
+ {
|
|
|
+ var edgeParent = GetParentTabStopElement(edge);
|
|
|
+ if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(Current))
|
|
|
+ {
|
|
|
+ canProcessTab = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ canProcessTab = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (isFocusOnLast || isFocusOnFirst)
|
|
|
+ {
|
|
|
+ if (Current is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle)
|
|
|
+ {
|
|
|
+ canProcessTab = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var focusedParent = GetParentTabStopElement(Current);
|
|
|
+ while (focusedParent != null)
|
|
|
+ {
|
|
|
+ if (focusedParent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle)
|
|
|
+ {
|
|
|
+ canProcessTab = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ focusedParent = GetParentTabStopElement(focusedParent as IInputElement);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return canProcessTab;
|
|
|
+ }
|
|
|
+
|
|
|
+ private AvaloniaObject? GetParentTabStopElement(IInputElement? current)
|
|
|
+ {
|
|
|
+ if (current != null)
|
|
|
+ {
|
|
|
+ var parent = FocusHelpers.GetFocusParent(current);
|
|
|
+
|
|
|
+ while (parent != null)
|
|
|
+ {
|
|
|
+ if (IsValidTabStopSearchCandidate(parent) && parent is InputElement element)
|
|
|
+ {
|
|
|
+ return element;
|
|
|
+ }
|
|
|
+
|
|
|
+ parent = FocusHelpers.GetFocusParent(parent);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool IsValidTabStopSearchCandidate(IInputElement? element)
|
|
|
+ {
|
|
|
+ var isValid = FocusHelpers.IsPotentialTabStop(element);
|
|
|
+
|
|
|
+ if (!isValid)
|
|
|
+ {
|
|
|
+ isValid = (element as InputElement)?.IsSet(KeyboardNavigation.TabNavigationProperty) ?? false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return isValid;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetFirstFocusableElementFromRoot(bool isReverse)
|
|
|
+ {
|
|
|
+ var root = (_contentRoot as Visual)?.VisualRoot as IInputElement;
|
|
|
+
|
|
|
+ if (root != null)
|
|
|
+ return !isReverse ? GetFirstFocusableElement(root, null) : GetLastFocusableElement(root, null);
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool IsFocusOnLastTabStop()
|
|
|
+ {
|
|
|
+ if (Current == null || _contentRoot is not Visual visual)
|
|
|
+ return false;
|
|
|
+ var root = visual.VisualRoot as IInputElement;
|
|
|
+
|
|
|
+ Debug.Assert(root != null);
|
|
|
+
|
|
|
+ var lastFocus = GetLastFocusableElement(root, null);
|
|
|
+
|
|
|
+ return lastFocus == Current;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool IsFocusOnFirstTabStop()
|
|
|
+ {
|
|
|
+ if (Current == null || _contentRoot is not Visual visual)
|
|
|
+ return false;
|
|
|
+ var root = visual.VisualRoot as IInputElement;
|
|
|
+
|
|
|
+ Debug.Assert(root != null);
|
|
|
+
|
|
|
+ var firstFocus = GetFirstFocusableElement(root, null);
|
|
|
+
|
|
|
+ return firstFocus == Current;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static IInputElement? GetFirstFocusableElement(IInputElement searchStart, IInputElement? firstFocus = null)
|
|
|
+ {
|
|
|
+ firstFocus = GetFirstFocusableElementInternal(searchStart, firstFocus);
|
|
|
+
|
|
|
+ if (firstFocus != null && !firstFocus.Focusable && FocusHelpers.CanHaveFocusableChildren(firstFocus as AvaloniaObject))
|
|
|
+ {
|
|
|
+ firstFocus = GetFirstFocusableElement(firstFocus, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ return firstFocus;
|
|
|
+ }
|
|
|
+
|
|
|
+ private IInputElement? GetLastFocusableElement(IInputElement searchStart, IInputElement? lastFocus = null)
|
|
|
+ {
|
|
|
+ lastFocus = GetLastFocusableElementInternal(searchStart, lastFocus);
|
|
|
+
|
|
|
+ if (lastFocus != null && !lastFocus.Focusable && FocusHelpers.CanHaveFocusableChildren(lastFocus as AvaloniaObject))
|
|
|
+ {
|
|
|
+ lastFocus = GetLastFocusableElement(lastFocus, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ return lastFocus;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool IsFocusedElementInPopup() => Current != null && GetRootOfPopupSubTree(Current) != null;
|
|
|
+
|
|
|
+ private Visual? GetRootOfPopupSubTree(IInputElement? current)
|
|
|
+ {
|
|
|
+ //TODO Popup api
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool FindAndSetNextFocus(NavigationDirection direction, XYFocusOptions xYFocusOptions)
|
|
|
+ {
|
|
|
+ var focusChanged = false;
|
|
|
+ if (xYFocusOptions.UpdateManifoldsFromFocusHintRect && xYFocusOptions.FocusHintRectangle != null)
|
|
|
+ {
|
|
|
+ _xyFocus.SetManifoldsFromBounds(xYFocusOptions.FocusHintRectangle ?? default);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FindNextFocus(direction, xYFocusOptions, false) is { } nextFocusedElement)
|
|
|
+ {
|
|
|
+ focusChanged = nextFocusedElement.Focus();
|
|
|
+
|
|
|
+ if (focusChanged && xYFocusOptions.UpdateManifold && nextFocusedElement is InputElement inputElement)
|
|
|
+ {
|
|
|
+ var bounds = xYFocusOptions.FocusHintRectangle ?? xYFocusOptions.FocusedElementBounds ?? default;
|
|
|
+
|
|
|
+ _xyFocus.UpdateManifolds(direction, bounds, inputElement, xYFocusOptions.IgnoreClipping);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return focusChanged;
|
|
|
+
|
|
|
+ }
|
|
|
}
|
|
|
}
|