Browse Source

WIP: Dead

feature/ui-automation-atspi
grokys 5 years ago
parent
commit
7d6a5f1b0e
  1. 20
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  2. 23
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  3. 2
      src/Avalonia.Controls/Platform/IPlatformAutomationPeerFactory.cs
  4. 69
      src/Avalonia.FreeDesktop/AtspiContext.cs
  5. 34
      src/Avalonia.FreeDesktop/AtspiContextFactory.cs
  6. 63
      src/Avalonia.FreeDesktop/AtspiRoot.cs
  7. 10
      src/Avalonia.X11/X11Window.cs
  8. 9
      src/Windows/Avalonia.Win32/Win32Platform.cs
  9. 11
      src/Windows/Avalonia.Win32/WindowImpl.cs
  10. 6
      tests/Avalonia.UnitTests/TestServices.cs
  11. 2
      tests/Avalonia.UnitTests/UnitTestApplication.cs

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

@ -72,6 +72,14 @@ namespace Avalonia.Controls.Automation.Peers
public void SetFocus() => SetFocusCore();
internal void CreatePlatformImpl()
{
if (PlatformImpl is object)
throw new InvalidOperationException("Automation peer platform implementation already created.");
PlatformImpl = CreatePlatformImplCore();
}
protected abstract IAutomationPeerImpl CreatePlatformImplCore();
protected abstract Rect GetBoundingRectangleCore();
protected abstract IReadOnlyList<AutomationPeer>? GetChildrenCore();
protected abstract string GetClassNameCore();
@ -104,17 +112,5 @@ namespace Avalonia.Controls.Automation.Peers
}
protected virtual Rect GetVisibleBoundingRectCore() => GetBoundingRectangleCore();
internal void CreatePlatformImpl()
{
var ifs = AvaloniaLocator.Current.GetService<IPlatformAutomationInterface>();
if (ifs is null)
{
throw new NotSupportedException("No automation interface registered for this platform.");
}
PlatformImpl = ifs.CreateAutomationPeerImpl(this);
}
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.VisualTree;
#nullable enable
@ -8,19 +10,34 @@ namespace Avalonia.Controls.Automation.Peers
{
public abstract class ControlAutomationPeer : AutomationPeer
{
public ControlAutomationPeer(Control owner)
protected ControlAutomationPeer(Control owner)
{
Owner = owner ?? throw new ArgumentNullException("owner");
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
public Control Owner { get; }
public static AutomationPeer? GetOrCreatePeer(Control element)
{
element = element ?? throw new ArgumentNullException("element");
element = element ?? throw new ArgumentNullException(nameof(element));
return element.GetOrCreateAutomationPeer();
}
protected override IAutomationPeerImpl CreatePlatformImplCore()
{
var root = Owner.GetVisualRoot();
if (root is null)
throw new InvalidOperationException("Cannot create automation peer for non-rooted control.");
if ((root as TopLevel)?.PlatformImpl is IPlatformAutomationPeerFactory factory)
{
return factory.CreateAutomationPeerImpl(this);
}
throw new InvalidOperationException("UI automation not available on this platform.");
}
protected override Rect GetBoundingRectangleCore()
{
var root = Owner.GetVisualRoot();

2
src/Avalonia.Controls/Platform/IPlatformAutomationInterface.cs → src/Avalonia.Controls/Platform/IPlatformAutomationPeerFactory.cs

@ -5,7 +5,7 @@ using Avalonia.Platform;
namespace Avalonia.Controls.Platform
{
public interface IPlatformAutomationInterface
public interface IPlatformAutomationPeerFactory
{
IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer);
}

69
src/Avalonia.FreeDesktop/AtspiContext.cs

@ -1,9 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls.Automation.Peers;
using Avalonia.FreeDesktop.Atspi;
using Avalonia.Platform;
using Tmds.DBus;
#nullable enable
namespace Avalonia.FreeDesktop
{
public class AtspiContext : IAccessible
internal class AtspiContext : IAccessible, IAutomationPeerImpl
{
private readonly AtspiRoot _root;
private readonly AutomationPeer _peer;
private readonly AtspiRole _role;
public AtspiContext(AtspiRoot root, AutomationPeer peer, AtspiRole role)
{
_root = root;
_peer = peer;
_role = role;
ObjectPath = new ObjectPath("/net/avaloniaui/a11y/" + Guid.NewGuid().ToString().Replace("-", ""));
}
public ObjectPath ObjectPath { get; }
Task<(string, ObjectPath)> IAccessible.GetChildAtIndexAsync(int Index)
{
throw new NotImplementedException();
}
Task<(string, ObjectPath)[]> IAccessible.GetChildrenAsync()
{
throw new NotImplementedException();
}
Task<int> IAccessible.GetIndexInParentAsync()
{
throw new NotImplementedException();
}
Task<(uint, (string, ObjectPath)[])[]> IAccessible.GetRelationSetAsync()
{
throw new NotImplementedException();
}
Task<uint> IAccessible.GetRoleAsync() => Task.FromResult((uint)_role);
Task<string> IAccessible.GetRoleNameAsync() => Task.FromResult(_role.ToString()); // TODO
Task<string> IAccessible.GetLocalizedRoleNameAsync() => Task.FromResult(_role.ToString()); // TODO
Task<uint[]> IAccessible.GetStateAsync() => Task.FromResult(new uint[] { 0, 0 }); // TODO
Task<IDictionary<string, string>> IAccessible.GetAttributesAsync() => Task.FromResult(_root.Attributes);
Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(_root.ApplicationPath);
Task<object?> IAccessible.GetAsync(string prop)
{
throw new NotImplementedException();
}
Task<AccessibleProperties> IAccessible.GetAllAsync()
{
throw new NotImplementedException();
}
Task IAccessible.SetAsync(string prop, object val)
{
throw new NotImplementedException();
}
Task<IDisposable> IAccessible.WatchPropertiesAsync(Action<PropertyChanges> handler)
{
throw new NotImplementedException();
}
}
}

34
src/Avalonia.FreeDesktop/AtspiContextFactory.cs

@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Controls.Automation.Peers;
using Avalonia.FreeDesktop.Atspi;
using Avalonia.Threading;
namespace Avalonia.FreeDesktop
{
internal static class AtspiContextFactory
{
public static AtspiContext Create(AtspiRoot root, AutomationPeer peer)
{
Dispatcher.UIThread.VerifyAccess();
if (peer.PlatformImpl is object)
{
throw new AvaloniaInternalException($"Peer already has a platform implementation: {peer}.");
}
var result = peer switch
{
ButtonAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_PUSH_BUTTON),
MenuAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_MENU),
MenuItemAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_MENU_ITEM),
TabControlAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_PAGE_TAB_LIST),
TabItemAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_PAGE_TAB),
TextAutomationPeer _ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_STATIC),
_ => new AtspiContext(root, peer, AtspiRole.ATSPI_ROLE_UNKNOWN),
};
//var _ = result.Update();
return result;
}
}
}

63
src/Avalonia.FreeDesktop/AtspiRoot.cs

@ -6,16 +6,18 @@ using System.Reactive.Disposables;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop.Atspi;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Threading;
using Tmds.DBus;
#nullable enable
namespace Avalonia.FreeDesktop
{
public class AtspiRoot : IAccessible, IApplication
public class AtspiRoot : IAccessible, IApplication, IPlatformAutomationPeerFactory
{
private const string RootPath = "/org/a11y/atspi/accessible/root";
private const string AtspiVersion = "2.1";
@ -26,16 +28,19 @@ namespace Avalonia.FreeDesktop
private readonly List<Child> _children = new List<Child>();
private Connection? _connection;
private (string, ObjectPath) _application;
private string? _localName;
private AccessibleProperties? _accessibleProperties;
private ApplicationProperties? _applicationProperties;
public AtspiRoot(Connection sessionConnection)
{
Register(sessionConnection);
Attributes = new Dictionary<string, string> { { "toolkit", "Avalonia" } };
}
ObjectPath IDBusObject.ObjectPath => RootPath;
public ObjectPath ObjectPath => RootPath;
public (string, ObjectPath) ApplicationPath => _accessibleProperties!.Parent;
public IDictionary<string, string> Attributes { get; }
public static AtspiRoot? RegisterRoot(Func<AutomationPeer> peerGetter)
{
@ -49,6 +54,11 @@ namespace Avalonia.FreeDesktop
return _instance;
}
public IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer)
{
return AtspiContextFactory.Create(this, peer);
}
private async void Register(Connection sessionConnection)
{
try
@ -71,7 +81,6 @@ namespace Avalonia.FreeDesktop
{
Name = Application.Current.Name ?? "Unnamed",
Locale = CultureInfo.CurrentCulture.Name,
Parent = _application,
ChildCount = _children.Count,
AccessibleId = string.Empty,
};
@ -84,7 +93,8 @@ namespace Avalonia.FreeDesktop
ToolkitName = "Avalonia",
};
_application = await socket.EmbedAsync(plug);
_accessibleProperties.Parent = await socket.EmbedAsync(plug);
_localName = connectionInfo.LocalName;
_connection = connection;
}
catch (Exception e)
@ -93,22 +103,33 @@ namespace Avalonia.FreeDesktop
}
}
Task<(string, ObjectPath)> IAccessible.GetChildAtIndexAsync(int index)
async Task<(string, ObjectPath)> IAccessible.GetChildAtIndexAsync(int index)
{
var child = _children[index];
var peer = child.Peer.PlatformImpl;
var peer = child.Peer;
if (peer is null)
{
await Dispatcher.UIThread.InvokeAsync(() => child.CreatePeer());
peer = child.Peer!;
}
var context = (AtspiContext?)peer.PlatformImpl;
if (context is null)
throw new AvaloniaInternalException("AutomationPeer has no platform implementation.");
return (_localName!, context.ObjectPath);
}
Task<(string, ObjectPath)[]> IAccessible.GetChildrenAsync()
{
var result = new List<(string, ObjectPath)>();
// var address = _connection!.ConnectAsync().Result.LocalName;
//
// foreach (var p in _automationPeers)
// {
// var peer = p();
// result.Add((address, p.GetHashCode().ToString()));
// result.Add((_localName, p.GetHashCode().ToString()));
// }
return Task.FromResult(result.ToArray());
@ -125,15 +146,8 @@ namespace Avalonia.FreeDesktop
Task<string> IAccessible.GetRoleNameAsync() => Task.FromResult("application");
Task<string> IAccessible.GetLocalizedRoleNameAsync() => Task.FromResult("application");
Task<uint[]> IAccessible.GetStateAsync() => Task.FromResult(new uint[] { 0, 0 });
Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(_application);
Task<IDictionary<string, string>> IAccessible.GetAttributesAsync()
{
return Task.FromResult<IDictionary<string, string>>(new Dictionary<string, string>
{
{ "toolkit", "Avalonia" }
});
}
Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(ApplicationPath);
Task<IDictionary<string, string>> IAccessible.GetAttributesAsync() => Task.FromResult(Attributes);
Task<string> IApplication.GetLocaleAsync(uint lcType) => Task.FromResult(CultureInfo.CurrentCulture.Name);
@ -207,9 +221,14 @@ namespace Avalonia.FreeDesktop
private class Child
{
private readonly Func<AutomationPeer> _peerGetter;
private AutomationPeer? _peer;
public Child(Func<AutomationPeer> peerGetter) => _peerGetter = peerGetter;
public AutomationPeer Peer => _peer ??= _peerGetter();
public AutomationPeer? Peer { get; private set; }
public void CreatePeer()
{
Dispatcher.UIThread.VerifyAccess();
Peer = _peerGetter();
}
}
}
}

10
src/Avalonia.X11/X11Window.cs

@ -25,7 +25,8 @@ namespace Avalonia.X11
{
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
ITopLevelImplWithNativeMenuExporter,
ITopLevelImplWithNativeControlHost
ITopLevelImplWithNativeControlHost,
IPlatformAutomationPeerFactory
{
private readonly AvaloniaX11Platform _platform;
private readonly IWindowImpl _popupParent;
@ -1133,6 +1134,13 @@ namespace Avalonia.X11
{
}
public IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer)
{
if (Atspi is null)
throw new InvalidOperationException("Cannot create automation peer: AT-SPI not available.");
return Atspi.CreateAutomationPeerImpl(peer);
}
public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);

9
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -66,8 +66,7 @@ namespace Avalonia.Win32
class Win32Platform : IPlatformThreadingInterface,
IPlatformSettings,
IWindowingPlatform,
IPlatformIconLoader,
IPlatformAutomationInterface
IPlatformIconLoader
{
private static readonly Win32Platform s_instance = new Win32Platform();
private static Thread _uiThread;
@ -114,7 +113,6 @@ namespace Avalonia.Win32
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<IPlatformAutomationInterface>().ToConstant(s_instance)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IPlatformIconLoader>().ToConstant(s_instance)
.Bind<AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider>().ToConstant(new NonPumpingWaitProvider())
@ -262,11 +260,6 @@ namespace Avalonia.Win32
}
}
public IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer)
{
return AutomationProviderFactory.Create(peer);
}
private static IconImpl CreateIconImpl(Stream stream)
{
try

11
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -24,8 +24,10 @@ namespace Avalonia.Win32
/// <summary>
/// Window implementation for Win32 platform.
/// </summary>
public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithNativeControlHost
public partial class WindowImpl : IWindowImpl,
EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithNativeControlHost,
IPlatformAutomationPeerFactory
{
private static readonly List<WindowImpl> s_instances = new List<WindowImpl>();
@ -655,6 +657,11 @@ namespace Avalonia.Win32
_topmost = value;
}
public IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer)
{
return AutomationProviderFactory.Create(peer);
}
protected virtual IntPtr CreateWindowOverride(ushort atom)
{
return CreateWindowEx(

6
tests/Avalonia.UnitTests/TestServices.cs

@ -61,7 +61,7 @@ namespace Avalonia.UnitTests
public TestServices(
IAssetLoader assetLoader = null,
IPlatformAutomationInterface automationPlatform = null,
IPlatformAutomationPeerFactory automationPlatform = null,
IFocusManager focusManager = null,
IGlobalClock globalClock = null,
IInputManager inputManager = null,
@ -103,7 +103,7 @@ namespace Avalonia.UnitTests
}
public IAssetLoader AssetLoader { get; }
public IPlatformAutomationInterface AutomationPlatform { get; }
public IPlatformAutomationPeerFactory AutomationPlatform { get; }
public IInputManager InputManager { get; }
public IFocusManager FocusManager { get; }
public IGlobalClock GlobalClock { get; }
@ -124,7 +124,7 @@ namespace Avalonia.UnitTests
public TestServices With(
IAssetLoader assetLoader = null,
IPlatformAutomationInterface automationPlatform = null,
IPlatformAutomationPeerFactory automationPlatform = null,
IFocusManager focusManager = null,
IGlobalClock globalClock = null,
IInputManager inputManager = null,

2
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -50,7 +50,7 @@ namespace Avalonia.UnitTests
{
AvaloniaLocator.CurrentMutable
.Bind<IAssetLoader>().ToConstant(Services.AssetLoader)
.Bind<IPlatformAutomationInterface>().ToConstant(Services.AutomationPlatform)
.Bind<IPlatformAutomationPeerFactory>().ToConstant(Services.AutomationPlatform)
.Bind<IFocusManager>().ToConstant(Services.FocusManager)
.Bind<IGlobalClock>().ToConstant(Services.GlobalClock)
.BindToSelf<IGlobalStyles>(this)

Loading…
Cancel
Save