Skip to content

Commit

Permalink
Merge pull request #12330 from AvaloniaUI/feature/embedded-automation…
Browse files Browse the repository at this point in the history
…-roots

Allow embedded root automation peers.
  • Loading branch information
danwalmsley authored Aug 10, 2023
2 parents 2c889d5 + 8ab5a55 commit 1896e8a
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 124 deletions.
11 changes: 9 additions & 2 deletions native/Avalonia.Native/src/OSX/automation.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<INSWindowHolder*>(window);
auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
Expand Down Expand Up @@ -284,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
Expand Down
23 changes: 22 additions & 1 deletion src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Provider;

namespace Avalonia.Automation.Peers
{
Expand Down Expand Up @@ -115,9 +116,14 @@ public abstract class AutomationPeer
/// <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.
Expand Down Expand Up @@ -247,6 +253,21 @@ protected virtual AutomationControlType GetControlTypeOverrideCore()
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IScrollProvider>();
_searchedForScrollable = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public ISelectionProvider? SelectionContainer
if (Owner.Parent is Control parent)
{
var parentPeer = GetOrCreate(parent);
return parentPeer as ISelectionProvider;
return parentPeer.GetProvider<ISelectionProvider>();
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Input;
Expand Down Expand Up @@ -32,7 +33,21 @@ protected override AutomationControlType GetAutomationControlTypeCore()
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()
Expand Down
33 changes: 33 additions & 0 deletions src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,36 @@

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 changes: 80 additions & 41 deletions src/Avalonia.Native/AvnAutomationPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -48,14 +49,21 @@ 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
{
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();
Expand All @@ -65,26 +73,23 @@ 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 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;
Expand All @@ -103,46 +108,80 @@ public IAvnWindowBase RootProvider_GetWindow()
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
Expand Down
Loading

0 comments on commit 1896e8a

Please sign in to comment.