Skip to content

Commit 1896e8a

Browse files
authored
Merge pull request #12330 from AvaloniaUI/feature/embedded-automation-roots
Allow embedded root automation peers.
2 parents 2c889d5 + 8ab5a55 commit 1896e8a

File tree

12 files changed

+247
-124
lines changed

12 files changed

+247
-124
lines changed

native/Avalonia.Native/src/OSX/automation.mm

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ + (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
7373
if (peer->IsRootProvider())
7474
{
7575
auto window = peer->RootProvider_GetWindow();
76+
77+
if (window == nullptr)
78+
{
79+
NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl.");
80+
return nil;
81+
}
82+
7683
auto holder = dynamic_cast<INSWindowHolder*>(window);
7784
auto view = holder->GetNSView();
7885
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@@ -284,8 +291,8 @@ - (id)accessibilityTopLevelUIElement
284291

285292
- (id)accessibilityWindow
286293
{
287-
id topLevel = [self accessibilityTopLevelUIElement];
288-
return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil;
294+
auto rootPeer = _peer->GetVisualRoot();
295+
return [AvnAccessibilityElement acquire:rootPeer];
289296
}
290297

291298
- (BOOL)isAccessibilityExpanded

src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Avalonia.Automation.Provider;
34

45
namespace Avalonia.Automation.Peers
56
{
@@ -115,9 +116,14 @@ public abstract class AutomationPeer
115116
/// <summary>
116117
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
117118
/// </summary>
118-
/// <returns></returns>
119119
public AutomationPeer? GetParent() => GetParentCore();
120120

121+
/// <summary>
122+
/// Gets the <see cref="AutomationPeer"/> that is the root of this <see cref="AutomationPeer"/>'s
123+
/// visual tree.
124+
/// </summary>
125+
public AutomationPeer? GetVisualRoot() => GetVisualRootCore();
126+
121127
/// <summary>
122128
/// Gets a value that indicates whether the element that is associated with this automation
123129
/// peer currently has keyboard focus.
@@ -247,6 +253,21 @@ protected virtual AutomationControlType GetControlTypeOverrideCore()
247253
return GetAutomationControlTypeCore();
248254
}
249255

256+
protected virtual AutomationPeer? GetVisualRootCore()
257+
{
258+
var peer = this;
259+
var parent = peer.GetParent();
260+
261+
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
262+
{
263+
peer = parent;
264+
parent = peer.GetParent();
265+
}
266+
267+
return peer;
268+
}
269+
270+
250271
protected virtual bool IsContentElementOverrideCore()
251272
{
252273
return IsControlElement() && IsContentElementCore();

src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
120120
return _parent;
121121
}
122122

123+
protected override AutomationPeer? GetVisualRootCore()
124+
{
125+
if (Owner.GetVisualRoot() is Control c)
126+
return CreatePeerForElement(c);
127+
return null;
128+
}
129+
123130
/// <summary>
124131
/// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
125132
/// </summary>

src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ protected virtual IScrollProvider? Scroller
2828
if (!_searchedForScrollable)
2929
{
3030
if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
31-
_scroller = GetOrCreate(scrollable) as IScrollProvider;
31+
_scroller = GetOrCreate(scrollable).GetProvider<IScrollProvider>();
3232
_searchedForScrollable = true;
3333
}
3434

src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public ISelectionProvider? SelectionContainer
2222
if (Owner.Parent is Control parent)
2323
{
2424
var parentPeer = GetOrCreate(parent);
25-
return parentPeer as ISelectionProvider;
25+
return parentPeer.GetProvider<ISelectionProvider>();
2626
}
2727

2828
return null;

src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Globalization;
34
using Avalonia.Automation.Provider;
45
using Avalonia.Controls;
56
using Avalonia.Input;
@@ -32,7 +33,21 @@ protected override AutomationControlType GetAutomationControlTypeCore()
3233
public AutomationPeer? GetPeerFromPoint(Point p)
3334
{
3435
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true);
35-
return hit is object ? GetOrCreate(hit) : null;
36+
37+
if (hit is null)
38+
return null;
39+
40+
var peer = GetOrCreate(hit);
41+
42+
while (peer != this && peer.GetProvider<IEmbeddedRootProvider>() is { } embedded)
43+
{
44+
var embeddedHit = embedded.GetPeerFromPoint(p);
45+
if (embeddedHit is null)
46+
break;
47+
peer = embeddedHit;
48+
}
49+
50+
return peer;
3651
}
3752

3853
protected void StartTrackingFocus()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using Avalonia.Automation.Peers;
3+
4+
namespace Avalonia.Automation.Provider
5+
{
6+
/// <summary>
7+
/// Exposure methods and properties to support UI Automation client access to the root of an
8+
/// automation tree hosted by another UI framework.
9+
/// </summary>
10+
/// <remarks>
11+
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and can be used
12+
/// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's
13+
/// automation support.
14+
/// </remarks>
15+
public interface IEmbeddedRootProvider
16+
{
17+
/// <summary>
18+
/// Gets the currently focused element.
19+
/// </summary>
20+
AutomationPeer? GetFocus();
21+
22+
/// <summary>
23+
/// Gets the element at the specified point, expressed in top-level coordinates.
24+
/// </summary>
25+
/// <param name="p">The point.</param>
26+
AutomationPeer? GetPeerFromPoint(Point p);
27+
28+
/// <summary>
29+
/// Raised by the automation peer when the focus changes.
30+
/// </summary>
31+
event EventHandler? FocusChanged;
32+
}
33+
}

src/Avalonia.Controls/Automation/Provider/IRootProvider.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,36 @@
44

55
namespace Avalonia.Automation.Provider
66
{
7+
/// <summary>
8+
/// Exposes methods and properties to support UI Automation client access to the root of an
9+
/// automation tree.
10+
/// </summary>
11+
/// <remarks>
12+
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and should only
13+
/// be implemented on true root elements, such as Windows. To embed an automation tree, use
14+
/// <see cref="IEmbeddedRootProvider"/> instead.
15+
/// </remarks>
716
public interface IRootProvider
817
{
18+
/// <summary>
19+
/// Gets the platform implementation of the TopLevel for the element.
20+
/// </summary>
921
ITopLevelImpl? PlatformImpl { get; }
22+
23+
/// <summary>
24+
/// Gets the currently focused element.
25+
/// </summary>
1026
AutomationPeer? GetFocus();
27+
28+
/// <summary>
29+
/// Gets the element at the specified point, expressed in top-level coordinates.
30+
/// </summary>
31+
/// <param name="p">The point.</param>
1132
AutomationPeer? GetPeerFromPoint(Point p);
33+
34+
/// <summary>
35+
/// Raised by the automation peer when the focus changes.
36+
/// </summary>
1237
event EventHandler? FocusChanged;
1338
}
1439
}

src/Avalonia.Native/AvnAutomationPeer.cs

Lines changed: 80 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ private AvnAutomationPeer(AutomationPeer inner)
2222
{
2323
_inner = inner;
2424
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
25-
if (inner is WindowBaseAutomationPeer window)
26-
window.FocusChanged += (_, _) => Node?.FocusChanged();
25+
if (inner is IRootProvider root)
26+
root.FocusChanged += (_, _) => Node?.FocusChanged();
2727
}
2828

2929
~AvnAutomationPeer() => Node?.Dispose();
@@ -39,6 +39,7 @@ private AvnAutomationPeer(AutomationPeer inner)
3939
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
4040
public IAvnString Name => _inner.GetName().ToAvnString();
4141
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
42+
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
4243

4344
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
4445
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@@ -48,14 +49,21 @@ private AvnAutomationPeer(AutomationPeer inner)
4849
public void SetFocus() => _inner.SetFocus();
4950
public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool();
5051

52+
public void SetNode(IAvnAutomationNode node)
53+
{
54+
if (Node is not null)
55+
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
56+
Node = node;
57+
}
58+
5159
public IAvnAutomationPeer? RootPeer
5260
{
5361
get
5462
{
5563
var peer = _inner;
5664
var parent = peer.GetParent();
5765

58-
while (peer is not IRootProvider && parent is not null)
66+
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
5967
{
6068
peer = parent;
6169
parent = peer.GetParent();
@@ -65,26 +73,23 @@ public IAvnAutomationPeer? RootPeer
6573
}
6674
}
6775

68-
public void SetNode(IAvnAutomationNode node)
69-
{
70-
if (Node is not null)
71-
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
72-
Node = node;
73-
}
74-
75-
public int IsRootProvider() => (_inner is IRootProvider).AsComBool();
76+
private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider<IEmbeddedRootProvider>();
77+
private IExpandCollapseProvider ExpandCollapseProvider => GetProvider<IExpandCollapseProvider>();
78+
private IInvokeProvider InvokeProvider => GetProvider<IInvokeProvider>();
79+
private IRangeValueProvider RangeValueProvider => GetProvider<IRangeValueProvider>();
80+
private IRootProvider RootProvider => GetProvider<IRootProvider>();
81+
private ISelectionItemProvider SelectionItemProvider => GetProvider<ISelectionItemProvider>();
82+
private IToggleProvider ToggleProvider => GetProvider<IToggleProvider>();
83+
private IValueProvider ValueProvider => GetProvider<IValueProvider>();
7684

77-
public IAvnWindowBase RootProvider_GetWindow()
78-
{
79-
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
80-
return ((WindowBaseImpl)window.PlatformImpl!).Native;
81-
}
82-
83-
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
85+
public int IsRootProvider() => IsProvider<IRootProvider>();
86+
87+
public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native;
88+
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus());
8489

8590
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
8691
{
87-
var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint());
92+
var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
8893

8994
if (result is null)
9095
return null;
@@ -103,46 +108,80 @@ public IAvnWindowBase RootProvider_GetWindow()
103108
return Wrap(result);
104109
}
105110

106-
public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
107111

108-
public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch
112+
public int IsEmbeddedRootProvider() => IsProvider<IEmbeddedRootProvider>();
113+
114+
public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus());
115+
116+
public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point)
117+
{
118+
var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
119+
120+
if (result is null)
121+
return null;
122+
123+
// The OSX accessibility APIs expect non-ignored elements when hit-testing.
124+
while (!result.IsControlElement())
125+
{
126+
var parent = result.GetParent();
127+
128+
if (parent is not null)
129+
result = parent;
130+
else
131+
break;
132+
}
133+
134+
return Wrap(result);
135+
}
136+
137+
public int IsExpandCollapseProvider() => IsProvider<IExpandCollapseProvider>();
138+
139+
public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch
109140
{
110141
ExpandCollapseState.Expanded => 1,
111142
ExpandCollapseState.PartiallyExpanded => 1,
112143
_ => 0,
113144
};
114145

115-
public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool();
116-
public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand();
117-
public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse();
146+
public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool();
147+
public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand();
148+
public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse();
118149

119-
public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool();
120-
public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke();
150+
public int IsInvokeProvider() => IsProvider<IInvokeProvider>();
151+
public void InvokeProvider_Invoke() => InvokeProvider.Invoke();
121152

122-
public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool();
123-
public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value;
124-
public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum;
125-
public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum;
126-
public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange;
127-
public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange;
128-
public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value);
153+
public int IsRangeValueProvider() => IsProvider<IRangeValueProvider>();
154+
public double RangeValueProvider_GetValue() => RangeValueProvider.Value;
155+
public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum;
156+
public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum;
157+
public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange;
158+
public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange;
159+
public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value);
129160

130-
public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool();
131-
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
161+
public int IsSelectionItemProvider() => IsProvider<ISelectionItemProvider>();
162+
public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
132163

133-
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
134-
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
135-
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
164+
public int IsToggleProvider() => IsProvider<IToggleProvider>();
165+
public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
166+
public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
136167

137-
public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
138-
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
139-
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
168+
public int IsValueProvider() => IsProvider<IValueProvider>();
169+
public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString();
170+
public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value);
140171

141172
[return: NotNullIfNotNull("peer")]
142173
public static AvnAutomationPeer? Wrap(AutomationPeer? peer)
143174
{
144175
return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer));
145176
}
177+
178+
private T GetProvider<T>()
179+
{
180+
return _inner.GetProvider<T>() ?? throw new InvalidOperationException(
181+
$"The peer {_inner} does not implement {typeof(T)}.");
182+
}
183+
184+
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
146185
}
147186

148187
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

0 commit comments

Comments
 (0)