@ -4,7 +4,6 @@ using System.Linq;
using Avalonia.Input.Navigation ;
using Avalonia.Input.Navigation ;
using Avalonia.Interactivity ;
using Avalonia.Interactivity ;
using Avalonia.Metadata ;
using Avalonia.Metadata ;
using Avalonia.Reactive ;
using Avalonia.VisualTree ;
using Avalonia.VisualTree ;
namespace Avalonia.Input
namespace Avalonia.Input
@ -12,7 +11,6 @@ namespace Avalonia.Input
/// <summary>
/// <summary>
/// Manages focus for the application.
/// Manages focus for the application.
/// </summary>
/// </summary>
[PrivateApi]
public class FocusManager : IFocusManager
public class FocusManager : IFocusManager
{
{
/// <summary>
/// <summary>
@ -42,58 +40,51 @@ namespace Avalonia.Input
RoutingStrategies . Tunnel ) ;
RoutingStrategies . Tunnel ) ;
}
}
[PrivateApi]
public FocusManager ( )
public FocusManager ( )
{
{
_ contentRoot = null ;
}
}
public FocusManager ( IInputElement contentRoot )
/// <summary>
{
/// Gets or sets the content root for the focus management system.
_ contentRoot = contentRoot ;
/// </summary>
}
[PrivateApi]
public IInputElement ? ContentRoot
internal void SetContentRoot ( IInputElement ? contentRoot )
{
{
_ contentRoot = contentRoot ;
get = > _ contentRoot ;
set = > _ contentRoot = value ;
}
}
private IInputElement ? Current = > KeyboardDevice . Instance ? . FocusedElement ;
private IInputElement ? Current = > KeyboardDevice . Instance ? . FocusedElement ;
private XYFocus _ xyFocus = new ( ) ;
private readonly XYFocus _ xyFocus = new ( ) ;
private XYFocusOptions _ xYFocusOptions = new XYFocusOptions ( ) ;
private IInputElement ? _ contentRoot ;
private IInputElement ? _ contentRoot ;
private XYFocusOptions ? _ reusableFocusOptions ;
/// <summary>
/// <inheritdoc />
/// Gets the currently focused <see cref="IInputElement"/>.
/// </summary>
public IInputElement ? GetFocusedElement ( ) = > Current ;
public IInputElement ? GetFocusedElement ( ) = > Current ;
/// <summary>
/// <inheritdoc />
/// Focuses a control.
/// </summary>
/// <param name="control">The control to focus.</param>
/// <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 (
public bool Focus (
IInputElement ? control ,
IInputElement ? element ,
NavigationMethod method = NavigationMethod . Unspecified ,
NavigationMethod method = NavigationMethod . Unspecified ,
KeyModifiers keyModifiers = KeyModifiers . None )
KeyModifiers keyModifiers = KeyModifiers . None )
{
{
if ( KeyboardDevice . Instance is not { } keyboardDevice )
if ( KeyboardDevice . Instance is not { } keyboardDevice )
return false ;
return false ;
if ( control is not null )
if ( element is not null )
{
{
if ( ! CanFocus ( control ) )
if ( ! CanFocus ( element ) )
return false ;
return false ;
if ( GetFocusScope ( control ) is StyledElement scope )
if ( GetFocusScope ( element ) is StyledElement scope )
{
{
scope . SetValue ( FocusedElementProperty , control ) ;
scope . SetValue ( FocusedElementProperty , element ) ;
_f ocusRoot = GetFocusRoot ( scope ) ;
_f ocusRoot = GetFocusRoot ( scope ) ;
}
}
keyboardDevice . SetFocusedElement ( control , method , keyModifiers ) ;
keyboardDevice . SetFocusedElement ( element , method , keyModifiers ) ;
return true ;
return true ;
}
}
else if ( _f ocusRoot ? . GetValue ( FocusedElementProperty ) is { } restore & &
else if ( _f ocusRoot ? . GetValue ( FocusedElementProperty ) is { } restore & &
@ -110,12 +101,7 @@ namespace Avalonia.Input
}
}
}
}
public void ClearFocus ( )
internal void ClearFocusOnElementRemoved ( IInputElement removedElement , Visual oldParent )
{
Focus ( null ) ;
}
public void ClearFocusOnElementRemoved ( IInputElement removedElement , Visual oldParent )
{
{
if ( oldParent is IInputElement parentElement & &
if ( oldParent is IInputElement parentElement & &
GetFocusScope ( parentElement ) is StyledElement scope & &
GetFocusScope ( parentElement ) is StyledElement scope & &
@ -129,6 +115,7 @@ namespace Avalonia.Input
Focus ( null ) ;
Focus ( null ) ;
}
}
[PrivateApi]
public IInputElement ? GetFocusedElement ( IFocusScope scope )
public IInputElement ? GetFocusedElement ( IFocusScope scope )
{
{
return ( scope as StyledElement ) ? . GetValue ( FocusedElementProperty ) ;
return ( scope as StyledElement ) ? . GetValue ( FocusedElementProperty ) ;
@ -138,6 +125,7 @@ namespace Avalonia.Input
/// Notifies the focus manager of a change in focus scope.
/// Notifies the focus manager of a change in focus scope.
/// </summary>
/// </summary>
/// <param name="scope">The new focus scope.</param>
/// <param name="scope">The new focus scope.</param>
[PrivateApi]
public void SetFocusScope ( IFocusScope scope )
public void SetFocusScope ( IFocusScope scope )
{
{
if ( GetFocusedElement ( scope ) is { } focused )
if ( GetFocusedElement ( scope ) is { } focused )
@ -153,12 +141,14 @@ namespace Avalonia.Input
}
}
}
}
[PrivateApi]
public void RemoveFocusRoot ( IFocusScope scope )
public void RemoveFocusRoot ( IFocusScope scope )
{
{
if ( scope = = _f ocusRoot )
if ( scope = = _f ocusRoot )
Clear Focus( ) ;
Focus ( null ) ;
}
}
[PrivateApi]
public static bool GetIsFocusScope ( IInputElement e ) = > e is IFocusScope ;
public static bool GetIsFocusScope ( IInputElement e ) = > e is IFocusScope ;
/// <summary>
/// <summary>
@ -176,25 +166,15 @@ namespace Avalonia.Input
? ? ( FocusManager ? ) AvaloniaLocator . Current . GetService < IFocusManager > ( ) ;
? ? ( FocusManager ? ) AvaloniaLocator . Current . GetService < IFocusManager > ( ) ;
}
}
/// <summary>
/// <inheritdoc />
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
public bool TryMoveFocus ( NavigationDirection direction , FindNextElementOptions ? options = null )
/// </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 )
{
{
return FindAndSetNextFocus ( direction , _ xYFocusOptions ) ;
ValidateDirection ( direction ) ;
}
/// <summary>
var focusOptions = ToFocusOptions ( options , true ) ;
/// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options.
var result = FindAndSetNextFocus ( direction , focusOptions ) ;
/// </summary>
_ reusableFocusOptions = focusOptions ;
/// <param name="direction">The direction to traverse (in tab order).</param>
return result ;
/// <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>
/// <summary>
@ -295,10 +275,7 @@ namespace Avalonia.Input
return true ;
return true ;
}
}
/// <summary>
/// <inheritdoc />
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
public IInputElement ? FindFirstFocusableElement ( )
public IInputElement ? FindFirstFocusableElement ( )
{
{
var root = ( _ contentRoot as Visual ) ? . GetSelfAndVisualDescendants ( ) . FirstOrDefault ( x = > x is IInputElement ) as IInputElement ;
var root = ( _ contentRoot as Visual ) ? . GetSelfAndVisualDescendants ( ) . FirstOrDefault ( x = > x is IInputElement ) as IInputElement ;
@ -317,10 +294,7 @@ namespace Avalonia.Input
return GetFirstFocusableElement ( searchScope ) ;
return GetFirstFocusableElement ( searchScope ) ;
}
}
/// <summary>
/// <inheritdoc />
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
public IInputElement ? FindLastFocusableElement ( )
public IInputElement ? FindLastFocusableElement ( )
{
{
var root = ( _ contentRoot as Visual ) ? . GetSelfAndVisualDescendants ( ) . FirstOrDefault ( x = > x is IInputElement ) as IInputElement ;
var root = ( _ contentRoot as Visual ) ? . GetSelfAndVisualDescendants ( ) . FirstOrDefault ( x = > x is IInputElement ) as IInputElement ;
@ -339,52 +313,59 @@ namespace Avalonia.Input
return GetFocusManager ( searchScope ) ? . GetLastFocusableElement ( searchScope ) ;
return GetFocusManager ( searchScope ) ? . GetLastFocusableElement ( searchScope ) ;
}
}
/// <summary>
/// <inheritdoc />
/// Retrieves the element that should receive focus based on the specified navigation direction.
public IInputElement ? FindNextElement ( NavigationDirection direction , FindNextElementOptions ? options = null )
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public IInputElement ? FindNextElement ( NavigationDirection direction )
{
{
var xyOption = new XYFocusOptions ( )
ValidateDirection ( direction ) ;
{
UpdateManifold = false
} ;
return FindNextFocus ( direction , xyOption ) ;
var focusOptions = ToFocusOptions ( options , false ) ;
var result = FindNextFocus ( direction , focusOptions ) ;
_ reusableFocusOptions = focusOptions ;
return result ;
}
}
/// <summary>
private static void ValidateDirection ( NavigationDirection direction )
/// 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 ) ) ;
if ( direction is not (
NavigationDirection . Next or
NavigationDirection . Previous or
NavigationDirection . Up or
NavigationDirection . Down or
NavigationDirection . Left or
NavigationDirection . Right ) )
{
throw new ArgumentOutOfRangeException (
nameof ( direction ) ,
direction ,
$"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " +
$"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," +
$" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported" ) ;
}
}
}
private static XYFocusOptions ValidateAndCreateFocusOptions ( NavigationDirection direction , FindNextElementOptions options )
private XYFocusOptions ToFocusOptions ( FindNextElementOptions ? options , bool updateManifold )
{
{
if ( direction is not NavigationDirection . Up
// XYFocus only uses the options and never modifies them; we can cache and reset them between calls.
and not NavigationDirection . Down
var focusOptions = _ reusableFocusOptions ;
and not NavigationDirection . Left
_ reusableFocusOptions = null ;
and not NavigationDirection . Right )
if ( focusOptions is null )
focusOptions = new XYFocusOptions ( ) ;
else
focusOptions . Reset ( ) ;
if ( options is not null )
{
{
throw new ArgumentOutOfRangeException ( nameof ( direction ) ,
focusOptions . SearchRoot = options . SearchRoot ;
$"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported" ) ;
focusOptions . ExclusionRect = options . ExclusionRect ;
focusOptions . FocusHintRectangle = options . FocusHintRectangle ;
focusOptions . NavigationStrategyOverride = options . NavigationStrategyOverride ;
focusOptions . IgnoreOcclusivity = options . IgnoreOcclusivity ;
}
}
return new XYFocusOptions
focusOptions . UpdateManifold = updateManifold ;
{
UpdateManifold = false ,
return focusOptions ;
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 )
internal IInputElement ? FindNextFocus ( NavigationDirection direction , XYFocusOptions focusOptions , bool updateManifolds = true )