Browse Source

Add ability to cancel focus change (#18373)

* implement pre events for focus change

* add cancelling api to focus change

* add overload for SetFocusedElement

* add focus redirection

* update with api change requests
pull/19055/head
Emmanuel Hansen 8 months ago
committed by GitHub
parent
commit
45cd39829a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 70
      src/Avalonia.Base/Input/FocusChangingEventArgs.cs
  2. 2
      src/Avalonia.Base/Input/FocusManager.cs
  3. 42
      src/Avalonia.Base/Input/InputElement.cs
  4. 88
      src/Avalonia.Base/Input/KeyboardDevice.cs
  5. 2
      src/Avalonia.Controls/TopLevel.cs
  6. 49
      tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs

70
src/Avalonia.Base/Input/FocusChangingEventArgs.cs

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class FocusChangingEventArgs : RoutedEventArgs
{
/// <summary>
/// Provides data for focus changing.
/// </summary>
internal FocusChangingEventArgs(RoutedEvent routedEvent) : base(routedEvent)
{
}
/// <summary>
/// Gets or sets the element that focus has moved to.
/// </summary>
public IInputElement? NewFocusedElement { get; internal set; }
/// <summary>
/// Gets or sets the element that previously had focus.
/// </summary>
public IInputElement? OldFocusedElement { get; init; }
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>
public NavigationMethod NavigationMethod { get; init; }
/// <summary>
/// Gets or sets any key modifiers active at the time of focus.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
/// <summary>
/// Gets whether focus change is canceled.
/// </summary>
public bool Canceled { get; private set; }
internal bool CanCancelOrRedirectFocus { get; init; }
/// <summary>
/// Attempts to cancel the current focus change
/// </summary>
/// <returns>true if focus change was cancelled; otherwise, false</returns>
public bool TryCancel()
{
Canceled = CanCancelOrRedirectFocus;
return Canceled;
}
/// <summary>
/// Attempts to redirect focus from the targeted element to the specified element.
/// </summary>
public bool TrySetNewFocusedElement(IInputElement? inputElement)
{
if(CanCancelOrRedirectFocus)
{
NewFocusedElement = inputElement;
}
return inputElement == NewFocusedElement;
}
}
}

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

@ -78,7 +78,7 @@ namespace Avalonia.Input
else
{
_focusRoot = null;
keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
return false;
}
}

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

@ -82,12 +82,24 @@ namespace Avalonia.Input
public static readonly RoutedEvent<GotFocusEventArgs> GotFocusEvent =
RoutedEvent.Register<InputElement, GotFocusEventArgs>(nameof(GotFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="GettingFocus"/> event.
/// </summary>
public static readonly RoutedEvent<FocusChangingEventArgs> GettingFocusEvent =
RoutedEvent.Register<InputElement, FocusChangingEventArgs>(nameof(GettingFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="LostFocus"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> LostFocusEvent =
RoutedEvent.Register<InputElement, RoutedEventArgs>(nameof(LostFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="LosingFocus"/> event.
/// </summary>
public static readonly RoutedEvent<FocusChangingEventArgs> LosingFocusEvent =
RoutedEvent.Register<InputElement, FocusChangingEventArgs>(nameof(LosingFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="KeyDown"/> event.
/// </summary>
@ -213,6 +225,8 @@ namespace Avalonia.Input
GotFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnGotFocusCore(e));
LostFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnLostFocusCore(e));
GettingFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnGettingFocus(e));
LosingFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnLosingFocus(e));
KeyDownEvent.AddClassHandler<InputElement>((x, e) => x.OnKeyDown(e));
KeyUpEvent.AddClassHandler<InputElement>((x, e) => x.OnKeyUp(e));
TextInputEvent.AddClassHandler<InputElement>((x, e) => x.OnTextInput(e));
@ -249,6 +263,15 @@ namespace Avalonia.Input
remove { RemoveHandler(GotFocusEvent, value); }
}
/// <summary>
/// Occurs before the control receives focus.
/// </summary>
public event EventHandler<FocusChangingEventArgs>? GettingFocus
{
add { AddHandler(GettingFocusEvent, value); }
remove { RemoveHandler(GettingFocusEvent, value); }
}
/// <summary>
/// Occurs when the control loses focus.
/// </summary>
@ -258,6 +281,15 @@ namespace Avalonia.Input
remove { RemoveHandler(LostFocusEvent, value); }
}
/// <summary>
/// Occurs before the control loses focus.
/// </summary>
public event EventHandler<FocusChangingEventArgs>? LosingFocus
{
add { AddHandler(LosingFocusEvent, value); }
remove { RemoveHandler(LosingFocusEvent, value); }
}
/// <summary>
/// Occurs when a key is pressed while the control has focus.
/// </summary>
@ -543,6 +575,16 @@ namespace Avalonia.Input
OnGotFocus(e);
}
protected virtual void OnGettingFocus(FocusChangingEventArgs e)
{
}
protected virtual void OnLosingFocus(FocusChangingEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="GotFocusEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling

88
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -129,41 +129,89 @@ namespace Avalonia.Input
}
public void SetFocusedElement(
IInputElement? element,
IInputElement? element,
NavigationMethod method,
KeyModifiers keyModifiers)
{
SetFocusedElement(element, method, keyModifiers, true);
}
public void SetFocusedElement(
IInputElement? element,
NavigationMethod method,
KeyModifiers keyModifiers,
bool isFocusChangeCancellable)
{
if (element != FocusedElement)
{
var interactive = FocusedElement as Interactive;
if (FocusedElement != null &&
(!((Visual)FocusedElement).IsAttachedToVisualTree ||
_focusedRoot != ((Visual?)element)?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
bool changeFocus = true;
var losingFocus = new FocusChangingEventArgs(InputElement.LosingFocusEvent)
{
ClearChildrenFocusWithin(_focusedRoot, true);
OldFocusedElement = FocusedElement,
NewFocusedElement = element,
NavigationMethod = method,
KeyModifiers = keyModifiers,
CanCancelOrRedirectFocus = isFocusChangeCancellable
};
interactive?.RaiseEvent(losingFocus);
if (losingFocus.Canceled)
{
changeFocus = false;
}
SetIsFocusWithin(FocusedElement, element);
_focusedElement = element;
_focusedRoot = ((Visual?)_focusedElement)?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs
if (changeFocus && losingFocus.NewFocusedElement is Interactive newFocus)
{
RoutedEvent = InputElement.LostFocusEvent,
});
var gettingFocus = new FocusChangingEventArgs(InputElement.GettingFocusEvent)
{
OldFocusedElement = FocusedElement,
NewFocusedElement = losingFocus.NewFocusedElement,
NavigationMethod = method,
KeyModifiers = keyModifiers,
CanCancelOrRedirectFocus = isFocusChangeCancellable
};
interactive = element as Interactive;
newFocus.RaiseEvent(gettingFocus);
interactive?.RaiseEvent(new GotFocusEventArgs
if (gettingFocus.Canceled)
{
changeFocus = false;
}
element = gettingFocus.NewFocusedElement;
}
if (changeFocus)
{
NavigationMethod = method,
KeyModifiers = keyModifiers,
});
// Clear keyboard focus from currently focused element
if (FocusedElement != null &&
(!((Visual)FocusedElement).IsAttachedToVisualTree ||
_focusedRoot != ((Visual?)element)?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
{
ClearChildrenFocusWithin(_focusedRoot, true);
}
SetIsFocusWithin(FocusedElement, element);
_focusedElement = element;
_focusedRoot = ((Visual?)_focusedElement)?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs(InputElement.LostFocusEvent));
_textInputManager.SetFocusedElement(element);
RaisePropertyChanged(nameof(FocusedElement));
(element as Interactive)?.RaiseEvent(new GotFocusEventArgs
{
NavigationMethod = method,
KeyModifiers = keyModifiers,
});
_textInputManager.SetFocusedElement(element);
RaisePropertyChanged(nameof(FocusedElement));
}
}
}

2
src/Avalonia.Controls/TopLevel.cs

@ -965,7 +965,7 @@ namespace Avalonia.Controls
focused = focused.VisualParent;
if (focused == this)
KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
}
protected override bool BypassFlowDirectionPolicies => true;

49
tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs

@ -159,5 +159,54 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Equal(1, propertyChangedRaised);
}
[Fact]
public void Cancelled_Focus_Change_Should_Not_Send_Got_Focus_Event()
{
var target = new KeyboardDevice();
var focused = new Control();
var root = new TestRoot();
bool focusCancelled = false;
focused.GettingFocus += (s, e) =>
{
focusCancelled = e.TryCancel();
};
focused.GotFocus += (s, e) =>
{
focusCancelled = false;
};
target.SetFocusedElement(
focused,
NavigationMethod.Unspecified,
KeyModifiers.None);
Assert.True(focusCancelled);
}
[Fact]
public void Redirected_Focus_Should_Change_Focused_Element()
{
var target = new KeyboardDevice();
var first = new Control();
var second = new Control();
var stack = new StackPanel();
stack.Children.AddRange(new[] { first, second });
var root = new TestRoot(stack);
first.GettingFocus += (s, e) =>
{
e.TrySetNewFocusedElement(second);
};
target.SetFocusedElement(
first,
NavigationMethod.Unspecified,
KeyModifiers.None);
Assert.True(second.IsFocused);
}
}
}

Loading…
Cancel
Save