Browse Source

WIP: I give up

feature/ui-automation-atspi
grokys 5 years ago
parent
commit
41efbbb4a4
  1. 44
      src/Avalonia.FreeDesktop/Atspi.cs
  2. 53
      src/Avalonia.FreeDesktop/AtspiCache.cs
  3. 28
      src/Avalonia.FreeDesktop/AtspiContext.cs
  4. 75
      src/Avalonia.FreeDesktop/AtspiRoot.cs

44
src/Avalonia.FreeDesktop/Atspi.cs

@ -148,6 +148,41 @@ namespace Avalonia.FreeDesktop.Atspi
public ObjectPath Path { get; }
}
internal readonly struct CacheItem
{
public CacheItem(
ObjectReference path,
ObjectReference application,
ObjectReference parent,
ObjectReference[] children,
string[] supportedInterfaces,
string name,
uint role,
string description,
int[] state)
{
Path = path;
Application = application;
Parent = parent;
Children = children;
SupportedInterfaces = supportedInterfaces;
Name = name;
Role = role;
Description = description;
State = state;
}
public ObjectReference Path { get; }
public ObjectReference Application { get; }
public ObjectReference Parent { get; }
public ObjectReference[] Children { get; }
public string[] SupportedInterfaces { get; }
public string Name { get; }
public uint Role { get; }
public string Description { get; }
public int[] State { get; }
};
[DBusInterface("org.a11y.Bus")]
internal interface IBus : IDBusObject
{
@ -176,6 +211,7 @@ namespace Avalonia.FreeDesktop.Atspi
[DBusInterface("org.a11y.atspi.Application")]
internal interface IApplication : IDBusObject
{
Task<string> GetApplicationBusAddressAsync();
Task<string> GetLocaleAsync(uint lcType);
Task RegisterEventListenerAsync(string Event);
Task DeregisterEventListenerAsync(string Event);
@ -185,6 +221,14 @@ namespace Avalonia.FreeDesktop.Atspi
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("org.a11y.atspi.Cache")]
internal interface ICache : IDBusObject
{
Task<CacheItem[]> GetItemsAsync();
Task<IDisposable> WatchAddAccessibleAsync(Action<CacheItem> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRemoveAccessibleAsync(Action<CacheItem> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.a11y.atspi.Socket")]
internal interface ISocket : IDBusObject
{

53
src/Avalonia.FreeDesktop/AtspiCache.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.FreeDesktop.Atspi;
using Tmds.DBus;
#nullable enable
namespace Avalonia.FreeDesktop
{
internal class AtspiCache : ICache
{
private readonly AtspiRoot _root;
private readonly ObservableCollection<CacheItem> _items = new ObservableCollection<CacheItem>();
public AtspiCache(AtspiRoot root)
{
_root = root;
}
public ObjectPath ObjectPath => "/org/a11y/atspi/cache";
public void Add(AtspiContext item) => _items.Add(item.ToCacheItem());
public Task<CacheItem[]> GetItemsAsync() => Task.FromResult(_items.ToArray());
public Task<IDisposable> WatchAddAccessibleAsync(Action<CacheItem> handler, Action<Exception>? onError = null)
{
void Listener(object s, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
foreach (CacheItem i in e.NewItems) handler(i);
}
_items.CollectionChanged += Listener;
return Task.FromResult(Disposable.Create(() => _items.CollectionChanged -= Listener));
}
public Task<IDisposable> WatchRemoveAccessibleAsync(Action<CacheItem> handler, Action<Exception>? onError = null)
{
void Listener(object s, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (CacheItem i in e.NewItems) handler(i);
}
_items.CollectionChanged += Listener;
return Task.FromResult(Disposable.Create(() => _items.CollectionChanged -= Listener));
}
}
}

28
src/Avalonia.FreeDesktop/AtspiContext.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Controls.Automation.Peers;
using Avalonia.FreeDesktop.Atspi;
@ -10,8 +11,15 @@ using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
/// <summary>
/// A node in the AT-SPI UI automation tree.
/// </summary>
/// <remarks>
/// This class is the platform implementation for an <see cref="AutomationPeer"/> when using AT-SPI.
/// </remarks>
internal class AtspiContext : IAccessible, IAutomationPeerImpl
{
private static uint _id;
private readonly AtspiRoot _root;
private readonly AutomationPeer _peer;
private readonly AtspiRole _role;
@ -21,11 +29,25 @@ namespace Avalonia.FreeDesktop
_root = root;
_peer = peer;
_role = role;
ObjectPath = new ObjectPath("/net/avaloniaui/a11y/" + Guid.NewGuid().ToString().Replace("-", ""));
ObjectPath = new ObjectPath("/org/a11y/atspi/accessible/" + ++_id);
}
public ObjectPath ObjectPath { get; }
public CacheItem ToCacheItem()
{
return new CacheItem(
new ObjectReference(_root.LocalName, ObjectPath),
_root.ApplicationPath,
_root.ApplicationPath,
new ObjectReference[0],
new[] { "org.a11y.atspi.Accessible" },
string.Empty,
(uint)_role,
string.Empty,
new[] { 0, 0 });
}
Task<ObjectReference> IAccessible.GetChildAtIndexAsync(int Index)
{
throw new NotImplementedException();
@ -65,12 +87,12 @@ namespace Avalonia.FreeDesktop
Task IAccessible.SetAsync(string prop, object val)
{
throw new NotImplementedException();
return Task.FromResult(Disposable.Empty);
}
Task<IDisposable> IAccessible.WatchPropertiesAsync(Action<PropertyChanges> handler)
{
throw new NotImplementedException();
return Task.FromResult(Disposable.Empty);
}
}
}

75
src/Avalonia.FreeDesktop/AtspiRoot.cs

@ -11,13 +11,21 @@ using Avalonia.FreeDesktop.Atspi;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Threading;
using JetBrains.Annotations;
using Tmds.DBus;
#nullable enable
namespace Avalonia.FreeDesktop
{
public class AtspiRoot : IAccessible, IApplication, IPlatformAutomationPeerFactory
/// <summary>
/// The root Application in the AT-SPI automation tree.
/// </summary>
/// <remarks>
/// When using AT-SPI there is a single AT-SPI root object for the application. Its children are the application's
/// open windows.
/// </remarks>
public class AtspiRoot : IAccessible, IApplication
{
private const string RootPath = "/org/a11y/atspi/accessible/root";
private const string AtspiVersion = "2.1";
@ -27,8 +35,10 @@ namespace Avalonia.FreeDesktop
private static bool _instanceInitialized;
private readonly List<Child> _children = new List<Child>();
private string? _applicationBusAddress;
private Connection? _connection;
private string? _localName;
private AtspiCache? _cache;
private AccessibleProperties? _accessibleProperties;
private ApplicationProperties? _applicationProperties;
@ -39,9 +49,10 @@ namespace Avalonia.FreeDesktop
}
public ObjectPath ObjectPath => RootPath;
public ObjectReference ApplicationPath => _accessibleProperties!.Parent;
public ObjectReference ApplicationPath => new ObjectReference(LocalName, ObjectPath);
public IDictionary<string, string> Attributes { get; }
public string LocalName => _localName ?? throw new AvaloniaInternalException("AT-SPI not initialized.");
public static AtspiRoot? RegisterRoot(Func<AutomationPeer> peerGetter)
{
if (!_instanceInitialized)
@ -56,7 +67,14 @@ namespace Avalonia.FreeDesktop
public IAutomationPeerImpl CreateAutomationPeerImpl(AutomationPeer peer)
{
return AtspiContextFactory.Create(this, peer);
if (_connection is null)
throw new AvaloniaInternalException("AT-SPI not initialized.");
var result = AtspiContextFactory.Create(this, peer);
var _ = _connection.RegisterObjectAsync(result);
_cache!.Add(result);
System.Diagnostics.Debug.WriteLine($"Created {result.ObjectPath} for {peer}");
return result;
}
private async void Register(Connection sessionConnection)
@ -65,21 +83,23 @@ namespace Avalonia.FreeDesktop
{
// Get the address of the a11y bus and open a connection to it.
var bus = sessionConnection.CreateProxy<IBus>("org.a11y.Bus", "/org/a11y/bus");
var address = await bus.GetAddressAsync();
var connection = new Connection(address);
_applicationBusAddress = await bus.GetAddressAsync();
var connection = new Connection(_applicationBusAddress);
var connectionInfo = await connection.ConnectAsync();
// Register the org.a11y.atspi.Application and org.a11y.atspi.Accessible interfaces at the well-known
// object path
// object path.
await connection.RegisterObjectAsync(this);
// Register ourselves on the a11y bus.
// Get the a11y Register object's Socket interface.
var socket = connection.CreateProxy<ISocket>("org.a11y.atspi.Registry", RootPath);
var plug = new ObjectReference(connectionInfo.LocalName, RootPath);
// Set up our properties now as they can be read when we call Socket.Embed.
_accessibleProperties = new AccessibleProperties
{
Name = Application.Current.Name ?? "Unnamed",
Description = string.Empty,
Locale = CultureInfo.CurrentCulture.Name,
ChildCount = _children.Count,
AccessibleId = string.Empty,
@ -93,9 +113,22 @@ namespace Avalonia.FreeDesktop
ToolkitName = "Avalonia",
};
_accessibleProperties.Parent = await socket.EmbedAsync(plug);
_localName = connectionInfo.LocalName;
// Store the connection object and local name as the call to Socket.Embed will result in a call
// to GetChildAtIndexAsync.
_connection = connection;
_localName = connectionInfo.LocalName;
// Embed ourselves using the Socket.Embed method. We pass the local name (aka unique name) for the
// connection along with the root a11y path and get back an object reference to the desktop object
// (I think?).
var plug = new ObjectReference(connectionInfo.LocalName, RootPath);
_accessibleProperties.Parent = await socket.EmbedAsync(plug);
// Create and register the cache.
_cache = new AtspiCache(this);
await connection.RegisterObjectAsync(_cache);
System.Diagnostics.Debug.WriteLine($"Set up AtspiRoot on {LocalName}");
}
catch (Exception e)
{
@ -119,20 +152,19 @@ namespace Avalonia.FreeDesktop
if (context is null)
throw new AvaloniaInternalException("AutomationPeer has no platform implementation.");
return new ObjectReference(_localName!, context.ObjectPath);
return new ObjectReference(LocalName, context.ObjectPath);
}
Task<ObjectReference[]> IAccessible.GetChildrenAsync()
async Task<ObjectReference[]> IAccessible.GetChildrenAsync()
{
var result = new List<ObjectReference>();
var result = new ObjectReference[_children.Count];
// foreach (var p in _automationPeers)
// {
// var peer = p();
// result.Add((_localName, p.GetHashCode().ToString()));
// }
for (var i = 0; i < _children.Count; ++i)
{
result[i] = await ((IAccessible)this).GetChildAtIndexAsync(i);
}
return Task.FromResult(result.ToArray());
return result;
}
Task<int> IAccessible.GetIndexInParentAsync() => Task.FromResult(-1);
@ -149,6 +181,7 @@ namespace Avalonia.FreeDesktop
Task<ObjectReference> IAccessible.GetApplicationAsync() => Task.FromResult(ApplicationPath);
Task<IDictionary<string, string>> IAccessible.GetAttributesAsync() => Task.FromResult(Attributes);
Task<string> IApplication.GetApplicationBusAddressAsync() => Task.FromResult(_applicationBusAddress!);
Task<string> IApplication.GetLocaleAsync(uint lcType) => Task.FromResult(CultureInfo.CurrentCulture.Name);
Task IApplication.RegisterEventListenerAsync(string Event)

Loading…
Cancel
Save