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); + } } }