Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow embedded root automation peers. #12330

Merged
merged 20 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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