From 0e7b8f6f4589195ae35e933c64f89f9478832fb1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jul 2023 23:17:48 +0200 Subject: [PATCH 01/17] 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 739f0ac2512..0a73c8bc7b1 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ public RootAutomationNode(AutomationPeer peer) 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 @@ public override IRawElementProviderSimple? HostRawElementProvider 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/17] 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 d0c8d7a9dbf..4b325a092dd 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -73,6 +73,13 @@ + (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer 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/17] 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 6c4e96b31be..038b62a7f60 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -74,10 +74,11 @@ public void SetNode(IAvnAutomationNode node) 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/17] 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 038b62a7f60..6b1b4a7a031 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,7 +72,7 @@ public void SetNode(IAvnAutomationNode node) 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 @@ public void SetNode(IAvnAutomationNode node) 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 @@ public void SetNode(IAvnAutomationNode node) 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/17] 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 6b1b4a7a031..8069ac76bfb 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -48,6 +48,13 @@ private AvnAutomationPeer(AutomationPeer inner) 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 @@ public IAvnAutomationPeer? RootPeer } } - 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 @@ public void SetNode(IAvnAutomationNode node) 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/17] 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 db16bf0a538..64727c43c5a 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -28,7 +28,7 @@ protected virtual IScrollProvider? Scroller 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 aea91b5e268..dab8c455671 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -22,7 +22,7 @@ public ISelectionProvider? SelectionContainer 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/17] 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 8069ac76bfb..76cae2684fc 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -62,7 +62,7 @@ public IAvnAutomationPeer? RootPeer 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/17] 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 0a73c8bc7b1..739f0ac2512 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ public RootAutomationNode(AutomationPeer peer) 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 @@ public override IRawElementProviderSimple? HostRawElementProvider 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/17] 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 9ec65592fa2..3786ba32c7f 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 @@ protected override AutomationControlType GetAutomationControlTypeCore() 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/17] 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 3786ba32c7f..ceb695422de 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -39,7 +39,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() 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 00000000000..1b1caef182f --- /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 ce380595596..6a266da5c50 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -4,11 +4,36 @@ 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 3eeedc4b5da..e835c6a57a8 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -54,25 +54,11 @@ public AutomationNode(AutomationPeer peer) _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 @@ public virtual IRawElementProviderFragmentRoot? FragmentRoot 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 @@ protected TResult InvokeSync(Func func 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 @@ protected TResult InvokeSync(Func func 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 739f0ac2512..7334186c804 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -9,18 +9,14 @@ 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 @@ public RootAutomationNode(AutomationPeer peer) 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 @@ public override IRawElementProviderSimple? HostRawElementProvider 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/17] 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 76cae2684fc..5933dc6c926 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,10 +72,11 @@ public IAvnAutomationPeer? RootPeer } } - 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 @@ public IAvnAutomationPeer? RootPeer 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/17] 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 3d3fe35d299..a264909ba62 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -115,9 +115,14 @@ public abstract class AutomationPeer /// /// 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 @@ protected virtual AutomationControlType GetControlTypeOverrideCore() 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 e835c6a57a8..569f7da738e 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -75,7 +75,7 @@ public Rect BoundingRectangle public virtual IRawElementProviderFragmentRoot? FragmentRoot { - get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + get => InvokeSync(() => GetRoot()); } public virtual IRawElementProviderSimple? HostRawElementProvider => null; @@ -243,20 +243,10 @@ protected void RaiseFocusChanged(AutomationNode? focused) (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/17] 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 5933dc6c926..d2d93b69a97 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -39,6 +39,7 @@ private AvnAutomationPeer(AutomationPeer inner) 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 bc372bbcb53..0911e5ffff6 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/17] 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 4b325a092dd..9fe0ff3c60b 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -291,8 +291,8 @@ - (id)accessibilityTopLevelUIElement - (id)accessibilityWindow { - id topLevel = [self accessibilityTopLevelUIElement]; - return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; + auto rootPeer = _peer->GetVisualRoot(); + return [AvnAccessibilityElement acquire:rootPeer]; } - (BOOL)isAccessibilityExpanded From 90e3760c0f8e1c346a4b9d2d0f5a17f0fadbef9d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 09:19:42 +0200 Subject: [PATCH 15/17] 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 d2d93b69a97..af4958b02fe 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -22,8 +22,8 @@ private AvnAutomationPeer(AutomationPeer inner) { _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 16/17] 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 a264909ba62..fb7cdd87edb 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 @@ protected virtual AutomationControlType GetControlTypeOverrideCore() 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 17/17] 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 c19d8872307..69f267a605f 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -120,6 +120,13 @@ protected override IReadOnlyList GetOrCreateChildrenCore() 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 . ///