diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index fc9269839a..a667731df4 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/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? 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(); - - if (ifs is null) - { - throw new NotSupportedException("No automation interface registered for this platform."); - } - - PlatformImpl = ifs.CreateAutomationPeerImpl(this); - } } } diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 8002a03035..5d27563a48 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/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(); diff --git a/src/Avalonia.Controls/Platform/IPlatformAutomationInterface.cs b/src/Avalonia.Controls/Platform/IPlatformAutomationPeerFactory.cs similarity index 80% rename from src/Avalonia.Controls/Platform/IPlatformAutomationInterface.cs rename to src/Avalonia.Controls/Platform/IPlatformAutomationPeerFactory.cs index 0258316ab1..ef52a5e5d6 100644 --- a/src/Avalonia.Controls/Platform/IPlatformAutomationInterface.cs +++ b/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); } diff --git a/src/Avalonia.FreeDesktop/AtspiContext.cs b/src/Avalonia.FreeDesktop/AtspiContext.cs index 2b2f1eabc8..f095ac124f 100644 --- a/src/Avalonia.FreeDesktop/AtspiContext.cs +++ b/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 IAccessible.GetIndexInParentAsync() + { + throw new NotImplementedException(); + } + + Task<(uint, (string, ObjectPath)[])[]> IAccessible.GetRelationSetAsync() + { + throw new NotImplementedException(); + } + + Task IAccessible.GetRoleAsync() => Task.FromResult((uint)_role); + Task IAccessible.GetRoleNameAsync() => Task.FromResult(_role.ToString()); // TODO + Task IAccessible.GetLocalizedRoleNameAsync() => Task.FromResult(_role.ToString()); // TODO + Task IAccessible.GetStateAsync() => Task.FromResult(new uint[] { 0, 0 }); // TODO + Task> IAccessible.GetAttributesAsync() => Task.FromResult(_root.Attributes); + Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(_root.ApplicationPath); + + Task IAccessible.GetAsync(string prop) + { + throw new NotImplementedException(); + } + + Task IAccessible.GetAllAsync() + { + throw new NotImplementedException(); + } + + Task IAccessible.SetAsync(string prop, object val) + { + throw new NotImplementedException(); + } + + Task IAccessible.WatchPropertiesAsync(Action handler) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.FreeDesktop/AtspiContextFactory.cs b/src/Avalonia.FreeDesktop/AtspiContextFactory.cs new file mode 100644 index 0000000000..dfc8bc4713 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Avalonia.FreeDesktop/AtspiRoot.cs b/src/Avalonia.FreeDesktop/AtspiRoot.cs index 36b7946d8d..435b3f7252 100644 --- a/src/Avalonia.FreeDesktop/AtspiRoot.cs +++ b/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 _children = new List(); 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 { { "toolkit", "Avalonia" } }; } - ObjectPath IDBusObject.ObjectPath => RootPath; + public ObjectPath ObjectPath => RootPath; + public (string, ObjectPath) ApplicationPath => _accessibleProperties!.Parent; + public IDictionary Attributes { get; } public static AtspiRoot? RegisterRoot(Func 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 IAccessible.GetRoleNameAsync() => Task.FromResult("application"); Task IAccessible.GetLocalizedRoleNameAsync() => Task.FromResult("application"); Task IAccessible.GetStateAsync() => Task.FromResult(new uint[] { 0, 0 }); - Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(_application); - - Task> IAccessible.GetAttributesAsync() - { - return Task.FromResult>(new Dictionary - { - { "toolkit", "Avalonia" } - }); - } + Task<(string, ObjectPath)> IAccessible.GetApplicationAsync() => Task.FromResult(ApplicationPath); + Task> IAccessible.GetAttributesAsync() => Task.FromResult(Attributes); Task IApplication.GetLocaleAsync(uint lcType) => Task.FromResult(CultureInfo.CurrentCulture.Name); @@ -207,9 +221,14 @@ namespace Avalonia.FreeDesktop private class Child { private readonly Func _peerGetter; - private AutomationPeer? _peer; public Child(Func peerGetter) => _peerGetter = peerGetter; - public AutomationPeer Peer => _peer ??= _peerGetter(); + public AutomationPeer? Peer { get; private set; } + + public void CreatePeer() + { + Dispatcher.UIThread.VerifyAccess(); + Peer = _peerGetter(); + } } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index d9661c76b3..38a215b9ef 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 89a6e3ec49..3ff31879eb 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/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().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) - .Bind().ToConstant(s_instance) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().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 diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ae949f371f..dadaa1ad12 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -24,8 +24,10 @@ namespace Avalonia.Win32 /// /// Window implementation for Win32 platform. /// - public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, - ITopLevelImplWithNativeControlHost + public partial class WindowImpl : IWindowImpl, + EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost, + IPlatformAutomationPeerFactory { private static readonly List s_instances = new List(); @@ -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( diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 5b3c29e60a..f51ac1cccb 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/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, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index be8aa82a6b..77ae8e5336 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -50,7 +50,7 @@ namespace Avalonia.UnitTests { AvaloniaLocator.CurrentMutable .Bind().ToConstant(Services.AssetLoader) - .Bind().ToConstant(Services.AutomationPlatform) + .Bind().ToConstant(Services.AutomationPlatform) .Bind().ToConstant(Services.FocusManager) .Bind().ToConstant(Services.GlobalClock) .BindToSelf(this)