Browse Source

Merge pull request #12330 from AvaloniaUI/feature/embedded-automation-roots

Allow embedded root automation peers.
release/11.0.3
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
1896e8ad68
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      native/Avalonia.Native/src/OSX/automation.mm
  2. 23
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  3. 7
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  4. 2
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  5. 2
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  6. 17
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  7. 33
      src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
  8. 25
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  9. 121
      src/Avalonia.Native/AvnAutomationPeer.cs
  10. 7
      src/Avalonia.Native/avn.idl
  11. 75
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  12. 48
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

11
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<INSWindowHolder*>(window);
auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@ -284,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

23
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
{
@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
/// </summary>
/// <returns></returns>
public AutomationPeer? GetParent() => GetParentCore();
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the root of this <see cref="AutomationPeer"/>'s
/// visual tree.
/// </summary>
public AutomationPeer? GetVisualRoot() => GetVisualRootCore();
/// <summary>
/// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus.
@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers
return GetAutomationControlTypeCore();
}
protected virtual AutomationPeer? GetVisualRootCore()
{
var peer = this;
var parent = peer.GetParent();
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
}
return peer;
}
protected virtual bool IsContentElementOverrideCore()
{
return IsControlElement() && IsContentElementCore();

7
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;
}
/// <summary>
/// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
/// </summary>

2
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<IScrollProvider>();
_searchedForScrollable = true;
}

2
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<ISelectionProvider>();
}
return null;

17
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<Control>(includeSelf: true);
return hit is object ? GetOrCreate(hit) : null;
if (hit is null)
return null;
var peer = GetOrCreate(hit);
while (peer != this && peer.GetProvider<IEmbeddedRootProvider>() is { } embedded)
{
var embeddedHit = embedded.GetPeerFromPoint(p);
if (embeddedHit is null)
break;
peer = embeddedHit;
}
return peer;
}
protected void StartTrackingFocus()

33
src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Automation.Peers;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposure methods and properties to support UI Automation client access to the root of an
/// automation tree hosted by another UI framework.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and can be used
/// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's
/// automation support.
/// </remarks>
public interface IEmbeddedRootProvider
{
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged;
}
}

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

@ -4,11 +4,36 @@ using Avalonia.Platform;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support UI Automation client access to the root of an
/// automation tree.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and should only
/// be implemented on true root elements, such as Windows. To embed an automation tree, use
/// <see cref="IEmbeddedRootProvider"/> instead.
/// </remarks>
public interface IRootProvider
{
/// <summary>
/// Gets the platform implementation of the TopLevel for the element.
/// </summary>
ITopLevelImpl? PlatformImpl { get; }
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged;
}
}

121
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();
@ -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();
@ -48,6 +49,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
@ -55,7 +63,7 @@ namespace Avalonia.Native
var peer = _inner;
var parent = peer.GetParent();
while (peer is not IRootProvider && parent is not null)
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
@ -65,26 +73,23 @@ 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 is IRootProvider).AsComBool();
private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider<IEmbeddedRootProvider>();
private IExpandCollapseProvider ExpandCollapseProvider => GetProvider<IExpandCollapseProvider>();
private IInvokeProvider InvokeProvider => GetProvider<IInvokeProvider>();
private IRangeValueProvider RangeValueProvider => GetProvider<IRangeValueProvider>();
private IRootProvider RootProvider => GetProvider<IRootProvider>();
private ISelectionItemProvider SelectionItemProvider => GetProvider<ISelectionItemProvider>();
private IToggleProvider ToggleProvider => GetProvider<IToggleProvider>();
private IValueProvider ValueProvider => GetProvider<IValueProvider>();
public IAvnWindowBase RootProvider_GetWindow()
{
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
return ((WindowBaseImpl)window.PlatformImpl!).Native;
}
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
public int IsRootProvider() => IsProvider<IRootProvider>();
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;
@ -103,46 +108,80 @@ namespace Avalonia.Native
return Wrap(result);
}
public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch
public int IsEmbeddedRootProvider() => IsProvider<IEmbeddedRootProvider>();
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<IExpandCollapseProvider>();
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<IInvokeProvider>();
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<IRangeValueProvider>();
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 is ISelectionItemProvider).AsComBool();
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
public int IsSelectionItemProvider() => IsProvider<ISelectionItemProvider>();
public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
public int IsToggleProvider() => IsProvider<IToggleProvider>();
public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
public int IsValueProvider() => IsProvider<IValueProvider>();
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<T>()
{
return _inner.GetProvider<T>() ?? throw new InvalidOperationException(
$"The peer {_inner} does not implement {typeof(T)}.");
}
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

7
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();

75
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<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged;
}
public AutomationPeer Peer { get; protected set; }
@ -89,21 +75,12 @@ namespace Avalonia.Win32.Automation
public virtual IRawElementProviderFragmentRoot? FragmentRoot
{
get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot;
get => InvokeSync(() => GetRoot());
}
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,21 +227,49 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException();
}
protected void RaiseChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
private AutomationNode? GetRoot()
protected void RaiseFocusChanged(AutomationNode? focused)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
private RootAutomationNode? GetRoot()
{
Dispatcher.UIThread.VerifyAccess();
return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode;
}
var peer = Peer;
var parent = peer.GetParent();
private void OnPeerChildrenChanged(object? sender, EventArgs e)
{
RaiseChildrenChanged();
}
while (peer.GetProvider<AAP.IRootProvider>() is null && parent is object)
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
peer = parent;
parent = peer.GetParent();
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
}
return peer is object ? GetOrCreate(peer) : null;
private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
{
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus()));
}
private static AutomationNode Create(AutomationPeer peer)

48
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<IRootProvider>() ?? 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()));
}
}
}

Loading…
Cancel
Save