From 7295446fe1b8120190ba19a25b41d77f8bdb4be3 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Fri, 12 May 2023 23:14:39 +0300 Subject: [PATCH 1/8] Ignore xmlns without CLR namespace on type resolution --- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f8eab5b654..a78af8e35c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -156,10 +156,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime throw new ArgumentException("Unable to resolve namespace for type " + qualifiedTypeName); foreach (var entry in lst) { - var asm = Assembly.Load(new AssemblyName(entry.ClrAssemblyName)); - var resolved = asm.GetType(entry.ClrNamespace + "." + name); - if (resolved != null) - return resolved; + if (entry.ClrAssemblyName is { Length: > 0 }) + { + var asm = Assembly.Load(new AssemblyName(entry.ClrAssemblyName)); + var resolved = asm.GetType(entry.ClrNamespace + "." + name); + if (resolved != null) + return resolved; + } } throw new ArgumentException( From 083fcb54e0f4781dfd5a94a93480bfa84e7ddc0a Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Fri, 12 May 2023 23:49:39 +0300 Subject: [PATCH 2/8] Improved XAML type resolution exception --- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index a78af8e35c..5b8388594f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -154,20 +154,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var namespaces = _nsInfo.XmlNamespaces; if (!namespaces.TryGetValue(ns, out var lst)) throw new ArgumentException("Unable to resolve namespace for type " + qualifiedTypeName); - foreach (var entry in lst) + var resolvable = lst.Where(static e => e.ClrAsseblyName is { Length: > 0 }); + foreach (var entry in resolvable) { - if (entry.ClrAssemblyName is { Length: > 0 }) - { - var asm = Assembly.Load(new AssemblyName(entry.ClrAssemblyName)); - var resolved = asm.GetType(entry.ClrNamespace + "." + name); - if (resolved != null) - return resolved; - } + var asm = Assembly.Load(new AssemblyName(entry.ClrAssemblyName)); + var resolved = asm.GetType(entry.ClrNamespace + "." + name); + if (resolved != null) + return resolved; } throw new ArgumentException( $"Unable to resolve type {qualifiedTypeName} from any of the following locations: " + - string.Join(",", lst.Select(e => $"`{e.ClrAssemblyName}:{e.ClrNamespace}.{name}`"))); + string.Join(",", resolvable.Select(e => $"`clr-namespace:{e.ClrNamespace};assembly={e.ClrAssemblyName}`"))) + { HelpLink = "https://docs.avaloniaui.net/guides/basics/introduction-to-xaml#valid-xaml-namespaces" }; } } From 75eb8a78d4e88c709ca21fae54ee6aa34d2ee361 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Mon, 15 May 2023 13:21:05 +0300 Subject: [PATCH 3/8] Fixed typo --- .../Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 5b8388594f..be6b37bb02 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -154,7 +154,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var namespaces = _nsInfo.XmlNamespaces; if (!namespaces.TryGetValue(ns, out var lst)) throw new ArgumentException("Unable to resolve namespace for type " + qualifiedTypeName); - var resolvable = lst.Where(static e => e.ClrAsseblyName is { Length: > 0 }); + var resolvable = lst.Where(static e => e.ClrAssemblyName is { Length: > 0 }); foreach (var entry in resolvable) { var asm = Assembly.Load(new AssemblyName(entry.ClrAssemblyName)); From 20064647dd52c6c58eb8e1502313e44037e739d6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 16 May 2023 17:59:32 -0400 Subject: [PATCH 4/8] Limit IFocusManager API, extend IInputElement API, remove visible static properties --- src/Avalonia.Base/Input/AccessKeyHandler.cs | 4 +- src/Avalonia.Base/Input/FocusManager.cs | 60 ++++++++++++------- src/Avalonia.Base/Input/IFocusManager.cs | 36 ++--------- src/Avalonia.Base/Input/IFocusScope.cs | 3 + src/Avalonia.Base/Input/IInputElement.cs | 4 +- src/Avalonia.Base/Input/IInputRoot.cs | 8 +++ src/Avalonia.Base/Input/IKeyboardDevice.cs | 8 +-- src/Avalonia.Base/Input/InputElement.cs | 17 +++--- src/Avalonia.Base/Input/KeyboardDevice.cs | 4 +- .../Input/KeyboardNavigationHandler.cs | 4 +- src/Avalonia.Base/Input/MouseDevice.cs | 2 + .../Input/Navigation/TabNavigation.cs | 6 +- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 +- .../Controls/ViewManager.cs | 2 +- .../AutoCompleteBox/AutoCompleteBox.cs | 2 +- src/Avalonia.Controls/Calendar/Calendar.cs | 2 +- src/Avalonia.Controls/ComboBox.cs | 3 +- src/Avalonia.Controls/ContextMenu.cs | 4 +- .../DateTimePickers/DatePickerPresenter.cs | 11 ++-- .../DateTimePickers/TimePickerPresenter.cs | 6 +- .../Flyouts/PopupFlyoutBase.cs | 4 +- src/Avalonia.Controls/ItemsControl.cs | 11 ++-- src/Avalonia.Controls/Primitives/Popup.cs | 6 +- src/Avalonia.Controls/TopLevel.cs | 5 +- src/Avalonia.Controls/TreeView.cs | 3 +- src/Avalonia.Controls/TreeViewItem.cs | 2 +- src/Avalonia.Controls/WindowBase.cs | 4 +- .../Diagnostics/DevTools.cs | 8 +-- .../LinuxFramebufferPlatform.cs | 2 +- .../WinForms/WinFormsAvaloniaControlHost.cs | 2 +- .../Input/WindowsKeyboardDevice.cs | 2 +- .../Input/InputElement_Focus.cs | 58 +++++++++--------- .../Input/PointerOverTests.cs | 4 +- .../FlyoutTests.cs | 10 ++-- .../ItemsControlTests.cs | 6 +- .../ListBoxTests.cs | 8 +-- .../DefaultMenuInteractionHandlerTests.cs | 4 +- .../Primitives/PopupTests.cs | 11 ++-- .../TabControlTests.cs | 8 +-- .../TreeViewTests.cs | 16 ++--- tests/Avalonia.LeakTests/ControlTests.cs | 4 +- tests/Avalonia.UnitTests/TestRoot.cs | 1 + 42 files changed, 192 insertions(+), 178 deletions(-) diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 2bd9fce947..23d1f51730 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -141,9 +141,11 @@ namespace Avalonia.Input if (MainMenu == null || !MainMenu.IsOpen) { + var focusManager = FocusManager.GetFocusManager(e.Source as IInputElement); + // TODO: Use FocusScopes to store the current element and restore it when context menu is closed. // Save currently focused input element. - _restoreFocusElement = FocusManager.Instance?.Current; + _restoreFocusElement = focusManager?.GetFocusedElement(); // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index c8de7267ca..4c464a44ae 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -10,6 +11,7 @@ namespace Avalonia.Input /// /// Manages focus for the application. /// + [Unstable] public class FocusManager : IFocusManager { /// @@ -21,7 +23,7 @@ namespace Avalonia.Input /// /// Initializes a new instance of the class. /// - static FocusManager() + internal FocusManager() { InputElement.PointerPressedEvent.AddClassHandler( typeof(IInputElement), @@ -29,15 +31,12 @@ namespace Avalonia.Input RoutingStrategies.Tunnel); } - /// - /// Gets the instance of the . - /// - public static IFocusManager? Instance => AvaloniaLocator.Current.GetService(); + private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; /// /// Gets the currently focused . /// - public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; + public IInputElement? GetFocusedElement() => Current; /// /// Gets the current focus scope. @@ -54,7 +53,7 @@ namespace Avalonia.Input /// The control to focus. /// The method by which focus was changed. /// Any key modifiers active at the time of focus. - public void Focus( + public bool Focus( IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) @@ -67,7 +66,7 @@ namespace Avalonia.Input if (scope != null) { Scope = scope; - SetFocusedElement(scope, control, method, keyModifiers); + return SetFocusedElement(scope, control, method, keyModifiers); } } else if (Current != null) @@ -79,28 +78,29 @@ namespace Avalonia.Input _focusScopes.TryGetValue(scope, out var element) && element != null) { - Focus(element, method); - return; + return Focus(element, method); } } if (Scope is object) { // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); + return SetFocusedElement(Scope, null); } } + + return false; } - public IInputElement? GetFocusedElement(IInputElement e) + public void ClearFocus() { - if (e is IFocusScope scope) - { - _focusScopes.TryGetValue(scope, out var result); - return result; - } + Focus(null); + } - return null; + public IInputElement? GetFocusedElement(IFocusScope scope) + { + _focusScopes.TryGetValue(scope, out var result); + return result; } /// @@ -114,7 +114,7 @@ namespace Avalonia.Input /// If the specified scope is the current then the keyboard focus /// will change. /// - public void SetFocusedElement( + public bool SetFocusedElement( IFocusScope scope, IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, @@ -124,7 +124,7 @@ namespace Avalonia.Input if (element is not null && !CanFocus(element)) { - return; + return false; } if (_focusScopes.TryGetValue(scope, out var existingElement)) @@ -144,6 +144,8 @@ namespace Avalonia.Input { KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers); } + + return true; } /// @@ -185,6 +187,20 @@ namespace Avalonia.Input public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; + /// + /// Public API customers should use TopLevel.GetTopLevel(control).FocusManager. + /// But since we have split projects, we can't access TopLevel from Avalonia.Base. + /// That's why we need this helper method instead. + /// + internal static FocusManager? GetFocusManager(IInputElement? element) + { + // Element might not be a visual, and not attached to the root. + // But IFocusManager is always expected to be a FocusManager. + return (FocusManager?)((IInputRoot?)(element as Visual)?.VisualRoot)?.FocusManager + // In our unit tests some elements might not have a root. Remove when + ?? (FocusManager?)AvaloniaLocator.Current.GetService(); + } + /// /// Checks if the specified element can be focused. /// @@ -221,7 +237,7 @@ namespace Avalonia.Input /// /// The event sender. /// The event args. - private static void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) + private void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) { if (sender is null) return; @@ -237,7 +253,7 @@ namespace Avalonia.Input { if (element is IInputElement inputElement && CanFocus(inputElement)) { - Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); + Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); break; } diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs index 0c85cad2f7..5691172f3f 100644 --- a/src/Avalonia.Base/Input/IFocusManager.cs +++ b/src/Avalonia.Base/Input/IFocusManager.cs @@ -11,40 +11,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - IInputElement? Current { get; } + IInputElement? GetFocusedElement(); /// - /// Gets the current focus scope. + /// Clears currently focused element. /// - IFocusScope? Scope { get; } - - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. - void Focus( - IInputElement? control, - NavigationMethod method = NavigationMethod.Unspecified, - KeyModifiers keyModifiers = KeyModifiers.None); - - /// - /// Notifies the focus manager of a change in focus scope. - /// - /// The new focus scope. - /// - /// This should not be called by client code. It is called by an - /// when it activates, e.g. when a Window is activated. - /// - void SetFocusScope(IFocusScope scope); - - /// - /// Notifies the focus manager that a focus scope has been removed. - /// - /// The focus scope to be removed. - /// This should not be called by client code. It is called by an - /// when it deactivates or closes, e.g. when a Window is closed. - void RemoveFocusScope(IFocusScope scope); + [Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")] + void ClearFocus(); } } diff --git a/src/Avalonia.Base/Input/IFocusScope.cs b/src/Avalonia.Base/Input/IFocusScope.cs index 56f558040e..4f7c454263 100644 --- a/src/Avalonia.Base/Input/IFocusScope.cs +++ b/src/Avalonia.Base/Input/IFocusScope.cs @@ -1,5 +1,8 @@ +using Avalonia.Metadata; + namespace Avalonia.Input { + [NotClientImplementable] public interface IFocusScope { } diff --git a/src/Avalonia.Base/Input/IInputElement.cs b/src/Avalonia.Base/Input/IInputElement.cs index 6c20d20b4d..39dc30befd 100644 --- a/src/Avalonia.Base/Input/IInputElement.cs +++ b/src/Avalonia.Base/Input/IInputElement.cs @@ -119,7 +119,9 @@ namespace Avalonia.Input /// /// Focuses the control. /// - void Focus(); + /// The method by which focus was changed. + /// Any key modifiers active at the time of focus. + bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None); /// /// Gets the key bindings for the element. diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs index 344a4eefd7..0100b22ba7 100644 --- a/src/Avalonia.Base/Input/IInputRoot.cs +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -18,6 +18,14 @@ namespace Avalonia.Input /// IKeyboardNavigationHandler KeyboardNavigationHandler { get; } + /// + /// Gets focus manager of the root. + /// + /// + /// Focus manager can be null only if application wasn't initialized yet. + /// + IFocusManager? FocusManager { get; } + /// /// Gets or sets the input element that the pointer is currently over. /// diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs index 0b7b5aaecc..172b58068c 100644 --- a/src/Avalonia.Base/Input/IKeyboardDevice.cs +++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs @@ -44,13 +44,7 @@ namespace Avalonia.Input } [NotClientImplementable] - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged + public interface IKeyboardDevice : IInputDevice { - IInputElement? FocusedElement { get; } - - void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers modifiers); } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 33ddbaedf9..68131e5bf7 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -458,9 +458,10 @@ namespace Avalonia.Input SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); PseudoClasses.Set(":disabled", !value); - if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this) + if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is {} focusManager + && Equals(focusManager.GetFocusedElement(), this)) { - FocusManager.Instance?.Focus(null); + focusManager.ClearFocus(); } } } @@ -491,12 +492,10 @@ namespace Avalonia.Input public GestureRecognizerCollection GestureRecognizers => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); - /// - /// Focuses the control. - /// - public void Focus() + /// + public bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { - FocusManager.Instance?.Focus(this); + return FocusManager.GetFocusManager(this)?.Focus(this, method, keyModifiers) ?? false; } /// @@ -506,7 +505,7 @@ namespace Avalonia.Input if (IsFocused) { - FocusManager.Instance?.Focus(null); + FocusManager.GetFocusManager(this)?.ClearFocus(); } } @@ -649,7 +648,7 @@ namespace Avalonia.Input } else if (change.Property == IsVisibleProperty && !change.GetNewValue() && IsFocused) { - FocusManager.Instance?.Focus(null); + FocusManager.GetFocusManager(this)?.ClearFocus(); } } diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index c46834fff4..f6d2a2195a 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -3,9 +3,11 @@ using System.Runtime.CompilerServices; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Interactivity; +using Avalonia.Metadata; namespace Avalonia.Input { + [Unstable] public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { private IInputElement? _focusedElement; @@ -13,7 +15,7 @@ namespace Avalonia.Input public event PropertyChangedEventHandler? PropertyChanged; - public static IKeyboardDevice? Instance => AvaloniaLocator.Current.GetService(); + internal static KeyboardDevice? Instance => AvaloniaLocator.Current.GetService() as KeyboardDevice; public IInputManager? InputManager => AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index ba909de60f..e96c80da14 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -88,7 +88,7 @@ namespace Avalonia.Input var method = direction == NavigationDirection.Next || direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; - FocusManager.Instance?.Focus(next, method, keyModifiers); + next.Focus(method, keyModifiers); } } @@ -99,7 +99,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnKeyDown(object? sender, KeyEventArgs e) { - var current = FocusManager.Instance?.Current; + var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement(); if (current != null && e.Key == Key.Tab) { diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 44412cd152..4aeffcdd72 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Reactive; using Avalonia.Input.Raw; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Utilities; #pragma warning disable CS0618 @@ -11,6 +12,7 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// + [Unstable] public class MouseDevice : IMouseDevice, IDisposable { private int _clickCount; diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index c460ecf3b3..9697e32926 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -190,9 +190,11 @@ namespace Avalonia.Input.Navigation private static IInputElement? FocusedElement(IInputElement? e) { // Focus delegation is enabled only if keyboard focus is outside the container - if (e != null && !e.IsKeyboardFocusWithin) + if (e != null && !e.IsKeyboardFocusWithin && e is IFocusScope scope) { - var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); + var focusManager = FocusManager.GetFocusManager(e); + + var focusedElement = focusManager?.GetFocusedElement(scope); if (focusedElement != null) { if (!IsFocusScope(e)) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index a55a47fa53..bfcd4750e3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3958,7 +3958,7 @@ namespace Avalonia.Controls { bool focusLeftDataGrid = true; bool dataGridWillReceiveRoutedEvent = true; - Visual focusedObject = FocusManager.Instance.Current as Visual; + Visual focusedObject = FocusManager.GetFocusManager(this)?.GetFocusedElement() as Visual; DataGridColumn editingColumn = null; while (focusedObject != null) @@ -4865,7 +4865,8 @@ namespace Avalonia.Controls if (!ctrl) { // If Enter was used by a TextBox, we shouldn't handle the key - if (FocusManager.Instance.Current is TextBox focusedTextBox && focusedTextBox.AcceptsReturn) + if (FocusManager.GetFocusManager(this)?.GetFocusedElement() is TextBox focusedTextBox + && focusedTextBox.AcceptsReturn) { return false; } diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs index 6b9d7934bf..674389d77c 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs @@ -695,7 +695,7 @@ namespace Avalonia.Controls { Control? focusedElement = null; - if (FocusManager.Instance?.Current is Visual child) + if (TopLevel.GetTopLevel(_owner)?.FocusManager?.GetFocusedElement() is Visual child) { var parent = child.GetVisualParent(); var owner = _owner; diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 20711eecbc..d0b894101f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -762,7 +762,7 @@ namespace Avalonia.Controls /// otherwise, false. protected bool HasFocus() { - Visual? focused = FocusManager.Instance?.Current as Visual; + Visual? focused = FocusManager.GetFocusManager(this)?.GetFocusedElement() as Visual; while (focused != null) { diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 10aadfa759..6468d0b4e8 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1567,7 +1567,7 @@ namespace Avalonia.Controls base.OnPointerReleased(e); if (!HasFocusInternal && e.InitialPressMouseButton == MouseButton.Left) { - FocusManager.Instance?.Focus(this); + Focus(); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f41c00662d..efa319ad54 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -230,8 +230,7 @@ namespace Avalonia.Controls var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) { - FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional); - e.Handled = true; + e.Handled = firstChild.Focus(NavigationMethod.Directional); } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 97a8c6fe97..98b85dc31e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -360,7 +360,7 @@ namespace Avalonia.Controls private void PopupOpened(object? sender, EventArgs e) { - _previousFocus = FocusManager.Instance?.Current; + _previousFocus = FocusManager.GetFocusManager(this)?.GetFocusedElement(); Focus(); _popupHostChangedHandler?.Invoke(_popup!.Host); @@ -390,7 +390,7 @@ namespace Avalonia.Controls } // HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic. - FocusManager.Instance?.Focus(_previousFocus); + _previousFocus?.Focus(); RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 0ae743f30a..fb61ea679c 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -323,10 +323,11 @@ namespace Avalonia.Controls e.Handled = true; break; case Key.Tab: - if (FocusManager.Instance?.Current is IInputElement focus) + var focusManager = FocusManager.GetFocusManager(this); + if (focusManager?.GetFocusedElement() is { } focus) { var nextFocus = KeyboardNavigationHandler.GetNext(focus, NavigationDirection.Next); - KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None); + nextFocus?.Focus(NavigationMethod.Tab); e.Handled = true; } break; @@ -449,15 +450,15 @@ namespace Avalonia.Controls if (monthCol < dayCol && monthCol < yearCol) { - KeyboardDevice.Instance?.SetFocusedElement(_monthSelector, NavigationMethod.Pointer, KeyModifiers.None); + _monthSelector?.Focus(NavigationMethod.Pointer); } else if (dayCol < monthCol && dayCol < yearCol) { - KeyboardDevice.Instance?.SetFocusedElement(_daySelector, NavigationMethod.Pointer, KeyModifiers.None); + _monthSelector?.Focus(NavigationMethod.Pointer); } else if (yearCol < monthCol && yearCol < dayCol) { - KeyboardDevice.Instance?.SetFocusedElement(_yearSelector, NavigationMethod.Pointer, KeyModifiers.None); + _yearSelector?.Focus(NavigationMethod.Pointer); } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index ba06e1b5e6..929ad68c24 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -161,10 +161,10 @@ namespace Avalonia.Controls e.Handled = true; break; case Key.Tab: - if (FocusManager.Instance?.Current is IInputElement focus) + if (FocusManager.GetFocusManager(this)?.GetFocusedElement() is { } focus) { var nextFocus = KeyboardNavigationHandler.GetNext(focus, NavigationDirection.Next); - KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None); + nextFocus?.Focus(NavigationMethod.Tab); e.Handled = true; } break; @@ -216,7 +216,7 @@ namespace Avalonia.Controls _periodSelector.SelectedValue = hr >= 12 ? 1 : 0; SetGrid(); - KeyboardDevice.Instance?.SetFocusedElement(_hourSelector, NavigationMethod.Pointer, KeyModifiers.None); + _hourSelector?.Focus(NavigationMethod.Pointer); } private void SetGrid() diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 5b23b5030f..7fd9fad605 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -250,14 +250,14 @@ namespace Avalonia.Controls.Primitives // Try and focus content inside Flyout if (Popup.Child.Focusable) { - FocusManager.Instance?.Focus(Popup.Child); + Popup.Child.Focus(); } else { var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next); if (nextFocus != null) { - FocusManager.Instance?.Focus(nextFocus); + nextFocus.Focus(); } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1613bda45b..0b6fc2b64b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -566,19 +566,20 @@ namespace Avalonia.Controls { if (!e.Handled) { - var focus = FocusManager.Instance; + var focus = FocusManager.GetFocusManager(this); var direction = e.Key.ToNavigationDirection(); var container = Presenter?.Panel as INavigableContainer; - if (container == null || - focus?.Current == null || + if (focus == null || + container == null || + focus.GetFocusedElement() == null || direction == null || direction.Value.IsTab()) { return; } - Visual? current = focus.Current as Visual; + Visual? current = focus.GetFocusedElement() as Visual; while (current != null) { @@ -588,7 +589,7 @@ namespace Avalonia.Controls if (next != null) { - focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers); + next.Focus(NavigationMethod.Directional, e.KeyModifiers); e.Handled = true; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 80b7841fc7..7ac219aa83 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -727,7 +727,7 @@ namespace Avalonia.Controls.Primitives Closed?.Invoke(this, EventArgs.Empty); - var focusCheck = FocusManager.Instance?.Current; + var focusCheck = FocusManager.GetFocusManager(this)?.GetFocusedElement(); // Focus is set to null as part of popup closing, so we only want to // set focus to PlacementTarget if this is the case @@ -744,7 +744,7 @@ namespace Avalonia.Controls.Primitives if (e is object) { - FocusManager.Instance?.Focus(e); + e.Focus(); } } else @@ -752,7 +752,7 @@ namespace Avalonia.Controls.Primitives var anc = this.FindLogicalAncestorOfType(); if (anc != null) { - FocusManager.Instance?.Focus(anc); + anc.Focus(); } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 85a35a3489..67847f621b 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -452,6 +452,9 @@ namespace Avalonia.Controls /// public IClipboard? Clipboard => PlatformImpl?.TryGetFeature(); + /// + public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { @@ -725,7 +728,7 @@ namespace Avalonia.Controls void PlatformImpl_LostFocus() { - var focused = (Visual?)FocusManager.Instance?.Current; + var focused = (Visual?)FocusManager?.GetFocusedElement(); if (focused == null) return; while (focused.VisualParent != null) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index ef10dcdb22..a499829d4a 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -560,8 +560,7 @@ namespace Avalonia.Controls if (next != null) { - FocusManager.Instance?.Focus(next, NavigationMethod.Directional); - e.Handled = true; + e.Handled = next.Focus(NavigationMethod.Directional); } } else diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index cf12c12447..a48706cb19 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -238,7 +238,7 @@ namespace Avalonia.Controls } else { - FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional); + treeViewItem.Focus(NavigationMethod.Directional); } return true; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index ac47e744e0..b19ad49820 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -234,7 +234,7 @@ namespace Avalonia.Controls if (this is IFocusScope scope) { - FocusManager.Instance?.RemoveFocusScope(scope); + ((FocusManager?)FocusManager)?.RemoveFocusScope(scope); } base.HandleClosed(); @@ -326,7 +326,7 @@ namespace Avalonia.Controls if (scope != null) { - FocusManager.Instance?.SetFocusScope(scope); + ((FocusManager?)FocusManager)?.SetFocusScope(scope); } IsActive = true; diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index cb896fd633..ff49d0dd87 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -86,7 +86,7 @@ namespace Avalonia.Diagnostics private static IDisposable Open(IDevToolsTopLevelGroup topLevelGroup, DevToolsOptions options, Window? owner, Application? app) { - var focussedControl = KeyboardDevice.Instance?.FocusedElement as Control; + var focusedControl = owner?.FocusManager?.GetFocusedElement() as Control; AvaloniaObject root = topLevelGroup switch { ClassicDesktopStyleApplicationLifetimeTopLevelGroup gr => new Controls.Application(gr, app ?? Application.Current!), @@ -98,7 +98,7 @@ namespace Avalonia.Diagnostics if (s_open.TryGetValue(topLevelGroup, out var mainWindow)) { mainWindow.Activate(); - mainWindow.SelectedControl(focussedControl); + mainWindow.SelectedControl(focusedControl); return Disposable.Empty; } if (topLevelGroup.Items.Count == 1 && topLevelGroup.Items is not INotifyCollectionChanged) @@ -110,7 +110,7 @@ namespace Avalonia.Diagnostics if (group.Key.Items.Contains(singleTopLevel)) { group.Value.Activate(); - group.Value.SelectedControl(focussedControl); + group.Value.SelectedControl(focusedControl); return Disposable.Empty; } } @@ -124,7 +124,7 @@ namespace Avalonia.Diagnostics Tag = topLevelGroup }; window.SetOptions(options); - window.SelectedControl(focussedControl); + window.SelectedControl(focusedControl); window.Closed += DevToolsClosed; s_open.Add(topLevelGroup, window); if (options.ShowAsChildWindow && owner is not null) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index bc178c8ecb..c352b5f9bf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -107,7 +107,7 @@ namespace Avalonia.LinuxFramebuffer if (_topLevel is IFocusScope scope) { - FocusManager.Instance?.SetFocusScope(scope); + ((FocusManager)_topLevel.FocusManager).SetFocusScope(scope); } } diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index a8060d3fbf..8d73bde919 100644 --- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -22,7 +22,7 @@ namespace Avalonia.Win32.Embedding UnmanagedMethods.SetParent(WindowHandle, Handle); _root.Prepare(); if (_root.IsFocused) - FocusManager.Instance.Focus(null); + _root.FocusManager.ClearFocus(); _root.GotFocus += RootGotFocus; FixPosition(); diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index 7e1e22579b..4f9b6c54d3 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -5,7 +5,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - class WindowsKeyboardDevice : KeyboardDevice + internal class WindowsKeyboardDevice : KeyboardDevice { private readonly byte[] _keyStates = new byte[256]; diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index ac1547d09f..aa2e47de2d 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); - Assert.Same(target, FocusManager.Instance.Current); + Assert.Same(target, root.FocusManager.GetFocusedElement()); } } @@ -39,14 +39,14 @@ namespace Avalonia.Base.UnitTests.Input Child = target = new Button() { IsVisible = false} }; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); target.Focus(); Assert.False(target.IsFocused); Assert.False(target.IsKeyboardFocusWithin); - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -67,14 +67,14 @@ namespace Avalonia.Base.UnitTests.Input } }; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); target.Focus(); Assert.False(target.IsFocused); Assert.False(target.IsKeyboardFocusWithin); - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -100,11 +100,11 @@ namespace Avalonia.Base.UnitTests.Input first.Focus(); - Assert.Same(first, FocusManager.Instance.Current); + Assert.Same(first, root.FocusManager.GetFocusedElement()); second.Focus(); - Assert.Same(first, FocusManager.Instance.Current); + Assert.Same(first, root.FocusManager.GetFocusedElement()); } } @@ -120,14 +120,14 @@ namespace Avalonia.Base.UnitTests.Input Child = target = new Button() { IsEnabled = false } }; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); target.Focus(); Assert.False(target.IsFocused); Assert.False(target.IsKeyboardFocusWithin); - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -148,14 +148,14 @@ namespace Avalonia.Base.UnitTests.Input } }; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); target.Focus(); Assert.False(target.IsFocused); Assert.False(target.IsKeyboardFocusWithin); - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -201,7 +201,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); target.IsVisible = false; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -224,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); container.IsVisible = false; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -243,7 +243,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); target.IsEnabled = false; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -266,7 +266,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); container.IsEnabled = false; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -285,7 +285,7 @@ namespace Avalonia.Base.UnitTests.Input target.Focus(); root.Child = null; - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } @@ -312,13 +312,13 @@ namespace Avalonia.Base.UnitTests.Input target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus")); Assert.False(target2.IsFocused); Assert.False(target2.Classes.Contains(":focus")); - FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + target2.Focus(NavigationMethod.Tab); Assert.False(target1.IsFocused); Assert.False(target1.Classes.Contains(":focus")); Assert.True(target2.IsFocused); @@ -348,19 +348,19 @@ namespace Avalonia.Base.UnitTests.Input target1.ApplyTemplate(); target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.False(target1.Classes.Contains(":focus-visible")); Assert.False(target2.IsFocused); Assert.False(target2.Classes.Contains(":focus-visible")); - FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + target2.Focus(NavigationMethod.Tab); Assert.False(target1.IsFocused); Assert.False(target1.Classes.Contains(":focus-visible")); Assert.True(target2.IsFocused); Assert.True(target2.Classes.Contains(":focus-visible")); - FocusManager.Instance?.Focus(target1, NavigationMethod.Directional); + target1.Focus(NavigationMethod.Directional); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus-visible")); Assert.False(target2.IsFocused); @@ -390,7 +390,7 @@ namespace Avalonia.Base.UnitTests.Input target1.ApplyTemplate(); target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus-within")); Assert.True(target1.IsKeyboardFocusWithin); @@ -425,7 +425,7 @@ namespace Avalonia.Base.UnitTests.Input target1.ApplyTemplate(); target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus-within")); Assert.True(target1.IsKeyboardFocusWithin); @@ -436,7 +436,7 @@ namespace Avalonia.Base.UnitTests.Input Assert.True(root.Classes.Contains(":focus-within")); Assert.True(root.IsKeyboardFocusWithin); - FocusManager.Instance?.Focus(target2); + target2.Focus(); Assert.False(target1.IsFocused); Assert.False(target1.Classes.Contains(":focus-within")); @@ -478,7 +478,7 @@ namespace Avalonia.Base.UnitTests.Input target1.ApplyTemplate(); target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus-within")); Assert.True(target1.IsKeyboardFocusWithin); @@ -534,7 +534,7 @@ namespace Avalonia.Base.UnitTests.Input target1.ApplyTemplate(); target2.ApplyTemplate(); - FocusManager.Instance?.Focus(target1); + target1.Focus(); Assert.True(target1.IsFocused); Assert.True(target1.Classes.Contains(":focus-within")); Assert.True(target1.IsKeyboardFocusWithin); @@ -545,7 +545,7 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1); - FocusManager.Instance?.Focus(target2); + target2.Focus(); Assert.False(target1.IsFocused); Assert.False(target1.Classes.Contains(":focus-within")); @@ -578,9 +578,9 @@ namespace Avalonia.Base.UnitTests.Input }; target.Focus(); - FocusManager.Instance.Focus(null); + root.FocusManager.ClearFocus(); - Assert.Null(FocusManager.Instance.Current); + Assert.Null(root.FocusManager.GetFocusedElement()); } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 629188800a..ae6b601896 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -20,7 +20,9 @@ namespace Avalonia.Base.UnitTests.Input [Fact] public void Close_Should_Remove_PointerOver() { - using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + using var app = UnitTestApplication.Start(new TestServices( + inputManager: new InputManager(), + focusManager: new FocusManager())); var renderer = RendererMocks.CreateRenderer(); var device = CreatePointerDeviceMock().Object; diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs index 7767de11c7..db12de9db9 100644 --- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs @@ -288,10 +288,10 @@ namespace Avalonia.Controls.UnitTests window.Show(); button.Focus(); - Assert.True(FocusManager.Instance?.Current == button); + Assert.True(window.FocusManager.GetFocusedElement() == button); button.Flyout.ShowAt(button); Assert.False(button.IsFocused); - Assert.True(FocusManager.Instance?.Current == flyoutTextBox); + Assert.True(window.FocusManager.GetFocusedElement() == flyoutTextBox); } } @@ -322,10 +322,10 @@ namespace Avalonia.Controls.UnitTests window.Content = button; window.Show(); - FocusManager.Instance?.Focus(button); - Assert.True(FocusManager.Instance?.Current == button); + button.Focus(); + Assert.True(window.FocusManager.GetFocusedElement() == button); button.Flyout.ShowAt(button); - Assert.True(FocusManager.Instance?.Current == button); + Assert.True(window.FocusManager.GetFocusedElement() == button); } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 86249c66ff..58e4ec1b75 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -576,8 +576,9 @@ namespace Avalonia.Controls.UnitTests }); var panel = Assert.IsAssignableFrom(target.ItemsPanelRoot); + var focusManager = ((IInputRoot)target.VisualRoot!).FocusManager; - Assert.Equal(panel.Children[1], FocusManager.Instance!.Current); + Assert.Equal(panel.Children[1], focusManager?.GetFocusedElement()); } [Fact] @@ -601,8 +602,9 @@ namespace Avalonia.Controls.UnitTests }); var panel = Assert.IsAssignableFrom(target.ItemsPanelRoot); + var focusManager = ((IInputRoot)target.VisualRoot!).FocusManager; - Assert.Equal(panel.Children[2], FocusManager.Instance!.Current); + Assert.Equal(panel.Children[2], focusManager?.GetFocusedElement()); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 72f476a3b0..18c1136ccb 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -979,7 +979,7 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(button, Key.Tab); var item = target.ContainerFromIndex(0); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); } [Fact] @@ -1026,17 +1026,17 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(button, Key.Tab); var item = target.ContainerFromIndex(1); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); RaiseKeyEvent(item, Key.Tab); - Assert.Same(button, FocusManager.Instance.Current); + Assert.Same(button, root.FocusManager.GetFocusedElement()); target.Selection.AnchorIndex = 2; RaiseKeyEvent(button, Key.Tab); item = target.ContainerFromIndex(2); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); } private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0) diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index e5c96dcab6..eaf95b0c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -267,7 +267,7 @@ namespace Avalonia.Controls.UnitTests.Platform target.KeyDown(item.Object, e); parentItem.Verify(x => x.Close()); - parentItem.Verify(x => x.Focus()); + parentItem.Verify(x => x.Focus(It.IsAny(), It.IsAny())); Assert.True(e.Handled); } @@ -351,7 +351,7 @@ namespace Avalonia.Controls.UnitTests.Platform target.KeyDown(item.Object, e); parentItem.Verify(x => x.Close()); - parentItem.Verify(x => x.Focus()); + parentItem.Verify(x => x.Focus(It.IsAny(), It.IsAny())); Assert.True(e.Handled); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 765f2d1c19..51399d1202 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -642,10 +642,11 @@ namespace Avalonia.Controls.UnitTests.Primitives tb.Focus(); - Assert.True(FocusManager.Instance?.Current == tb); + var focusManager = TopLevel.GetTopLevel(tb)!.FocusManager; + tb = Assert.IsType(focusManager.GetFocusedElement()); //Ensure focus remains in the popup - var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next); + var nextFocus = KeyboardNavigationHandler.GetNext(tb, NavigationDirection.Next); Assert.True(nextFocus == b); @@ -684,7 +685,8 @@ namespace Avalonia.Controls.UnitTests.Primitives p.Close(); - var focus = FocusManager.Instance?.Current; + var focusManager = window.FocusManager; + var focus = focusManager.GetFocusedElement(); Assert.True(focus == window); } } @@ -723,7 +725,8 @@ namespace Avalonia.Controls.UnitTests.Primitives windowTB.Focus(); - var focus = FocusManager.Instance?.Current; + var focusManager = window.FocusManager; + var focus = focusManager.GetFocusedElement(); Assert.True(focus == windowTB); diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 0d3eb80ae7..398ac3ff43 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -461,7 +461,7 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(button, Key.Tab); var item = target.ContainerFromIndex(0); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); } [Fact] @@ -513,17 +513,17 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(button, Key.Tab); var item = target.ContainerFromIndex(1); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); RaiseKeyEvent(item, Key.Tab); - Assert.Same(button, FocusManager.Instance.Current); + Assert.Same(button, root.FocusManager.GetFocusedElement()); target.Selection.AnchorIndex = 2; RaiseKeyEvent(button, Key.Tab); item = target.ContainerFromIndex(2); - Assert.Same(item, FocusManager.Instance.Current); + Assert.Same(item, root.FocusManager.GetFocusedElement()); } private static IControlTemplate TabControlTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 51300f343a..604ff1c715 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -969,7 +969,6 @@ namespace Avalonia.Controls.UnitTests public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { using var app = Start(); - var focus = FocusManager.Instance!; var navigation = AvaloniaLocator.Current.GetRequiredService(); var data = CreateTestTreeData(); @@ -984,6 +983,7 @@ namespace Avalonia.Controls.UnitTests { Children = { target, button }, }); + var focus = root.FocusManager; root.LayoutManager.ExecuteInitialLayoutPass(); ExpandAll(target); @@ -994,20 +994,19 @@ namespace Avalonia.Controls.UnitTests target.SelectedItem = item; node.Focus(); - Assert.Same(node, focus.Current); + Assert.Same(node, focus.GetFocusedElement()); - navigation.Move(focus.Current!, NavigationDirection.Next); - Assert.Same(button, focus.Current); + navigation.Move(focus.GetFocusedElement()!, NavigationDirection.Next); + Assert.Same(button, focus.GetFocusedElement()); - navigation.Move(focus.Current!, NavigationDirection.Next); - Assert.Same(node, focus.Current); + navigation.Move(focus.GetFocusedElement()!, NavigationDirection.Next); + Assert.Same(node, focus.GetFocusedElement()); } [Fact] public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree() { using var app = Start(); - var focus = FocusManager.Instance!; var data = CreateTestTreeData(); var selectedNode = new Node { Value = "Out of Tree Selected Item" }; @@ -1025,6 +1024,7 @@ namespace Avalonia.Controls.UnitTests { Children = { target, button }, }); + var focus = root.FocusManager; root.LayoutManager.ExecuteInitialLayoutPass(); ExpandAll(target); @@ -1035,7 +1035,7 @@ namespace Avalonia.Controls.UnitTests target.SelectedItem = selectedNode; node.Focus(); - Assert.Same(node, focus.Current); + Assert.Same(node, focus.GetFocusedElement()); } [Fact] diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index d6318bba0b..b3a6b52d68 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -561,7 +561,7 @@ namespace Avalonia.LeakTests var window = new Window { Focusable = true }; window.Show(); - Assert.Same(window, FocusManager.Instance.Current); + Assert.Same(window, window.FocusManager.GetFocusedElement()); // Context menu in resources means the baseline may not be 0. var initialMenuCount = 0; @@ -608,7 +608,7 @@ namespace Avalonia.LeakTests var window = new Window { Focusable = true }; window.Show(); - Assert.Same(window, FocusManager.Instance.Current); + Assert.Same(window, window.FocusManager.GetFocusedElement()); // Context menu in resources means the baseline may not be 0. var initialMenuCount = 0; diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index c17eeda3e1..8dabfe2197 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -54,6 +54,7 @@ namespace Avalonia.UnitTests public IAccessKeyHandler AccessKeyHandler => null; public IKeyboardNavigationHandler KeyboardNavigationHandler => null; + public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); public IInputElement PointerOverElement { get; set; } From 6294093a18bde8ba5e5e0f6718b7c988868be7b9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 16 May 2023 21:45:06 -0400 Subject: [PATCH 5/8] Use [PrivateApi] on members --- src/Avalonia.Base/Input/FocusManager.cs | 2 +- src/Avalonia.Base/Input/KeyboardDevice.cs | 2 +- src/Avalonia.Base/Input/MouseDevice.cs | 2 +- src/Avalonia.Base/Input/PenDevice.cs | 2 ++ src/Avalonia.Base/Input/TouchDevice.cs | 2 ++ 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 4c464a44ae..da84a3272f 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input /// /// Manages focus for the application. /// - [Unstable] + [PrivateApi] public class FocusManager : IFocusManager { /// diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index f6d2a2195a..a81bc6b2e0 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -7,7 +7,7 @@ using Avalonia.Metadata; namespace Avalonia.Input { - [Unstable] + [PrivateApi] public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { private IInputElement? _focusedElement; diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 4aeffcdd72..f3e77433a9 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -12,7 +12,7 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - [Unstable] + [PrivateApi] public class MouseDevice : IMouseDevice, IDisposable { private int _clickCount; diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index b3cd39212b..832f32fb03 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Avalonia.Input.Raw; +using Avalonia.Metadata; using Avalonia.Platform; #pragma warning disable CS0618 @@ -11,6 +12,7 @@ namespace Avalonia.Input /// /// Represents a pen/stylus device. /// + [PrivateApi] public class PenDevice : IPenDevice, IDisposable { private readonly Dictionary _pointers = new(); diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index bab1b9f784..78b570da14 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Avalonia.Input.Raw; +using Avalonia.Metadata; using Avalonia.Platform; #pragma warning disable CS0618 @@ -14,6 +15,7 @@ namespace Avalonia.Input /// /// This class is supposed to be used on per-toplevel basis, don't use a shared one /// + [PrivateApi] public class TouchDevice : IPointerDevice, IDisposable { private readonly Dictionary _pointers = new Dictionary(); From cd9a19387aef7395794d71170071926347dc0be4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 16 May 2023 21:52:08 -0400 Subject: [PATCH 6/8] Keep FocusManager.OnPreviewPointerPressed static as it was before --- src/Avalonia.Base/Input/FocusManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index da84a3272f..2127e19627 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -23,7 +23,7 @@ namespace Avalonia.Input /// /// Initializes a new instance of the class. /// - internal FocusManager() + static FocusManager() { InputElement.PointerPressedEvent.AddClassHandler( typeof(IInputElement), @@ -237,7 +237,7 @@ namespace Avalonia.Input /// /// The event sender. /// The event args. - private void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) + private static void OnPreviewPointerPressed(object? sender, RoutedEventArgs e) { if (sender is null) return; @@ -253,7 +253,7 @@ namespace Avalonia.Input { if (element is IInputElement inputElement && CanFocus(inputElement)) { - Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); + inputElement.Focus(NavigationMethod.Pointer, ev.KeyModifiers); break; } From 44e3a532b600318db6b7b69e0c3a377cb7e45b88 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 16 May 2023 21:56:11 -0400 Subject: [PATCH 7/8] Fix comment --- src/Avalonia.Base/Input/FocusManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 2127e19627..214ba21c5c 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -197,7 +197,7 @@ 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?)((IInputRoot?)(element as Visual)?.VisualRoot)?.FocusManager - // In our unit tests some elements might not have a root. Remove when + // In our unit tests some elements might not have a root. Remove when we migrate to headless tests. ?? (FocusManager?)AvaloniaLocator.Current.GetService(); } From d3fbc105af38328ff25a30a442b57038e13acf11 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 16 May 2023 23:26:11 -0400 Subject: [PATCH 8/8] Fix "GetFocusManager" with some unit tests --- src/Avalonia.Base/Input/FocusManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 214ba21c5c..c3316eb278 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -196,7 +196,7 @@ 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?)((IInputRoot?)(element as Visual)?.VisualRoot)?.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. ?? (FocusManager?)AvaloniaLocator.Current.GetService(); }