diff --git a/src/Avalonia.Base/Input/FocusChangingEventArgs.cs b/src/Avalonia.Base/Input/FocusChangingEventArgs.cs
new file mode 100644
index 0000000000..0c392c1989
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Provides data for focus changing.
+ ///
+ internal FocusChangingEventArgs(RoutedEvent routedEvent) : base(routedEvent)
+ {
+ }
+
+ ///
+ /// Gets or sets the element that focus has moved to.
+ ///
+ public IInputElement? NewFocusedElement { get; internal set; }
+
+ ///
+ /// Gets or sets the element that previously had focus.
+ ///
+ public IInputElement? OldFocusedElement { get; init; }
+
+ ///
+ /// Gets or sets a value indicating how the change in focus occurred.
+ ///
+ public NavigationMethod NavigationMethod { get; init; }
+
+ ///
+ /// Gets or sets any key modifiers active at the time of focus.
+ ///
+ public KeyModifiers KeyModifiers { get; init; }
+
+ ///
+ /// Gets whether focus change is canceled.
+ ///
+ public bool Canceled { get; private set; }
+
+ internal bool CanCancelOrRedirectFocus { get; init; }
+
+ ///
+ /// Attempts to cancel the current focus change
+ ///
+ /// true if focus change was cancelled; otherwise, false
+ public bool TryCancel()
+ {
+ Canceled = CanCancelOrRedirectFocus;
+
+ return Canceled;
+ }
+
+ ///
+ /// Attempts to redirect focus from the targeted element to the specified element.
+ ///
+ public bool TrySetNewFocusedElement(IInputElement? inputElement)
+ {
+ if(CanCancelOrRedirectFocus)
+ {
+ NewFocusedElement = inputElement;
+ }
+
+ return inputElement == NewFocusedElement;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs
index cee17e4dd1..ea3430ea20 100644
--- a/src/Avalonia.Base/Input/FocusManager.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs
index c9a033ef27..badf2aa6e7 100644
--- a/src/Avalonia.Base/Input/InputElement.cs
+++ b/src/Avalonia.Base/Input/InputElement.cs
@@ -82,12 +82,24 @@ namespace Avalonia.Input
public static readonly RoutedEvent GotFocusEvent =
RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent GettingFocusEvent =
+ RoutedEvent.Register(nameof(GettingFocus), RoutingStrategies.Bubble);
+
///
/// Defines the event.
///
public static readonly RoutedEvent LostFocusEvent =
RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent LosingFocusEvent =
+ RoutedEvent.Register(nameof(LosingFocus), RoutingStrategies.Bubble);
+
///
/// Defines the event.
///
@@ -213,6 +225,8 @@ namespace Avalonia.Input
GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocusCore(e));
LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocusCore(e));
+ GettingFocusEvent.AddClassHandler((x, e) => x.OnGettingFocus(e));
+ LosingFocusEvent.AddClassHandler((x, e) => x.OnLosingFocus(e));
KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e));
KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e));
TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e));
@@ -249,6 +263,15 @@ namespace Avalonia.Input
remove { RemoveHandler(GotFocusEvent, value); }
}
+ ///
+ /// Occurs before the control receives focus.
+ ///
+ public event EventHandler? GettingFocus
+ {
+ add { AddHandler(GettingFocusEvent, value); }
+ remove { RemoveHandler(GettingFocusEvent, value); }
+ }
+
///
/// Occurs when the control loses focus.
///
@@ -258,6 +281,15 @@ namespace Avalonia.Input
remove { RemoveHandler(LostFocusEvent, value); }
}
+ ///
+ /// Occurs before the control loses focus.
+ ///
+ public event EventHandler? LosingFocus
+ {
+ add { AddHandler(LosingFocusEvent, value); }
+ remove { RemoveHandler(LosingFocusEvent, value); }
+ }
+
///
/// Occurs when a key is pressed while the control has focus.
///
@@ -543,6 +575,16 @@ namespace Avalonia.Input
OnGotFocus(e);
}
+ protected virtual void OnGettingFocus(FocusChangingEventArgs e)
+ {
+
+ }
+
+ protected virtual void OnLosingFocus(FocusChangingEventArgs e)
+ {
+
+ }
+
///
/// Invoked when an unhandled reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs
index a6d2ab7a86..3971ef9364 100644
--- a/src/Avalonia.Base/Input/KeyboardDevice.cs
+++ b/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));
+ }
}
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 9d95ea5737..7ef1907117 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/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;
diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs
index c486a66da0..5d86ce13ad 100644
--- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs
+++ b/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);
+ }
}
}