Browse Source

Focus Traversal Api (#18647)

* add focus traversal

* add control focus search overrides

* fix whitespace

* rebased and updated api

* update tests, make focusmanager constructor public

* fix tests

* fix whitespace issue

* addressed comment

* add more tests

* remove redundant private focus  state. fix tests

* Update FocusManager.cs

* fix constructors

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/19529/head
Emmanuel Hansen 9 months ago
committed by GitHub
parent
commit
22eb8e6ec3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      src/Avalonia.Base/Input/FindNextElementOptions.cs
  2. 95
      src/Avalonia.Base/Input/FocusHelpers.cs
  3. 899
      src/Avalonia.Base/Input/FocusManager.cs
  4. 172
      src/Avalonia.Base/Input/InputElement.cs
  5. 17
      src/Avalonia.Base/Input/Navigation/TabNavigation.cs
  6. 2
      src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs
  7. 16
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  8. 2
      src/Avalonia.Controls/Application.cs
  9. 3
      src/Avalonia.Controls/TopLevel.cs
  10. 251
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  11. 1
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  12. 1
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  13. 3
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  14. 1
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  15. 1
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  16. 1
      tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs
  17. 1
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  18. 2
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  19. 1
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  20. 1
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  21. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  22. 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  23. 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  24. 1
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  25. 1
      tests/Avalonia.LeakTests/ControlTests.cs
  26. 2
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs
  27. 3
      tests/Avalonia.UnitTests/TestRoot.cs
  28. 10
      tests/Avalonia.UnitTests/TestServices.cs
  29. 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs

17
src/Avalonia.Base/Input/FindNextElementOptions.cs

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Input
{
public sealed class FindNextElementOptions
{
public InputElement? SearchRoot { get; init; }
public Rect ExclusionRect { get; init; }
public Rect? FocusHintRectangle { get; init; }
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
public bool IgnoreOcclusivity { get; init; }
}
}

95
src/Avalonia.Base/Input/FocusHelpers.cs

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
internal static class FocusHelpers
{
public static IEnumerable<IInputElement> GetInputElementChildren(AvaloniaObject? parent)
{
// TODO: add control overrides to return custom focus list from control
if (parent is Visual visual)
{
return visual.VisualChildren.OfType<IInputElement>();
}
return Array.Empty<IInputElement>();
}
public static bool CanHaveFocusableChildren(AvaloniaObject? parent)
{
if (parent == null)
return false;
var children = GetInputElementChildren(parent);
bool hasFocusChildren = true;
foreach (var child in children)
{
if (IsVisible(child))
{
if (child.Focusable)
{
hasFocusChildren = true;
}
else if (CanHaveFocusableChildren(child as AvaloniaObject))
{
hasFocusChildren = true;
}
}
if (hasFocusChildren)
break;
}
return hasFocusChildren;
}
public static IInputElement? GetFocusParent(IInputElement? inputElement)
{
if (inputElement == null)
return null;
if (inputElement is Visual visual)
{
var rootVisual = visual.VisualRoot;
if (inputElement != rootVisual)
return visual.Parent as IInputElement;
}
return null;
}
public static bool IsPotentialTabStop(IInputElement? element)
{
if (element is InputElement inputElement)
return inputElement.IsTabStop;
return false;
}
internal static bool IsVisible(IInputElement? element)
{
if(element is Visual visual)
return visual.IsEffectivelyVisible;
return false;
}
internal static bool IsFocusable(IInputElement? element)
{
return element?.Focusable ?? false;
}
internal static bool CanHaveChildren(IInputElement? element)
{
// We don't currently have a flag to indicate a visual can have children, so we just return whether the element is a visual
return element is Visual;
}
}
}

899
src/Avalonia.Base/Input/FocusManager.cs

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

172
src/Avalonia.Base/Input/InputElement.cs

@ -51,7 +51,7 @@ namespace Avalonia.Input
AvaloniaProperty.RegisterDirect<InputElement, bool>(
nameof(IsKeyboardFocusWithin),
o => o.IsKeyboardFocusWithin);
/// <summary>
/// Defines the <see cref="IsFocused"/> property.
/// </summary>
@ -129,7 +129,7 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, TextInputEventArgs>(
nameof(TextInput),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TextInputMethodClientRequested"/> event.
/// </summary>
@ -177,13 +177,13 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, PointerReleasedEventArgs>(
nameof(PointerReleased),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerCaptureLost"/> routed event.
/// </summary>
public static readonly RoutedEvent<PointerCaptureLostEventArgs> PointerCaptureLostEvent =
RoutedEvent.Register<InputElement, PointerCaptureLostEventArgs>(
nameof(PointerCaptureLost),
nameof(PointerCaptureLost),
RoutingStrategies.Direct);
/// <summary>
@ -253,8 +253,8 @@ namespace Avalonia.Input
PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true);
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true);
// Access Key Handling
AccessKeyHandler.AccessKeyEvent.AddClassHandler<InputElement>((x, e) => x.OnAccessKey(e));
}
@ -469,7 +469,7 @@ namespace Avalonia.Input
public bool IsKeyboardFocusWithin
{
get => _isKeyboardFocusWithin;
internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
}
/// <summary>
@ -517,7 +517,7 @@ namespace Avalonia.Input
SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
PseudoClasses.Set(":disabled", !value);
if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is {} focusManager
if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager
&& Equals(focusManager.GetFocusedElement(), this))
{
focusManager.ClearFocus();
@ -554,7 +554,7 @@ namespace Avalonia.Input
/// <inheritdoc />
public bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None)
{
return FocusManager.GetFocusManager(this)?.Focus(this, method, keyModifiers) ?? false;
return FocusManager.GetFocusManager(this)?.Focus(this, method, keyModifiers) ?? false;
}
/// <inheritdoc/>
@ -564,7 +564,7 @@ namespace Avalonia.Input
if (IsFocused)
{
FocusManager.GetFocusManager(this)?.ClearFocusOnElementRemoved(this, e.Parent);
FocusManager.GetFocusManager(e.Root as IInputElement)?.ClearFocusOnElementRemoved(this, e.Parent);
}
IsKeyboardFocusWithin = false;
@ -630,7 +630,7 @@ namespace Avalonia.Input
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnLostFocus(RoutedEventArgs e)
{
{
}
/// <summary>
@ -746,6 +746,30 @@ namespace Avalonia.Input
}
}
/// <summary>
/// Called when FocusManager get the next TabStop to interact with the focused control.
/// </summary>
/// <returns>Next tab stop.</returns>
protected internal virtual InputElement? GetNextTabStopOverride() => null;
/// <summary>
/// Called when FocusManager get the previous TabStop to interact with the focused control.
/// </summary>
/// <returns>Previous tab stop.</returns>
protected internal virtual InputElement? GetPreviousTabStopOverride() => null;
/// <summary>
/// Called when FocusManager is looking for the first focusable element from the specified search scope.
/// </summary>
/// <returns>First focusable element if available.</returns>
protected internal virtual InputElement? GetFirstFocusableElementOverride() => null;
/// <summary>
/// Called when FocusManager is looking for the last focusable element from the specified search scope.
/// </summary>
/// <returns>Last focusable element if available/>.</returns>
protected internal virtual InputElement? GetLastFocusableElementOverride() => null;
/// <summary>
/// Invoked when an unhandled <see cref="PointerCaptureLostEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
@ -757,6 +781,127 @@ namespace Avalonia.Input
}
internal static bool ProcessTabStop(IInputElement? contentRoot,
IInputElement? focusedElement,
IInputElement? candidateTabStopElement,
bool isReverse,
bool didCycleFocusAtRootVisual,
out IInputElement? newTabStop)
{
newTabStop = null;
bool isTabStopOverridden = false;
bool isCandidateTabStopOverridden = false;
IInputElement? currentFocusedTarget = focusedElement;
InputElement? focusedTargetAsIE = focusedElement as InputElement;
InputElement? candidateTargetAsIE = candidateTabStopElement as InputElement;
InputElement? newCandidateTargetAsIE = null;
IInputElement? newCandidateTabStop = null;
IInputElement? spNewTabStop = null;
if (focusedTargetAsIE != null)
{
isTabStopOverridden = focusedTargetAsIE.ProcessTabStopInternal(candidateTabStopElement, isReverse, didCycleFocusAtRootVisual, out spNewTabStop);
}
if (!isTabStopOverridden && candidateTargetAsIE != null)
{
isTabStopOverridden = candidateTargetAsIE.ProcessCandidateTabStopInternal(focusedElement, null, isReverse, out spNewTabStop);
}
else if (isTabStopOverridden && newTabStop != null)
{
newCandidateTargetAsIE = spNewTabStop as InputElement;
if (newCandidateTargetAsIE != null)
{
isCandidateTabStopOverridden = newCandidateTargetAsIE.ProcessCandidateTabStopInternal(focusedElement, spNewTabStop, isReverse, out newCandidateTabStop);
}
}
if (isCandidateTabStopOverridden)
{
if (newCandidateTabStop != null)
{
newTabStop = newCandidateTabStop;
}
isTabStopOverridden = true;
}
else if (isTabStopOverridden)
{
if (newTabStop != null)
{
newTabStop = spNewTabStop;
}
isTabStopOverridden = true;
}
return isTabStopOverridden;
}
private bool ProcessTabStopInternal(IInputElement? candidateTabStopElement,
bool isReverse,
bool didCycleFocusAtRootVisual,
out IInputElement? newTabStop)
{
InputElement? current = this;
newTabStop = null;
var candidateTabStopOverridden = false;
while (current != null && !candidateTabStopOverridden)
{
candidateTabStopOverridden = current.ProcessTabStopOverride(this,
candidateTabStopElement,
isReverse,
didCycleFocusAtRootVisual,
ref newTabStop);
current = (current as Visual)?.Parent as InputElement;
}
return candidateTabStopOverridden;
}
private bool ProcessCandidateTabStopInternal(IInputElement? currentTabStop,
IInputElement? overridenCandidateTabStopElement,
bool isReverse,
out IInputElement? newTabStop)
{
InputElement? current = this;
newTabStop = null;
var candidateTabStopOverridden = false;
while (current != null && !candidateTabStopOverridden)
{
candidateTabStopOverridden = current.ProcessCandidateTabStopOverride(currentTabStop,
this,
overridenCandidateTabStopElement,
isReverse,
ref newTabStop);
current = (current as Visual)?.Parent as InputElement;
}
return candidateTabStopOverridden;
}
protected internal virtual bool ProcessTabStopOverride(IInputElement? focusedElement,
IInputElement? candidateTabStopElement,
bool isReverse,
bool didCycleFocusAtRootVisual,
ref IInputElement? newTabStop)
{
return false;
}
protected internal virtual bool ProcessCandidateTabStopOverride(IInputElement? focusedElement,
IInputElement? candidateTabStopElement,
IInputElement? overridenCandidateTabStopElement,
bool isReverse,
ref IInputElement? newTabStop)
{
return false;
}
/// <summary>
/// Invoked when an unhandled <see cref="PointerWheelChangedEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
@ -765,6 +910,7 @@ namespace Avalonia.Input
/// <param name="e">Data about the event.</param>
protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e)
{
}
/// <summary>
@ -884,7 +1030,7 @@ namespace Avalonia.Input
// PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
// will cause extra allocations and overhead.
var children = VisualChildren;
// ReSharper disable once ForCanBeConvertedToForeach
@ -903,7 +1049,7 @@ namespace Avalonia.Input
PseudoClasses.Set(":focus", isFocused.Value);
PseudoClasses.Set(":focus-visible", _isFocusVisible);
}
if (isPointerOver.HasValue)
{
PseudoClasses.Set(":pointerover", isPointerOver.Value);

17
src/Avalonia.Base/Input/Navigation/TabNavigation.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Input.Navigation
@ -225,8 +226,8 @@ namespace Avalonia.Input.Navigation
{
if (e is Visual elementAsVisual)
{
var children = elementAsVisual.VisualChildren;
var count = children.Count;
var children = FocusHelpers.GetInputElementChildren(elementAsVisual).ToArray();
var count = children.Length;
for (int i = 0; i < count; i++)
{
@ -261,8 +262,8 @@ namespace Avalonia.Input.Navigation
{
if (e is Visual elementAsVisual)
{
var children = elementAsVisual.VisualChildren;
var count = children.Count;
var children = FocusHelpers.GetInputElementChildren(elementAsVisual).ToArray();
var count = children.Length;
for (int i = count - 1; i >= 0; i--)
{
@ -284,7 +285,7 @@ namespace Avalonia.Input.Navigation
return null;
}
private static IInputElement? GetFirstTabInGroup(IInputElement container)
internal static IInputElement? GetFirstTabInGroup(IInputElement container)
{
IInputElement? firstTabElement = null;
int minIndexFirstTab = int.MinValue;
@ -372,7 +373,7 @@ namespace Avalonia.Input.Navigation
{
if (GetParent(e) is Visual parentAsVisual && e is Visual elementAsVisual)
{
var children = parentAsVisual.VisualChildren;
var children = FocusHelpers.GetInputElementChildren(parentAsVisual).ToList();
var count = children.Count;
var i = 0;
@ -576,7 +577,7 @@ namespace Avalonia.Input.Navigation
{
if (GetParent(e) is Visual parentAsVisual && e is Visual elementAsVisual)
{
var children = parentAsVisual.VisualChildren;
var children = FocusHelpers.GetInputElementChildren(parentAsVisual).ToList();
var count = children.Count;
IInputElement? prev = null;
@ -646,7 +647,7 @@ namespace Avalonia.Input.Navigation
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)
internal static bool IsTabStop(IInputElement e)
{
if (e is InputElement ie)
return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEffectivelyEnabled;

2
src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs

@ -118,7 +118,7 @@ public partial class XYFocus
return !visibleBounds.Intersects(elementBounds);
}
private static Rect? GetBoundsForRanking(InputElement element, bool ignoreClipping)
internal static Rect? GetBoundsForRanking(InputElement element, bool ignoreClipping)
{
if (element.GetTransformedBounds() is { } bounds)
{

16
src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs

@ -23,21 +23,21 @@ public partial class XYFocus
}
private XYFocusAlgorithms.XYFocusManifolds mManifolds = new();
private XYFocusAlgorithms.XYFocusManifolds _manifolds = new();
private PooledList<XYFocusParams> _pooledCandidates = new();
private static readonly XYFocus _instance = new();
internal XYFocusAlgorithms.XYFocusManifolds ResetManifolds()
{
mManifolds.Reset();
return mManifolds;
_manifolds.Reset();
return _manifolds;
}
internal void SetManifoldsFromBounds(Rect bounds)
{
mManifolds.VManifold = (bounds.Left, bounds.Right);
mManifolds.HManifold = (bounds.Top, bounds.Bottom);
_manifolds.VManifold = (bounds.Left, bounds.Right);
_manifolds.HManifold = (bounds.Top, bounds.Bottom);
}
internal void UpdateManifolds(
@ -47,7 +47,7 @@ public partial class XYFocus
bool ignoreClipping)
{
var candidateBounds = GetBoundsForRanking(candidate, ignoreClipping)!.Value;
XYFocusAlgorithms.UpdateManifolds(direction, elementBounds, candidateBounds, mManifolds);
XYFocusAlgorithms.UpdateManifolds(direction, elementBounds, candidateBounds, _manifolds);
}
internal static InputElement? TryDirectionalFocus(
@ -261,7 +261,7 @@ public partial class XYFocus
if (updateManifolds)
{
// Update the manifolds with the newly selected focus
XYFocusAlgorithms.UpdateManifolds(direction, bounds, param.Bounds, mManifolds);
XYFocusAlgorithms.UpdateManifolds(direction, bounds, param.Bounds, _manifolds);
}
break;
@ -346,7 +346,7 @@ public partial class XYFocus
XYFocusAlgorithms.ShouldCandidateBeConsideredForRanking(bounds, candidateBounds, maxRootBoundsDistance,
direction, exclusionBounds, ignoreCone))
{
candidate.Score = XYFocusAlgorithms.GetScoreProjection(direction, bounds, candidateBounds, mManifolds, maxRootBoundsDistance);
candidate.Score = XYFocusAlgorithms.GetScoreProjection(direction, bounds, candidateBounds, _manifolds, maxRootBoundsDistance);
}
else if (mode == XYFocusNavigationStrategy.NavigationDirectionDistance ||
mode == XYFocusNavigationStrategy.RectilinearDistance)

2
src/Avalonia.Controls/Application.cs

@ -265,7 +265,6 @@ namespace Avalonia
public virtual void RegisterServices()
{
AvaloniaSynchronizationContext.InstallIfNeeded();
var focusManager = new FocusManager();
InputManager = new InputManager();
if (PlatformSettings is { } settings)
@ -279,7 +278,6 @@ namespace Avalonia
.Bind<IGlobalDataTemplates>().ToConstant(this)
.Bind<IGlobalStyles>().ToConstant(this)
.Bind<IThemeVariantHost>().ToConstant(this)
.Bind<IFocusManager>().ToConstant(focusManager)
.Bind<IInputManager>().ToConstant(InputManager)
.Bind< IToolTipService>().ToConstant(new ToolTipService(InputManager))
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()

3
src/Avalonia.Controls/TopLevel.cs

@ -590,7 +590,7 @@ namespace Avalonia.Controls
public IClipboard? Clipboard => PlatformImpl?.TryGetFeature<IClipboard>();
/// <inheritdoc />
public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
public IFocusManager? FocusManager => _focusManager ??= new FocusManager(this);
/// <inheritdoc />
public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -665,6 +665,7 @@ namespace Avalonia.Controls
}
private IDisposable? _insetsPaddings;
private FocusManager? _focusManager;
private void InvalidateChildInsetsPadding()
{

251
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@ -702,6 +702,257 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Same(innerButton, focusManager.GetFocusedElement());
}
[Fact]
public void Can_Get_First_Focusable_Element()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var container = new StackPanel
{
Children =
{
target1,
target2,
target3,
target4
}
};
var root = new TestRoot
{
Child = container
};
var firstFocusable = FocusManager.FindFirstFocusableElement(container);
Assert.Equal(target1, firstFocusable);
firstFocusable = (root.FocusManager as FocusManager)?.FindFirstFocusableElement();
Assert.Equal(target1, firstFocusable);
}
}
[Fact]
public void Can_Get_Last_Focusable_Element()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var container = new StackPanel
{
Children =
{
target1,
target2,
target3,
target4
}
};
var root = new TestRoot
{
Child = container
};
var lastFocusable = FocusManager.FindLastFocusableElement(container);
Assert.Equal(target4, lastFocusable);
lastFocusable = (root.FocusManager as FocusManager)?.FindLastFocusableElement();
Assert.Equal(target4, lastFocusable);
}
}
[Fact]
public void Can_Get_Next_Element()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var container = new StackPanel
{
Children =
{
target1,
target2,
target3,
target4
}
};
var root = new TestRoot
{
Child = container
};
var focusManager = FocusManager.GetFocusManager(container);
target1.Focus();
var next = focusManager.FindNextElement(NavigationDirection.Next);
Assert.Equal(next, target2);
}
}
[Fact]
public void Can_Get_Next_Element_With_Options()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var target5 = new Button { Focusable = true, Content = "5" };
var seachStack = new StackPanel()
{
Children =
{
target3,
target4
}
};
var container = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children =
{
target1,
target2,
seachStack,
target5
}
};
var root = new TestRoot
{
Child = container
};
root.InvalidateMeasure();
root.ExecuteInitialLayoutPass();
var focusManager = FocusManager.GetFocusManager(container);
target1.Focus();
var options = new FindNextElementOptions()
{
SearchRoot = seachStack
};
// Search root is right of the current focus, should return the first focusable element in the search root
var next = focusManager.FindNextElement(NavigationDirection.Right, options);
Assert.Equal(next, target3);
target5.Focus();
// Search root is right of the current focus, should return the first focusable element in the search root
next = focusManager.FindNextElement(NavigationDirection.Left, options);
Assert.Equal(next, target3);
// Search root isn't to the right of the current focus, should return null
next = focusManager.FindNextElement(NavigationDirection.Right, options);
Assert.Null(next);
}
}
[Fact]
public void Focus_Should_Move_According_To_Direction()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var container = new StackPanel
{
Children =
{
target1,
target2,
target3,
target4
}
};
var root = new TestRoot
{
Child = container
};
var focusManager = FocusManager.GetFocusManager(container);
var hasMoved = focusManager.TryMoveFocus(NavigationDirection.Next);
Assert.True(target1.IsFocused);
Assert.True(hasMoved);
hasMoved = focusManager.TryMoveFocus(NavigationDirection.Previous);
Assert.True(target4.IsFocused);
Assert.True(hasMoved);
}
}
[Fact]
public void Focus_Should_Move_According_To_XY_Direction()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Button { Focusable = true, Content = "1" };
var target2 = new Button { Focusable = true, Content = "2" };
var target3 = new Button { Focusable = true, Content = "3" };
var target4 = new Button { Focusable = true, Content = "4" };
var center = new Button
{
[XYFocus.LeftProperty] = target1,
[XYFocus.RightProperty] = target2,
[XYFocus.UpProperty] = target3,
[XYFocus.DownProperty] = target4,
};
var container = new Canvas
{
Children =
{
target1,
target2,
target3,
target4,
center
}
};
var root = new TestRoot
{
Child = container
};
var focusManager = FocusManager.GetFocusManager(container);
center.Focus();
var options = new FindNextElementOptions()
{
SearchRoot = container
};
var hasMoved = focusManager.TryMoveFocus(NavigationDirection.Up, options);
Assert.True(target3.IsFocused);
Assert.True(hasMoved);
}
}
private class TestFocusScope : Panel, IFocusScope
{
}

1
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -24,7 +24,6 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(
inputManager: new InputManager(),
focusManager: new FocusManager(),
renderInterface: new HeadlessPlatformRenderInterface()));
var renderer = new Mock<IHitTester>();

1
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -1261,7 +1261,6 @@ namespace Avalonia.Controls.UnitTests
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),

3
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@ -334,13 +334,12 @@ namespace Avalonia.Controls.UnitTests
})
},
};
kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
root.ApplyTemplate();
root.Presenter.UpdateChild();
target.ApplyTemplate();
target.Presenter.UpdateChild();
kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);

1
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -672,7 +672,6 @@ namespace Avalonia.Controls.UnitTests
windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
inputManager: new InputManager(),
windowImpl: windowImpl.Object,

1
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -647,7 +647,6 @@ namespace Avalonia.Controls.UnitTests
return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
new MockWindowingPlatform(null,
x => UseOverlayPopups ? null : MockWindowingPlatform.CreatePopupMock(x).Object),
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice()));
}

1
tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs

@ -90,7 +90,6 @@ namespace Avalonia.Controls.UnitTests
windowingPlatform: new MockWindowingPlatform(
null,
window => MockWindowingPlatform.CreatePopupMock(window).Object),
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice()));
}

1
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -1233,7 +1233,6 @@ namespace Avalonia.Controls.UnitTests
{
return UnitTestApplication.Start(
TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
fontManagerImpl: new HeadlessFontManagerStub(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),

2
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1102,7 +1102,6 @@ namespace Avalonia.Controls.UnitTests
public void Tab_Navigation_Should_Move_To_First_Item_When_No_Anchor_Element_Selected()
{
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice());
using var app = UnitTestApplication.Start(services);
@ -1146,7 +1145,6 @@ namespace Avalonia.Controls.UnitTests
public void Tab_Navigation_Should_Move_To_Anchor_Element()
{
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice());
using var app = UnitTestApplication.Start(services);

1
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -921,7 +921,6 @@ namespace Avalonia.Controls.UnitTests
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),

1
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -1347,7 +1347,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
return UnitTestApplication.Start(TestServices.StyledWindow.With(
windowingPlatform: CreateMockWindowingPlatform(),
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler()));
}

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1346,7 +1346,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
return UnitTestApplication.Start(
TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
fontManagerImpl: new HeadlessFontManagerStub(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),

2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -509,7 +509,6 @@ namespace Avalonia.Controls.UnitTests
public void Tab_Navigation_Should_Move_To_First_TabItem_When_No_Anchor_Element_Selected()
{
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice());
using var app = UnitTestApplication.Start(services);
@ -558,7 +557,6 @@ namespace Avalonia.Controls.UnitTests
public void Tab_Navigation_Should_Move_To_Anchor_TabItem()
{
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice());
using var app = UnitTestApplication.Start(services);

1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -2130,7 +2130,6 @@ namespace Avalonia.Controls.UnitTests
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),

1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -1836,7 +1836,6 @@ namespace Avalonia.Controls.UnitTests
{
return UnitTestApplication.Start(
TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
fontManagerImpl: new HeadlessFontManagerStub(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),

1
tests/Avalonia.LeakTests/ControlTests.cs

@ -1027,7 +1027,6 @@ namespace Avalonia.LeakTests
{
Disposable.Create(Cleanup),
UnitTestApplication.Start(TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
inputManager: new InputManager()))
};

2
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs

@ -25,7 +25,7 @@ public class BindingTests_Delay : ScopedTestBase, IDisposable
public BindingTests_Delay()
{
_dispatcher = new ManualTimerDispatcher();
_app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice()));
_app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, keyboardDevice: () => new KeyboardDevice()));
_source = new BindingTests.Source { Foo = InitialFooValue };
_target = new TextBox { DataContext = _source };

3
tests/Avalonia.UnitTests/TestRoot.cs

@ -15,6 +15,7 @@ namespace Avalonia.UnitTests
public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, IRenderRoot, IStyleHost, ILogicalRoot
{
private readonly NameScope _nameScope = new NameScope();
private FocusManager? _focusManager;
public TestRoot()
{
@ -63,7 +64,7 @@ namespace Avalonia.UnitTests
IHitTester IRenderRoot.HitTester => HitTester;
public IKeyboardNavigationHandler KeyboardNavigationHandler => null;
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
public IFocusManager FocusManager => _focusManager ??= new FocusManager(this);
public IPlatformSettings PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
public IInputElement PointerOverElement { get; set; }

10
tests/Avalonia.UnitTests/TestServices.cs

@ -41,7 +41,6 @@ namespace Avalonia.UnitTests
windowingPlatform: new MockWindowingPlatform());
public static readonly TestServices RealFocus = new TestServices(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
@ -51,7 +50,6 @@ namespace Avalonia.UnitTests
textShaperImpl: new HeadlessTextShaperStub());
public static readonly TestServices FocusableWindow = new TestServices(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
@ -73,7 +71,6 @@ namespace Avalonia.UnitTests
public TestServices(
IAssetLoader assetLoader = null,
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func<IKeyboardDevice> keyboardDevice = null,
Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@ -90,7 +87,6 @@ namespace Avalonia.UnitTests
IWindowingPlatform windowingPlatform = null)
{
AssetLoader = assetLoader;
FocusManager = focusManager;
InputManager = inputManager;
KeyboardDevice = keyboardDevice;
KeyboardNavigation = keyboardNavigation;
@ -109,7 +105,6 @@ namespace Avalonia.UnitTests
internal TestServices(
IGlobalClock globalClock,
IAssetLoader assetLoader = null,
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func<IKeyboardDevice> keyboardDevice = null,
Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@ -125,7 +120,7 @@ namespace Avalonia.UnitTests
IWindowImpl windowImpl = null,
IWindowingPlatform windowingPlatform = null,
IAccessKeyHandler accessKeyHandler = null
) : this(assetLoader, focusManager, inputManager, keyboardDevice,
) : this(assetLoader, inputManager, keyboardDevice,
keyboardNavigation,
mouseDevice, platform, renderInterface, renderLoop, standardCursorFactory, theme,
dispatcherImpl, fontManagerImpl, textShaperImpl, windowImpl, windowingPlatform)
@ -136,7 +131,6 @@ namespace Avalonia.UnitTests
public IAssetLoader AssetLoader { get; }
public IInputManager InputManager { get; }
public IFocusManager FocusManager { get; }
internal IGlobalClock GlobalClock { get; set; }
internal IAccessKeyHandler AccessKeyHandler { get; }
public Func<IKeyboardDevice> KeyboardDevice { get; }
@ -154,7 +148,6 @@ namespace Avalonia.UnitTests
internal TestServices With(
IAssetLoader assetLoader = null,
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func<IKeyboardDevice> keyboardDevice = null,
Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@ -176,7 +169,6 @@ namespace Avalonia.UnitTests
return new TestServices(
globalClock ?? GlobalClock,
assetLoader: assetLoader ?? AssetLoader,
focusManager: focusManager ?? FocusManager,
inputManager: inputManager ?? InputManager,
keyboardDevice: keyboardDevice ?? KeyboardDevice,
keyboardNavigation: keyboardNavigation ?? KeyboardNavigation,

1
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -63,7 +63,6 @@ namespace Avalonia.UnitTests
{
AvaloniaLocator.CurrentMutable
.Bind<IAssetLoader>().ToConstant(Services.AssetLoader)
.Bind<IFocusManager>().ToConstant(Services.FocusManager)
.Bind<IGlobalClock>().ToConstant(Services.GlobalClock)
.BindToSelf<IGlobalStyles>(this)
.Bind<IInputManager>().ToConstant(Services.InputManager)

Loading…
Cancel
Save