From 0e7b8f6f4589195ae35e933c64f89f9478832fb1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jul 2023 23:17:48 +0200 Subject: [PATCH 01/33] Allow embedded root automation peers. --- .../Avalonia.Win32/Automation/RootAutomationNode.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 739f0ac251..0a73c8bc7b 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32.Automation return null; var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); - var found = InvokeSync(() => Peer.GetPeerFromPoint(p)); + var found = InvokeSync(() => GetPeerFromPoint(p)); var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } @@ -101,5 +101,15 @@ namespace Avalonia.Win32.Automation return result; } } + + private AutomationPeer? GetPeerFromPoint(Point p) + { + var hit = Peer.GetPeerFromPoint(p); + + while (hit != Peer && hit?.GetProvider() is { } embeddedRoot) + hit = embeddedRoot.GetPeerFromPoint(p); + + return hit; + } } } From 2c91d7f89323c736dd4afeaf6e13ad5efc247b06 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 12:45:58 +0200 Subject: [PATCH 02/33] Handle null from RootProvider_GetWindow. --- native/Avalonia.Native/src/OSX/automation.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index d0c8d7a9db..4b325a092d 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -73,6 +73,13 @@ private: if (peer->IsRootProvider()) { auto window = peer->RootProvider_GetWindow(); + + if (window == nullptr) + { + NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl."); + return nil; + } + auto holder = dynamic_cast(window); auto view = holder->GetNSView(); return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; From 8d5ef676f5098257889925419501799596aa1bf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 12:46:25 +0200 Subject: [PATCH 03/33] Allow non-ControlAutomationPeer IRootProviders. --- src/Avalonia.Native/AvnAutomationPeer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 6c4e96b31b..038b62a7f6 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -74,10 +74,11 @@ namespace Avalonia.Native public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); - public IAvnWindowBase RootProvider_GetWindow() + public IAvnWindowBase? RootProvider_GetWindow() { - var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; - return ((WindowBaseImpl)window.PlatformImpl!).Native; + if (((IRootProvider)_inner).PlatformImpl is WindowBaseImpl impl) + return impl.Native; + return null; } public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); From da993425e7a18c545d591259eb1a19a8bc27f5a9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 13:46:35 +0200 Subject: [PATCH 04/33] Fix determining if a peer supports a provider. We should use `GetProvider` instead of a plain cast as a peer may decide to dynamically support a provider, or delegate its implementation. --- src/Avalonia.Native/AvnAutomationPeer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 038b62a7f6..6b1b4a7a03 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,7 +72,7 @@ namespace Avalonia.Native Node = node; } - public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + public int IsRootProvider() => (_inner.GetProvider() is not null).AsComBool(); public IAvnWindowBase? RootProvider_GetWindow() { @@ -104,7 +104,7 @@ namespace Avalonia.Native return Wrap(result); } - public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); + public int IsExpandCollapseProvider() => (_inner.GetProvider() is not null).AsComBool(); public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch { @@ -128,14 +128,14 @@ namespace Avalonia.Native public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); - public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); + public int IsSelectionItemProvider() => (_inner.GetProvider() is not null).AsComBool(); public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); - public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); + public int IsToggleProvider() => (_inner.GetProvider() is not null).AsComBool(); public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); - public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); + public int IsValueProvider() => (_inner.GetProvider() is not null).AsComBool(); public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); From c9dfda42ebd524e19205d756ba6c625496e9361d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 17:31:43 +0200 Subject: [PATCH 05/33] More fixing of provider resolution. The previous commit missed some providers, and we also need to call `GetProvider` when calling members on the provider. --- src/Avalonia.Native/AvnAutomationPeer.cs | 88 +++++++++++++----------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 6b1b4a7a03..8069ac76bf 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -48,6 +48,13 @@ namespace Avalonia.Native public void SetFocus() => _inner.SetFocus(); public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + public IAvnAutomationPeer? RootPeer { get @@ -65,27 +72,22 @@ namespace Avalonia.Native } } - public void SetNode(IAvnAutomationNode node) - { - if (Node is not null) - throw new InvalidOperationException("The AvnAutomationPeer already has a node."); - Node = node; - } - - public int IsRootProvider() => (_inner.GetProvider() is not null).AsComBool(); + private IRootProvider RootProvider => GetProvider(); + private IExpandCollapseProvider ExpandCollapseProvider => GetProvider(); + private IInvokeProvider InvokeProvider => GetProvider(); + private IRangeValueProvider RangeValueProvider => GetProvider(); + private ISelectionItemProvider SelectionItemProvider => GetProvider(); + private IToggleProvider ToggleProvider => GetProvider(); + private IValueProvider ValueProvider => GetProvider(); - public IAvnWindowBase? RootProvider_GetWindow() - { - if (((IRootProvider)_inner).PlatformImpl is WindowBaseImpl impl) - return impl.Native; - return null; - } - - public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); + public int IsRootProvider() => IsProvider(); + + public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native; + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus()); public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) { - var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); if (result is null) return null; @@ -104,46 +106,54 @@ namespace Avalonia.Native return Wrap(result); } - public int IsExpandCollapseProvider() => (_inner.GetProvider() is not null).AsComBool(); + public int IsExpandCollapseProvider() => IsProvider(); - public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch { ExpandCollapseState.Expanded => 1, ExpandCollapseState.PartiallyExpanded => 1, _ => 0, }; - public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); - public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); - public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand(); + public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse(); - public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); - public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + public int IsInvokeProvider() => IsProvider(); + public void InvokeProvider_Invoke() => InvokeProvider.Invoke(); - public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); - public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; - public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; - public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; - public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; - public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; - public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + public int IsRangeValueProvider() => IsProvider(); + public double RangeValueProvider_GetValue() => RangeValueProvider.Value; + public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum; + public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum; + public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange; + public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange; + public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value); - public int IsSelectionItemProvider() => (_inner.GetProvider() is not null).AsComBool(); - public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); + public int IsSelectionItemProvider() => IsProvider(); + public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool(); - public int IsToggleProvider() => (_inner.GetProvider() is not null).AsComBool(); - public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; - public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + public int IsToggleProvider() => IsProvider(); + public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState; + public void ToggleProvider_Toggle() => ToggleProvider.Toggle(); - public int IsValueProvider() => (_inner.GetProvider() is not null).AsComBool(); - public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); - public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); + public int IsValueProvider() => IsProvider(); + public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value); [return: NotNullIfNotNull("peer")] public static AvnAutomationPeer? Wrap(AutomationPeer? peer) { return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); } + + private T GetProvider() + { + return _inner.GetProvider() ?? throw new InvalidOperationException( + $"The peer {_inner} does not implement {typeof(T)}."); + } + + private int IsProvider() => (_inner.GetProvider() is not null).AsComBool(); } internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray From 6b68a8e5c6480edb095c33d0fd744e01ef2c4637 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 19:09:33 +0200 Subject: [PATCH 06/33] More fixing of provider resolution. Found a few more places that were doing casts instead of calling `GetProvider()`. --- .../Automation/Peers/ItemsControlAutomationPeer.cs | 2 +- .../Automation/Peers/ListItemAutomationPeer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index db16bf0a53..64727c43c5 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers if (!_searchedForScrollable) { if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) - _scroller = GetOrCreate(scrollable) as IScrollProvider; + _scroller = GetOrCreate(scrollable).GetProvider(); _searchedForScrollable = true; } diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index aea91b5e26..dab8c45567 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is Control parent) { var parentPeer = GetOrCreate(parent); - return parentPeer as ISelectionProvider; + return parentPeer.GetProvider(); } return null; From 8ffbb2a214150830c278aff0c60d45d196594548 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 19:52:36 +0200 Subject: [PATCH 07/33] More fixing of provider resolution. Arrgh! Forgot to save the file. --- src/Avalonia.Native/AvnAutomationPeer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 8069ac76bf..76cae2684f 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -62,7 +62,7 @@ namespace Avalonia.Native var peer = _inner; var parent = peer.GetParent(); - while (peer is not IRootProvider && parent is not null) + while (peer.GetProvider() is null && parent is not null) { peer = parent; parent = peer.GetParent(); From 02789d2d48e442997d70a5eba024d02094fcb554 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 00:19:37 +0200 Subject: [PATCH 08/33] Revert "Allow embedded root automation peers." This reverts commit 0e7b8f6f4589195ae35e933c64f89f9478832fb1. The code is in the wrong place. --- .../Avalonia.Win32/Automation/RootAutomationNode.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 0a73c8bc7b..739f0ac251 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32.Automation return null; var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); - var found = InvokeSync(() => GetPeerFromPoint(p)); + var found = InvokeSync(() => Peer.GetPeerFromPoint(p)); var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } @@ -101,15 +101,5 @@ namespace Avalonia.Win32.Automation return result; } } - - private AutomationPeer? GetPeerFromPoint(Point p) - { - var hit = Peer.GetPeerFromPoint(p); - - while (hit != Peer && hit?.GetProvider() is { } embeddedRoot) - hit = embeddedRoot.GetPeerFromPoint(p); - - return hit; - } } } From 0c7c315a10c114427e7a6514bf37949aeaf34122 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 00:23:19 +0200 Subject: [PATCH 09/33] Hit-test embedded root automation peers. --- .../Peers/WindowBaseAutomationPeer.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index 9ec65592fa..3786ba32c7 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Globalization; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Input; @@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers public AutomationPeer? GetPeerFromPoint(Point p) { var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); - return hit is object ? GetOrCreate(hit) : null; + + if (hit is null) + return null; + + var peer = GetOrCreate(hit); + + while (peer != this && peer.GetProvider() is { } embedded) + { + var embeddedHit = embedded.GetPeerFromPoint(p); + if (embeddedHit is null) + break; + peer = embeddedHit; + } + + return peer; } protected void StartTrackingFocus() From b7fcb141420001364d7a06efced40dfe2d357e93 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 12:53:44 +0200 Subject: [PATCH 10/33] Added IEmbeddedRootProvider. For some reason, on win32 embedded `IRawElementProviderFragmentRoot`s just don't show up, so we need an interface to distinguish between "actual" root peers and "embedded" root peers. Ideally `IRootProvider` and `IEmbeddedRootProvider` would share a common interface but that would be a breaking change. --- .../Peers/WindowBaseAutomationPeer.cs | 2 +- .../Provider/IEmbeddedRootProvider.cs | 33 +++++++++ .../Automation/Provider/IRootProvider.cs | 25 +++++++ .../Automation/AutomationNode.cs | 69 +++++++++++-------- .../Automation/RootAutomationNode.cs | 48 ++----------- 5 files changed, 108 insertions(+), 69 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index 3786ba32c7..ceb695422d 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -39,7 +39,7 @@ namespace Avalonia.Automation.Peers var peer = GetOrCreate(hit); - while (peer != this && peer.GetProvider() is { } embedded) + while (peer != this && peer.GetProvider() is { } embedded) { var embeddedHit = embedded.GetPeerFromPoint(p); if (embeddedHit is null) diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs new file mode 100644 index 0000000000..1b1caef182 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposure methods and properties to support UI Automation client access to the root of an + /// automation tree hosted by another UI framework. + /// + /// + /// This interface is implemented by the class, and can be used + /// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's + /// automation support. + /// + public interface IEmbeddedRootProvider + { + /// + /// Gets the currently focused element. + /// + AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. + AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// + event EventHandler? FocusChanged; + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index ce38059559..6a266da5c5 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -4,11 +4,36 @@ using Avalonia.Platform; namespace Avalonia.Automation.Provider { + /// + /// Exposes methods and properties to support UI Automation client access to the root of an + /// automation tree. + /// + /// + /// This interface is implemented by the class, and should only + /// be implemented on true root elements, such as Windows. To embed an automation tree, use + /// instead. + /// public interface IRootProvider { + /// + /// Gets the platform implementation of the TopLevel for the element. + /// ITopLevelImpl? PlatformImpl { get; } + + /// + /// Gets the currently focused element. + /// AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// event EventHandler? FocusChanged; } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 3eeedc4b5d..e835c6a57a 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); - peer.ChildrenChanged += Peer_ChildrenChanged; - peer.PropertyChanged += Peer_PropertyChanged; - } - - private void Peer_ChildrenChanged(object? sender, EventArgs e) - { - ChildrenChanged(); - } + peer.ChildrenChanged += OnPeerChildrenChanged; + peer.PropertyChanged += OnPeerPropertyChanged; - private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) - { - if (s_propertyMap.TryGetValue(e.Property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( - this, - (int)id, - e.OldValue as IConvertible, - e.NewValue as IConvertible); - } + if (Peer.GetProvider() is { } embeddedRoot) + embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged; } public AutomationPeer Peer { get; protected set; } @@ -95,15 +81,6 @@ namespace Avalonia.Win32.Automation public virtual IRawElementProviderSimple? HostRawElementProvider => null; public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; - public void ChildrenChanged() - { - UiaCoreProviderApi.UiaRaiseStructureChangedEvent( - this, - StructureChangeType.ChildrenInvalidated, - null, - 0); - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -250,6 +227,21 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } + protected void RaiseChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + null, + 0); + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } private AutomationNode? GetRoot() { @@ -267,6 +259,29 @@ namespace Avalonia.Win32.Automation return peer is object ? GetOrCreate(peer) : null; } + private void OnPeerChildrenChanged(object? sender, EventArgs e) + { + RaiseChildrenChanged(); + } + + private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); + } + } + + private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e) + { + if (Peer.GetProvider() is { } embeddedRoot) + RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus())); + } + private static AutomationNode Create(AutomationPeer peer) { return peer.GetProvider() is object ? diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 739f0ac251..7334186c80 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation; namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] - internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot, - IRawElementProviderAdviseEvents + internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot { - private int _raiseFocusChanged; - public RootAutomationNode(AutomationPeer peer) : base(peer) { Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); - Peer.FocusChanged += FocusChanged; + Peer.FocusChanged += OnRootFocusChanged; } public override IRawElementProviderFragmentRoot? FragmentRoot => this; @@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } - - public void FocusChanged(object? sender, EventArgs e) - { - RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); - } - public Rect ToScreen(Rect rect) { if (WindowImpl is null) @@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation return result; } } + + private void OnRootFocusChanged(object? sender, EventArgs e) + { + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); + } } } From 4fc2a0dfe799c7d944ad7bd114185cca362cd7a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 17:04:45 +0200 Subject: [PATCH 11/33] Add IEmbeddedRootProvider to AvnAutomationPeer. --- src/Avalonia.Native/AvnAutomationPeer.cs | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 76cae2684f..5933dc6c92 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,10 +72,11 @@ namespace Avalonia.Native } } - private IRootProvider RootProvider => GetProvider(); + private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider(); private IExpandCollapseProvider ExpandCollapseProvider => GetProvider(); private IInvokeProvider InvokeProvider => GetProvider(); private IRangeValueProvider RangeValueProvider => GetProvider(); + private IRootProvider RootProvider => GetProvider(); private ISelectionItemProvider SelectionItemProvider => GetProvider(); private IToggleProvider ToggleProvider => GetProvider(); private IValueProvider ValueProvider => GetProvider(); @@ -106,6 +107,32 @@ namespace Avalonia.Native return Wrap(result); } + + public int IsEmbeddedRootProvider() => IsProvider(); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus()); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is not null) + result = parent; + else + break; + } + + return Wrap(result); + } + public int IsExpandCollapseProvider() => IsProvider(); public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch From c1645ca31ae595a1bed83413aba59020d011d24c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 19:37:37 +0200 Subject: [PATCH 12/33] Allow an AutomationPeer to override its visual root. This is needed for example when a UI framework hosts a peer in the automation tree of a main window whose control is actually hosted in a popup. It allows the bounding rectangle to be calculated correctly in that case. s --- .../Automation/Peers/AutomationPeer.cs | 23 ++++++++++++++++++- .../Automation/AutomationNode.cs | 16 +++---------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 3d3fe35d29..a264909ba6 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -115,9 +115,14 @@ namespace Avalonia.Automation.Peers /// /// Gets the that is the parent of this . /// - /// public AutomationPeer? GetParent() => GetParentCore(); + /// + /// Gets the that is the root of this 's + /// visual tree. + /// + public AutomationPeer? GetVisualRoot() => GetVisualRootCore(); + /// /// Gets a value that indicates whether the element that is associated with this automation /// peer currently has keyboard focus. @@ -247,6 +252,22 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } + protected virtual AutomationPeer? GetVisualRootCore() + { + var parent = GetParent(); + + while (parent != null) + { + var nextParent = parent.GetParent(); + if (nextParent == null) + return parent; + parent = nextParent; + } + + return null; + } + + protected virtual bool IsContentElementOverrideCore() { return IsControlElement() && IsContentElementCore(); diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index e835c6a57a..569f7da738 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -75,7 +75,7 @@ namespace Avalonia.Win32.Automation public virtual IRawElementProviderFragmentRoot? FragmentRoot { - get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + get => InvokeSync(() => GetRoot()); } public virtual IRawElementProviderSimple? HostRawElementProvider => null; @@ -243,20 +243,10 @@ namespace Avalonia.Win32.Automation (int)UiaEventId.AutomationFocusChanged); } - private AutomationNode? GetRoot() + private RootAutomationNode? GetRoot() { Dispatcher.UIThread.VerifyAccess(); - - var peer = Peer; - var parent = peer.GetParent(); - - while (peer.GetProvider() is null && parent is object) - { - peer = parent; - parent = peer.GetParent(); - } - - return peer is object ? GetOrCreate(peer) : null; + return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode; } private void OnPeerChildrenChanged(object? sender, EventArgs e) From 651f558b67578971a981cdcba479ecb5783cd74b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 19:58:16 +0200 Subject: [PATCH 13/33] Added new members to IAvnAutomationPeer. --- src/Avalonia.Native/AvnAutomationPeer.cs | 1 + src/Avalonia.Native/avn.idl | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 5933dc6c92..d2d93b69a9 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -39,6 +39,7 @@ namespace Avalonia.Native public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); public IAvnString Name => _inner.GetName().ToAvnString(); public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot()); public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); public int IsContentElement() => _inner.IsContentElement().AsComBool(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index bc372bbcb5..0911e5ffff 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -921,6 +921,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetLabeledBy(); IAvnString* GetName(); IAvnAutomationPeer* GetParent(); + IAvnAutomationPeer* GetVisualRoot(); bool HasKeyboardFocus(); bool IsContentElement(); bool IsControlElement(); @@ -935,7 +936,11 @@ interface IAvnAutomationPeer : IUnknown IAvnWindowBase* RootProvider_GetWindow(); IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); - + + bool IsEmbeddedRootProvider(); + IAvnAutomationPeer* EmbeddedRootProvider_GetFocus(); + IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point); + bool IsExpandCollapseProvider(); bool ExpandCollapseProvider_GetIsExpanded(); bool ExpandCollapseProvider_GetShowsMenu(); From 1b2d3948d07da0c6569b90b7634d58fcba2d3bd2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 00:41:16 +0200 Subject: [PATCH 14/33] Fix accessibilityWindow. Seems it was broken before and always would have returned null. --- native/Avalonia.Native/src/OSX/automation.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 4b325a092d..9fe0ff3c60 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -291,8 +291,8 @@ private: - (id)accessibilityWindow { - id topLevel = [self accessibilityTopLevelUIElement]; - return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; + auto rootPeer = _peer->GetVisualRoot(); + return [AvnAccessibilityElement acquire:rootPeer]; } - (BOOL)isAccessibilityExpanded From 608c251fb27bd37b5ceb1224b62b53954cce67d7 Mon Sep 17 00:00:00 2001 From: Color_yr <402067010@qq.com> Date: Fri, 28 Jul 2023 06:32:34 +0000 Subject: [PATCH 15/33] Make the animation display complete --- src/Avalonia.Base/Animation/CrossFade.cs | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 640d6456a3..d598db870f 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -35,6 +35,18 @@ namespace Avalonia.Animation { Children = { + new KeyFrame() + { + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 1d + } + }, + Cue = new Cue(0d) + }, new KeyFrame() { Setters = @@ -54,6 +66,18 @@ namespace Avalonia.Animation { Children = { + new KeyFrame() + { + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, new KeyFrame() { Setters = @@ -117,11 +141,13 @@ namespace Avalonia.Animation if (from != null) { + from.Opacity = 0f; tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken)); } if (to != null) { + to.Opacity = 1f; to.IsVisible = true; tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken)); } From 90e3760c0f8e1c346a4b9d2d0f5a17f0fadbef9d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 09:19:42 +0200 Subject: [PATCH 16/33] Use interface instead of concrete class. --- src/Avalonia.Native/AvnAutomationPeer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index d2d93b69a9..af4958b02f 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -22,8 +22,8 @@ namespace Avalonia.Native { _inner = inner; _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); - if (inner is WindowBaseAutomationPeer window) - window.FocusChanged += (_, _) => Node?.FocusChanged(); + if (inner is IRootProvider root) + root.FocusChanged += (_, _) => Node?.FocusChanged(); } ~AvnAutomationPeer() => Node?.Dispose(); From ae82bc1b8f39362dbfec7412c2a5381a92998359 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 11:19:57 +0200 Subject: [PATCH 17/33] Fix GetVisualRootCore. Needs to check for `IRootProvider`. Fixes integration tests on Windows. --- .../Automation/Peers/AutomationPeer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index a264909ba6..fb7cdd87ed 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Automation.Provider; namespace Avalonia.Automation.Peers { @@ -254,17 +255,16 @@ namespace Avalonia.Automation.Peers protected virtual AutomationPeer? GetVisualRootCore() { - var parent = GetParent(); + var peer = this; + var parent = peer.GetParent(); - while (parent != null) + while (peer.GetProvider() is null && parent is not null) { - var nextParent = parent.GetParent(); - if (nextParent == null) - return parent; - parent = nextParent; + peer = parent; + parent = peer.GetParent(); } - return null; + return peer; } From 6b3db2a3f56231075042983d01272605974a6dee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 13:15:30 +0200 Subject: [PATCH 18/33] Shortcut finding the visual root for controls. --- .../Automation/Peers/ControlAutomationPeer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index c19d887230..69f267a605 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers return _parent; } + protected override AutomationPeer? GetVisualRootCore() + { + if (Owner.GetVisualRoot() is Control c) + return CreatePeerForElement(c); + return null; + } + /// /// Invalidates the peer's children and causes a re-read from . /// From 55c9da56ebc569e061b7a34b5ae02ea748978ebb Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 28 Jul 2023 16:10:13 +0200 Subject: [PATCH 19/33] feat(NumericUpDown): TextAlignment --- .../NumericUpDown/NumericUpDown.cs | 15 +++++++++++++++ .../Controls/NumericUpDown.xaml | 1 + .../Controls/NumericUpDown.xaml | 1 + 3 files changed, 17 insertions(+) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 30f9d8f380..84772e7789 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -126,6 +126,12 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property + /// + public static readonly StyledProperty TextAlignmentProperty = + TextBox.TextAlignmentProperty.AddOwner(); + private IDisposable? _textBoxTextChangedSubscription; private bool _internalValueSet; @@ -299,6 +305,15 @@ namespace Avalonia.Controls set => SetValue(VerticalContentAlignmentProperty, value); } + /// + /// Gets or sets the of the + /// + public Media.TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + /// /// Initializes new instance of class. /// diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 1f84ee664c..a470eb1d3b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -57,6 +57,7 @@ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Text="{TemplateBinding Text}" + TextAlignment="{TemplateBinding TextAlignment}" AcceptsReturn="False" TextWrapping="NoWrap" /> diff --git a/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml index 4ce6a20dc6..ff4be12fda 100644 --- a/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml @@ -31,6 +31,7 @@ DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" IsReadOnly="{TemplateBinding IsReadOnly}" Text="{TemplateBinding Text}" + TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="NoWrap" Watermark="{TemplateBinding Watermark}" /> From e1ac9be7f1ae659a44c449e692946977c330af29 Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Sat, 29 Jul 2023 15:14:50 -0400 Subject: [PATCH 20/33] Catch errors so that for loop can register all names --- .../DBusIme/DBusTextInputMethodBase.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 9ce6604594..b897d52204 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -62,9 +62,15 @@ namespace Avalonia.FreeDesktop.DBusIme foreach (var name in _knownNames) { var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); - _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); - var nameOwner = await dbus.GetNameOwnerAsync(name); - OnNameChange(null, (name, null, nameOwner)); + try + { + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } + catch (DBusException) + { + } } } From 9ba9414c223216e0d36ceef25ae34c12e252bb81 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 27 Jul 2023 03:24:57 +0300 Subject: [PATCH 21/33] unit test for issue added --- .../HotKeyedControlsTests.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs new file mode 100644 index 0000000000..55a3f0d5d4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Windows.Input; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + internal class HotKeyedTextBox : TextBox, ICommandSource + { + private class DelegateCommand : ICommand + { + private readonly Action _action; + public DelegateCommand(Action action) => _action = action; + public event EventHandler CanExecuteChanged { add { } remove { } } + public bool CanExecute(object parameter) => true; + public void Execute(object parameter) => _action(); + } + + public static readonly StyledProperty HotKeyProperty = + HotKeyManager.HotKeyProperty.AddOwner(); + + private KeyGesture _hotkey; + + public KeyGesture HotKey + { + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_hotkey != null) + { + this.SetValue(HotKeyProperty, _hotkey); + } + + base.OnAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (this.HotKey != null) + { + _hotkey = this.HotKey; + this.SetValue(HotKeyProperty, null); + } + + base.OnDetachedFromLogicalTree(e); + } + + public void CanExecuteChanged(object sender, EventArgs e) + { + } + + protected override Type StyleKeyOverride => typeof(TextBox); + + public ICommand Command => _command; + + public object CommandParameter => null; + + private readonly DelegateCommand _command; + + public HotKeyedTextBox() + { + _command = new DelegateCommand(() => Focus()); + } + } + + public class HotKeyedControlsTests + { + private static Window PreparedWindow(object content = null) + { + var platform = AvaloniaLocator.Current.GetRequiredService(); + var windowImpl = Mock.Get(platform.CreateWindow()); + windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); + var w = new Window(windowImpl.Object) { Content = content }; + w.ApplyTemplate(); + return w; + } + + private static IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start( + TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform( + null, + window => MockWindowingPlatform.CreatePopupMock(window).Object), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + [Fact] + public void HotKeyedTextBox_Focus_Performed_On_Hotkey() + { + using var _ = CreateServicesWithFocus(); + + var keyboardDevice = new KeyboardDevice(); + var hotKeyedTextBox = new HotKeyedTextBox { HotKey = new KeyGesture(Key.F, KeyModifiers.Control) }; + var root = PreparedWindow(); + root.Content = hotKeyedTextBox; + root.Show(); + + Assert.False(hotKeyedTextBox.IsFocused); + + keyboardDevice.ProcessRawEvent( + new RawKeyEventArgs( + keyboardDevice, + 0, + root, + RawKeyEventType.KeyDown, + Key.F, + RawInputModifiers.Control)); + + Assert.True(hotKeyedTextBox.IsFocused); + } + } +} From a5c7b1d35842b40592bc04e5f0565243f1ce6e05 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 27 Jul 2023 03:29:27 +0300 Subject: [PATCH 22/33] issue fix --- src/Avalonia.Controls/HotkeyManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index de753f0bd0..6ad4a8cc76 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -149,10 +149,10 @@ namespace Avalonia.Controls return; var control = args.Sender as Control; - if (control is not IClickableControl) + if (control is not IClickableControl and not ICommandSource) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl nor ICommandSource and does not support binding a HotKey ({args.NewValue})."); return; } From d0b1389ee888d3ae2bac4a12dc04386d703e9062 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 2 Aug 2023 12:51:48 +0600 Subject: [PATCH 23/33] override_redirect and Handle fixes for X11Window --- src/Avalonia.X11/X11Window.cs | 36 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index be2d754819..a2019c276b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -17,7 +17,6 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Storage; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; @@ -36,6 +35,7 @@ namespace Avalonia.X11 { private readonly AvaloniaX11Platform _platform; private readonly bool _popup; + private readonly bool _overrideRedirect; private readonly X11Info _x11; private XConfigureEvent? _configure; private PixelPoint? _configurePoint; @@ -72,10 +72,11 @@ namespace Avalonia.X11 WaitPaint } - public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent) + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool overrideRedirect = false) { _platform = platform; _popup = popupParent != null; + _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; _mouse = new MouseDevice(); _touch = new TouchDevice(); @@ -92,7 +93,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (_popup) + if (_overrideRedirect) { attr.override_redirect = 1; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -155,7 +156,7 @@ namespace Avalonia.X11 else _renderHandle = _handle; - Handle = new SurfacePlatformHandle(this); + Handle = new PlatformHandle(_handle, "XID"); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask @@ -165,15 +166,18 @@ namespace Avalonia.X11 ignoredMask |= platform.XI2.AddWindow(_handle, this); var mask = new IntPtr(0xffffff ^ (int)ignoredMask); XSelectInput(_x11.Display, _handle, mask); - var protocols = new[] + if (!_overrideRedirect) { - _x11.Atoms.WM_DELETE_WINDOW - }; - XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); - XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, - 32, PropertyMode.Replace, new[] {_x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL}, 1); + var protocols = new[] + { + _x11.Atoms.WM_DELETE_WINDOW + }; + XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, + 32, PropertyMode.Replace, new[] { _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL }, 1); - SetWmClass(_platform.Options.WmClass); + SetWmClass(_platform.Options.WmClass); + } var surfaces = new List { @@ -187,7 +191,7 @@ namespace Avalonia.X11 if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); - surfaces.Add(Handle); + surfaces.Add(new SurfacePlatformHandle(this)); Surfaces = surfaces.ToArray(); UpdateMotifHints(); @@ -257,6 +261,8 @@ namespace Avalonia.X11 private void UpdateMotifHints() { + if(_overrideRedirect) + return; var functions = MotifFunctions.Move | MotifFunctions.Close | MotifFunctions.Resize | MotifFunctions.Minimize | MotifFunctions.Maximize; var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | @@ -286,6 +292,8 @@ namespace Avalonia.X11 private void UpdateSizeHints(PixelSize? preResize) { + if(_overrideRedirect) + return; var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; @@ -507,7 +515,7 @@ namespace Avalonia.X11 } UpdateImePosition(); - if (changedSize && !updatedSizeViaScaling && !_popup) + if (changedSize && !updatedSizeViaScaling && !_overrideRedirect) Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.AsyncRenderTargetResize); @@ -984,7 +992,7 @@ namespace Avalonia.X11 XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize); XFlush(_x11.Display); - if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) + if (force || !_wasMappedAtLeastOnce || (_overrideRedirect && needImmediatePopupResize)) { _realSize = pixelSize; Resized?.Invoke(ClientSize, reason); From ec21caf7fe0419b2284e34d63cd13da6de9460dc Mon Sep 17 00:00:00 2001 From: 3dfxuser <3dfxuser@gmail.com> Date: Wed, 2 Aug 2023 11:43:53 +0300 Subject: [PATCH 24/33] Add null check for TextInputMethodClient in OnSelectionChanged() method --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 7d5130cf5d..f708d6936c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -113,6 +113,11 @@ namespace Avalonia.Android private void OnSelectionChanged() { + if (Client is null) + { + return; + } + var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); From f253b614b3dcf3c8acedf0614cbbfd6b6287c105 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 1 Aug 2023 18:12:53 +0200 Subject: [PATCH 25/33] Generate pdb for ref assemblies --- nukebuild/Build.cs | 5 +- nukebuild/RefAssemblyGenerator.cs | 109 ++++++++++++++++++------------ 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bbfc28aa9f..f8fbf64e83 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -279,8 +279,9 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); - RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + - Parameters.Version + ".nupkg"); + RefAssemblyGenerator.GenerateRefAsmsInPackage( + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg", + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg"); }); Target ValidateApiDiff => _ => _ diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 54e428c442..e93070e2f0 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -1,8 +1,10 @@ +#nullable enable + +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using ILRepacking; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,8 +12,8 @@ public class RefAssemblyGenerator { class Resolver : DefaultAssemblyResolver, IAssemblyResolver { - private readonly string _dir; - Dictionary _cache = new(); + readonly string _dir; + readonly Dictionary _cache = new(); public Resolver(string dir) { @@ -31,17 +33,17 @@ public class RefAssemblyGenerator public static void PatchRefAssembly(string file) { - var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk")!; var snk = new byte[reader.Length]; - reader.Read(snk, 0, snk.Length); + reader.ReadExactly(snk, 0, snk.Length); var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters { ReadWrite = true, InMemory = true, ReadSymbols = true, - SymbolReaderProvider = new DefaultSymbolReaderProvider(false), - AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + SymbolReaderProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: true), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)!) }); var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, @@ -58,7 +60,7 @@ public class RefAssemblyGenerator { StrongNameKeyBlob = snk, WriteSymbols = def.MainModule.HasSymbols, - SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + SymbolWriterProvider = new PortablePdbWriterProvider(), DeterministicMvid = def.MainModule.HasSymbols }); } @@ -146,7 +148,7 @@ public class RefAssemblyGenerator m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; } - static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute? unstableAttribute) { if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) return; @@ -172,43 +174,66 @@ public class RefAssemblyGenerator }); } - public static void GenerateRefAsmsInPackage(string packagePath) + public static void GenerateRefAsmsInPackage(string mainPackagePath, string symbolsPackagePath) { - using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), - ZipArchiveMode.Update)) + using var mainArchive = OpenPackage(mainPackagePath); + using var symbolsArchive = OpenPackage(symbolsPackagePath); + + foreach (var entry in mainArchive.Entries + .Where(e => e.FullName.StartsWith("ref/", StringComparison.Ordinal)) + .ToArray()) { - foreach (var entry in archive.Entries.ToList()) - { - if (entry.FullName.StartsWith("ref/")) - entry.Delete(); - } - - foreach (var entry in archive.Entries.ToList()) + entry.Delete(); + } + + foreach (var libEntry in GetLibEntries(mainArchive, ".xml")) + { + var refEntry = mainArchive.CreateEntry("ref/" + libEntry.FullName.Substring(4), CompressionLevel.Optimal); + using var src = libEntry.Open(); + using var dst = refEntry.Open(); + src.CopyTo(dst); + } + + var pdbEntries = GetLibEntries(symbolsArchive, ".pdb").ToDictionary(e => e.FullName); + + var libs = GetLibEntries(mainArchive, ".dll") + .Select(e => (NameParts: e.FullName.Split('/'), Entry: e)) + .Select(e => ( + Tfm: e.NameParts[1], + DllName: e.NameParts[2], + DllEntry: e.Entry, + PdbName: Path.ChangeExtension(e.NameParts[2], ".pdb"), + PdbEntry: pdbEntries.TryGetValue(Path.ChangeExtension(e.Entry.FullName, ".pdb"), out var pdbEntry) ? + pdbEntry : + throw new InvalidOperationException($"Missing symbols for {e.Entry.FullName}"))) + .GroupBy(e => e.Tfm); + + foreach (var tfm in libs) + { + using var _ = Helpers.UseTempDir(out var temp); + + foreach (var lib in tfm) { - if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) - { - var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), - CompressionLevel.Optimal); - using (var src = entry.Open()) - using (var dst = newEntry.Open()) - src.CopyTo(dst); - } - } + var extractedDllPath = Path.Combine(temp, lib.DllName); + var extractedPdbPath = Path.Combine(temp, lib.PdbName); + + lib.DllEntry.ExtractToFile(extractedDllPath); + lib.PdbEntry.ExtractToFile(extractedPdbPath); - var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) - .Select((e => new { s = e.FullName.Split('/'), e = e })) - .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) - .GroupBy(x => x.Tfm); - foreach(var tfm in libs) - using (Helpers.UseTempDir(out var temp)) - { - foreach (var l in tfm) - l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - PatchRefAssembly(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); - } + PatchRefAssembly(extractedDllPath); + + mainArchive.CreateEntryFromFile(extractedDllPath, $"ref/{lib.Tfm}/{lib.DllName}"); + symbolsArchive.CreateEntryFromFile(extractedPdbPath, $"ref/{lib.Tfm}/{lib.PdbName}"); + } } + + static ZipArchive OpenPackage(string packagePath) + => new(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update); + + static ZipArchiveEntry[] GetLibEntries(ZipArchive archive, string extension) + => archive.Entries + .Where(e => e.FullName.StartsWith("lib/", StringComparison.Ordinal) + && e.FullName.EndsWith(extension, StringComparison.Ordinal)) + .ToArray(); } } From adcaf6a317038033391f3d6a22e4b702118eea1b Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 3 Aug 2023 22:38:52 +0300 Subject: [PATCH 26/33] fix: try 1 --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From b0f6d17c558fd7ece0c3d56d090433087de66871 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 00:31:33 +0300 Subject: [PATCH 27/33] unit tests added --- .../WindowDataContextTests.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs new file mode 100644 index 0000000000..239b090515 --- /dev/null +++ b/tests/Avalonia.LeakTests/WindowDataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class WindowDataContextTests +{ + public WindowDataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From c418442d1345ee8ea5ed113ae9453bd740088c08 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:17:57 +0300 Subject: [PATCH 28/33] Revert "unit tests added" This reverts commit b0f6d17c558fd7ece0c3d56d090433087de66871. --- .../WindowDataContextTests.cs | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs deleted file mode 100644 index 239b090515..0000000000 --- a/tests/Avalonia.LeakTests/WindowDataContextTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Reactive; -using Avalonia.Threading; -using Avalonia.UnitTests; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests; - -internal class ViewModelForDisposingTest -{ - ~ViewModelForDisposingTest() { ; } -} - -[DotMemoryUnit(FailIfRunWithoutSupport = false)] -public class WindowDataContextTests -{ - public WindowDataContextTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() - { - static IDisposable Run() - { - var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); - var lifetime = new ClassicDesktopStyleApplicationLifetime(); - lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - - return Disposable.Create(lifetime, lt => lt.Shutdown()) - .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); - } - - using var _ = Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() - { - static void Run() - { - using var _ = UnitTestApplication.Start(TestServices.StyledWindow); - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - } - - Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } -} From ca2ca4ee9fae2e20155da6bd3148dc3e999fe57c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:20:34 +0300 Subject: [PATCH 29/33] Revert "fix: try 1" This reverts commit adcaf6a317038033391f3d6a22e4b702118eea1b. --- src/Avalonia.Controls/TopLevel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 66e402d642..04a5a0e6aa 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,7 +591,6 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); - StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 5b182890f3c3dece03e81c79ae1bdf889e85961c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:27:31 +0300 Subject: [PATCH 30/33] Added unit tests for non-disposable DataContext issue (#12123) --- tests/Avalonia.LeakTests/DataContextTests.cs | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/DataContextTests.cs diff --git a/tests/Avalonia.LeakTests/DataContextTests.cs b/tests/Avalonia.LeakTests/DataContextTests.cs new file mode 100644 index 0000000000..d16f8c1f57 --- /dev/null +++ b/tests/Avalonia.LeakTests/DataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class DataContextTests +{ + public DataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From 628455ec3d3291083d76216de9d9b7934d1fcfcd Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:29:17 +0300 Subject: [PATCH 31/33] To Avalonia.Controls.TopLevel.HandleClosed method body was added Avalonia.Controls.TopLevel.StopRendering method call --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 8328cc79e2bbe0d1b038d689a47df1ddea907665 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 8 Aug 2023 15:25:00 +0600 Subject: [PATCH 32/33] Unwrap win32 data object --- src/Windows/Avalonia.Win32/DataObject.cs | 1 + src/Windows/Avalonia.Win32/OleDropTarget.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index a215a0a322..df19cdc329 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -103,6 +103,7 @@ namespace Avalonia.Win32 private IDataObject _wrapped; + public IDataObject Wrapped => _wrapped; public DataObject(IDataObject wrapped) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index a81652ffc2..94d744301a 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -217,7 +217,7 @@ namespace Avalonia.Win32 if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) { - return dataObject; + return dataObject.Wrapped; } return new OleDataObject(pDataObj); } From 2c889d558acf18b6e30714a56dc4158d4d9fe647 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Aug 2023 11:28:10 +0100 Subject: [PATCH 33/33] Fixes #YOUTRACK-HDSW-24 --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4258572df7..ebfa325067 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all