Browse Source

Implement AT-SPI2 Accessibility Backend (#20735)

* Implement AT-SPI2 Accessibility Backend

* Fix Avalonia.DBus submodule inclusion

* review comment fixes

* update dbus

* Refactor for explicit atspi accessibility node lifetime management.

* update dbus

* fix review comment

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/20765/head
Jumar Macato 4 weeks ago
committed by GitHub
parent
commit
00e5a24969
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .gitmodules
  2. 2
      Avalonia.Desktop.slnf
  3. 28
      Avalonia.sln
  4. 1
      external/Avalonia.DBus
  5. 72
      src/Avalonia.FreeDesktop.AtSpi/ApplicationAccessibleHandler.cs
  6. 28
      src/Avalonia.FreeDesktop.AtSpi/ApplicationAtSpiNode.cs
  7. 33
      src/Avalonia.FreeDesktop.AtSpi/ApplicationNodeApplicationHandler.cs
  8. 74
      src/Avalonia.FreeDesktop.AtSpi/AtSpiAccessibilityWatcher.cs
  9. 20
      src/Avalonia.FreeDesktop.AtSpi/AtSpiCacheHandler.cs
  10. 89
      src/Avalonia.FreeDesktop.AtSpi/AtSpiConstants.cs
  11. 8
      src/Avalonia.FreeDesktop.AtSpi/AtSpiCoordType.cs
  12. 107
      src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.RoleMapping.cs
  13. 111
      src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.StateMapping.cs
  14. 359
      src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs
  15. 159
      src/Avalonia.FreeDesktop.AtSpi/AtSpiRegistryEventTracker.cs
  16. 126
      src/Avalonia.FreeDesktop.AtSpi/AtSpiRole.cs
  17. 469
      src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs
  18. 52
      src/Avalonia.FreeDesktop.AtSpi/AtSpiState.cs
  19. 58
      src/Avalonia.FreeDesktop.AtSpi/Avalonia.FreeDesktop.AtSpi.csproj
  20. 876
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Accessible.xml
  21. 144
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Action.xml
  22. 86
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Application.xml
  23. 114
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Cache.xml
  24. 202
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Collection.xml
  25. 344
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Component.xml
  26. 142
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/DeviceEventController.xml
  27. 134
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/EditableText.xml
  28. 742
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Event.xml
  29. 86
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Image.xml
  30. 130
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Registry.xml
  31. 148
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Selection.xml
  32. 98
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Socket.xml
  33. 7
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Status.xml
  34. 688
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Text.xml
  35. 158
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Types.xml
  36. 47
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/Value.xml
  37. 9
      src/Avalonia.FreeDesktop.AtSpi/DBusXml/org.a11y.Bus.xml
  38. 152
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs
  39. 172
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiActionHandler.cs
  40. 272
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiCollectionHandler.cs
  41. 133
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiComponentHandler.cs
  42. 80
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiCoordinateHelper.cs
  43. 72
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEditableTextHandler.cs
  44. 64
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEventObjectHandler.cs
  45. 47
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEventWindowHandler.cs
  46. 48
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiImageHandler.cs
  47. 138
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs
  48. 296
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiTextHandler.cs
  49. 46
      src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiValueHandler.cs
  50. 88
      src/Avalonia.FreeDesktop.AtSpi/RootAtSpiNode.cs
  51. 1
      src/Avalonia.X11/Avalonia.X11.csproj
  52. 173
      src/Avalonia.X11/X11AtSpiAccessibility.cs
  53. 9
      src/Avalonia.X11/X11Platform.cs
  54. 19
      src/Avalonia.X11/X11Window.cs
  55. 28
      src/tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.csproj
  56. 6
      src/tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.props

3
.gitmodules

@ -4,3 +4,6 @@
[submodule "XamlX"]
path = external/XamlX
url = https://github.com/kekekeks/XamlX.git
[submodule "Avalonia.DBus"]
path = external/Avalonia.DBus
url = https://github.com/AvaloniaUI/Avalonia.DBus.git

2
Avalonia.Desktop.slnf

@ -24,6 +24,7 @@
"src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",
"src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj",
"src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj",
"src\\Avalonia.FreeDesktop.AtSpi\\Avalonia.FreeDesktop.AtSpi.csproj",
"src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj",
"src\\Avalonia.Metal\\Avalonia.Metal.csproj",
"src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj",
@ -46,6 +47,7 @@
"src\\tools\\Avalonia.Analyzers.CodeFixes.CSharp\\Avalonia.Analyzers.CodeFixes.CSharp.csproj",
"src\\tools\\Avalonia.Analyzers.CSharp\\Avalonia.Analyzers.CSharp.csproj",
"src\\tools\\Avalonia.Analyzers.VisualBasic\\Avalonia.Analyzers.VisualBasic.csproj",
"src\\tools\\Avalonia.DBus.Generators\\Avalonia.DBus.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",

28
Avalonia.sln

@ -283,6 +283,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.CodeFixe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.VisualBasic", "src\tools\Avalonia.Analyzers.VisualBasic\Avalonia.Analyzers.VisualBasic.csproj", "{A7644C3B-B843-44F1-9940-560D56CB0936}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop.AtSpi", "src\Avalonia.FreeDesktop.AtSpi\Avalonia.FreeDesktop.AtSpi.csproj", "{742C3613-514C-4D6B-804A-2A7925F278F3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DBus.Generators", "src\tools\Avalonia.DBus.Generators\Avalonia.DBus.Generators.csproj", "{98A16FFD-0C99-4665-AC64-DC17E86879A2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -661,14 +665,22 @@ Global
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.Build.0 = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.Build.0 = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.Build.0 = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.Build.0 = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.Build.0 = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.Build.0 = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.Build.0 = Release|Any CPU
{742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.Build.0 = Release|Any CPU
{98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -754,8 +766,10 @@ Global
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A7644C3B-B843-44F1-9940-560D56CB0936} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{FDFB9C25-552D-420B-9D4A-DB0BB6472239} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{A7644C3B-B843-44F1-9940-560D56CB0936} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{742C3613-514C-4D6B-804A-2A7925F278F3} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{98A16FFD-0C99-4665-AC64-DC17E86879A2} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
external/Avalonia.DBus

@ -0,0 +1 @@
Subproject commit f91a822c258476f185e51112388775591e6ef9d6

72
src/Avalonia.FreeDesktop.AtSpi/ApplicationAccessibleHandler.cs

@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
namespace Avalonia.FreeDesktop.AtSpi;
/// <summary>
/// <see cref="IOrgA11yAtspiAccessible"/> implementation for <see cref="ApplicationAtSpiNode"/>.
/// </summary>
internal sealed class ApplicationAccessibleHandler(AtSpiServer server, ApplicationAtSpiNode appNode)
: IOrgA11yAtspiAccessible
{
private static readonly List<string> s_interfaces =
[
AtSpiConstants.IfaceAccessible,
AtSpiConstants.IfaceApplication,
];
public uint Version => AtSpiConstants.AccessibleVersion;
public string Name => appNode.Name;
public string Description => string.Empty;
public AtSpiObjectReference Parent =>
new(string.Empty, new DBusObjectPath(AtSpiConstants.NullPath));
public int ChildCount => appNode.WindowChildren.Count;
public string Locale => AtSpiConstants.ResolveLocale();
public string AccessibleId => string.Empty;
public string HelpText => string.Empty;
public ValueTask<AtSpiObjectReference> GetChildAtIndexAsync(int index)
{
var children = appNode.WindowChildren;
if (index >= 0 && index < children.Count)
return ValueTask.FromResult(server.GetReference(children[index]));
return ValueTask.FromResult(server.GetNullReference());
}
public ValueTask<List<AtSpiObjectReference>> GetChildrenAsync()
{
var children = appNode.WindowChildren;
var refs = new List<AtSpiObjectReference>(children.Count);
foreach (var child in children)
refs.Add(server.GetReference(child));
return ValueTask.FromResult(refs);
}
public ValueTask<int> GetIndexInParentAsync() => ValueTask.FromResult(-1);
public ValueTask<List<AtSpiRelationEntry>> GetRelationSetAsync() =>
ValueTask.FromResult(new List<AtSpiRelationEntry>());
public ValueTask<uint> GetRoleAsync() => ValueTask.FromResult((uint)AtSpiRole.Application);
public ValueTask<string> GetRoleNameAsync() => ValueTask.FromResult("application");
public ValueTask<string> GetLocalizedRoleNameAsync() => ValueTask.FromResult("application");
public ValueTask<List<uint>> GetStateAsync() =>
ValueTask.FromResult(AtSpiConstants.BuildStateSet([AtSpiState.Active]));
public ValueTask<AtSpiAttributeSet> GetAttributesAsync() =>
ValueTask.FromResult(new AtSpiAttributeSet());
public ValueTask<AtSpiObjectReference> GetApplicationAsync() =>
ValueTask.FromResult(server.GetRootReference());
public ValueTask<List<string>> GetInterfacesAsync() =>
ValueTask.FromResult(s_interfaces.ToList());
}

28
src/Avalonia.FreeDesktop.AtSpi/ApplicationAtSpiNode.cs

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Avalonia.FreeDesktop.AtSpi.Handlers;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Synthetic application root node that is not backed by an <see cref="Avalonia.Automation.Peers.AutomationPeer"/>.
/// Registered at <c>/org/a11y/atspi/accessible/root</c> and serves as the AT-SPI tree root.
/// </summary>
internal sealed class ApplicationAtSpiNode(string? applicationName)
{
private readonly List<RootAtSpiNode> _windowChildren = [];
public string Path => RootPath;
public string Name { get; } = applicationName
?? Application.Current?.Name
?? Process.GetCurrentProcess().ProcessName;
public AtSpiRole Role => AtSpiRole.Application;
public List<RootAtSpiNode> WindowChildren => _windowChildren;
public void AddWindowChild(RootAtSpiNode windowNode) => _windowChildren.Add(windowNode);
public void RemoveWindowChild(RootAtSpiNode windowNode) => _windowChildren.Remove(windowNode);
}
}

33
src/Avalonia.FreeDesktop.AtSpi/ApplicationNodeApplicationHandler.cs

@ -0,0 +1,33 @@
using System.Threading.Tasks;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
namespace Avalonia.FreeDesktop.AtSpi;
/// <summary>
/// <see cref="IOrgA11yAtspiApplication"/> implementation for <see cref="ApplicationAtSpiNode"/>.
/// </summary>
internal sealed class ApplicationNodeApplicationHandler : IOrgA11yAtspiApplication
{
public ApplicationNodeApplicationHandler()
{
var version = AtSpiConstants.ResolveToolkitVersion();
ToolkitName = "Avalonia";
Version = version;
ToolkitVersion = version;
AtspiVersion = "2.1";
InterfaceVersion = AtSpiConstants.ApplicationVersion;
}
public string ToolkitName { get; }
public string Version { get; }
public string ToolkitVersion { get; }
public string AtspiVersion { get; }
public uint InterfaceVersion { get; }
public int Id { get; set; }
public ValueTask<string> GetLocaleAsync(uint lctype) =>
ValueTask.FromResult(AtSpiConstants.ResolveLocale());
public ValueTask<string> GetApplicationBusAddressAsync() =>
ValueTask.FromResult(string.Empty);
}

74
src/Avalonia.FreeDesktop.AtSpi/AtSpiAccessibilityWatcher.cs

@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using Avalonia.Logging;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Monitors the session bus to detect whether a screen reader is active.
/// </summary>
internal sealed class AtSpiAccessibilityWatcher : IAsyncDisposable
{
private DBusConnection? _sessionConnection;
private IDisposable? _propertiesWatcher;
public bool IsEnabled { get; private set; }
public event EventHandler<bool>? IsEnabledChanged;
public async Task InitAsync()
{
try
{
_sessionConnection = await DBusConnection.ConnectSessionAsync();
var proxy = new OrgA11yStatusProxy(
_sessionConnection, BusNameA11y, new DBusObjectPath(PathA11y));
try
{
var props = await proxy.GetAllPropertiesAsync();
IsEnabled = props.IsEnabled || props.ScreenReaderEnabled;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(this, "AT-SPI status properties query failed, defaulting to disabled: {Exception}", e);
IsEnabled = false;
}
_propertiesWatcher = await proxy.WatchPropertiesChangedAsync(
(changed, _, _) =>
{
var enabled = changed.IsEnabled || changed.ScreenReaderEnabled;
if (enabled == IsEnabled) return;
IsEnabled = enabled;
IsEnabledChanged?.Invoke(this, enabled);
},
sender: null,
emitOnCapturedContext: true);
}
catch (Exception e)
{
// D-Bus session bus unavailable or org.a11y.Bus not present.
// Silently degrade - accessibility remains disabled.
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(this, "AT-SPI watcher unavailable; accessibility remains disabled: {Exception}", e);
IsEnabled = false;
}
}
public async ValueTask DisposeAsync()
{
_propertiesWatcher?.Dispose();
_propertiesWatcher = null;
if (_sessionConnection is not null)
{
await _sessionConnection.DisposeAsync();
_sessionConnection = null;
}
}
}
}

20
src/Avalonia.FreeDesktop.AtSpi/AtSpiCacheHandler.cs

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Registers a dummy AT-SPI cache interface at /org/a11y/atspi/cache.
/// </summary>
internal sealed class AtSpiCacheHandler : IOrgA11yAtspiCache
{
public uint Version => CacheVersion;
public ValueTask<List<AtSpiAccessibleCacheItem>> GetItemsAsync()
{
return ValueTask.FromResult(new List<AtSpiAccessibleCacheItem>());
}
}
}

89
src/Avalonia.FreeDesktop.AtSpi/AtSpiConstants.cs

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Globalization;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Well-known AT-SPI2 D-Bus paths, interface names, and utility methods.
/// </summary>
internal static class AtSpiConstants
{
// D-Bus paths
internal const string RootPath = "/org/a11y/atspi/accessible/root";
internal const string CachePath = "/org/a11y/atspi/cache";
internal const string NullPath = "/org/a11y/atspi/null";
internal const string AppPathPrefix = "/net/avaloniaui/a11y";
internal const string RegistryPath = "/org/a11y/atspi/registry";
// Interface names
internal const string IfaceAccessible = "org.a11y.atspi.Accessible";
internal const string IfaceApplication = "org.a11y.atspi.Application";
internal const string IfaceComponent = "org.a11y.atspi.Component";
internal const string IfaceAction = "org.a11y.atspi.Action";
internal const string IfaceValue = "org.a11y.atspi.Value";
internal const string IfaceEventObject = "org.a11y.atspi.Event.Object";
internal const string IfaceEventWindow = "org.a11y.atspi.Event.Window";
internal const string IfaceCache = "org.a11y.atspi.Cache";
internal const string IfaceSelection = "org.a11y.atspi.Selection";
internal const string IfaceImage = "org.a11y.atspi.Image";
internal const string IfaceText = "org.a11y.atspi.Text";
internal const string IfaceEditableText = "org.a11y.atspi.EditableText";
internal const string IfaceCollection = "org.a11y.atspi.Collection";
// Bus names
internal const string BusNameRegistry = "org.a11y.atspi.Registry";
internal const string BusNameA11y = "org.a11y.Bus";
internal const string PathA11y = "/org/a11y/bus";
// Interface versions
internal const uint AccessibleVersion = 1;
internal const uint ApplicationVersion = 1;
internal const uint ComponentVersion = 1;
internal const uint ActionVersion = 1;
internal const uint ValueVersion = 1;
internal const uint EventObjectVersion = 1;
internal const uint EventWindowVersion = 1;
internal const uint CacheVersion = 1;
internal const uint ImageVersion = 1;
internal const uint SelectionVersion = 1;
internal const uint TextVersion = 1;
internal const uint EditableTextVersion = 1;
internal const uint CollectionVersion = 1;
internal const uint WidgetLayer = 3;
internal const uint WindowLayer = 7;
internal static List<uint> BuildStateSet(IReadOnlyCollection<AtSpiState>? states)
{
if (states == null || states.Count == 0)
return [0u, 0u];
uint low = 0;
uint high = 0;
foreach (var state in states)
{
var bit = (uint)state;
if (bit < 32)
low |= 1u << (int)bit;
else if (bit < 64)
high |= 1u << (int)(bit - 32);
}
return [low, high];
}
internal static string ResolveLocale()
{
var culture = CultureInfo.CurrentUICulture.Name;
if (string.IsNullOrWhiteSpace(culture))
culture = "en_US";
return culture.Replace('-', '_');
}
internal static string ResolveToolkitVersion()
{
// TODO: Better way of doing this?
return typeof(AtSpiConstants).Assembly.GetName().Version?.ToString() ?? "0";
}
}
}

8
src/Avalonia.FreeDesktop.AtSpi/AtSpiCoordType.cs

@ -0,0 +1,8 @@
namespace Avalonia.FreeDesktop.AtSpi;
internal enum AtSpiCoordType : uint
{
Screen = 0,
Window = 1,
Parent = 2,
}

107
src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.RoleMapping.cs

@ -0,0 +1,107 @@
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
namespace Avalonia.FreeDesktop.AtSpi
{
internal partial class AtSpiNode
{
public static AtSpiRole ToAtSpiRole(AutomationControlType controlType, AutomationPeer? peer = null)
{
return controlType switch
{
AutomationControlType.None => AtSpiRole.Panel,
AutomationControlType.Button => peer?.GetProvider<IToggleProvider>() is not null
? AtSpiRole.ToggleButton
: AtSpiRole.PushButton,
AutomationControlType.Calendar => AtSpiRole.Calendar,
AutomationControlType.CheckBox => AtSpiRole.CheckBox,
AutomationControlType.ComboBox => AtSpiRole.ComboBox,
AutomationControlType.ComboBoxItem => AtSpiRole.ListItem,
AutomationControlType.Edit => AtSpiRole.Entry,
AutomationControlType.Hyperlink => AtSpiRole.Link,
AutomationControlType.Image => AtSpiRole.Image,
AutomationControlType.ListItem => AtSpiRole.ListItem,
AutomationControlType.List => AtSpiRole.List,
AutomationControlType.Menu => AtSpiRole.Menu,
AutomationControlType.MenuBar => AtSpiRole.MenuBar,
AutomationControlType.MenuItem => AtSpiRole.MenuItem,
AutomationControlType.ProgressBar => AtSpiRole.ProgressBar,
AutomationControlType.RadioButton => AtSpiRole.RadioButton,
AutomationControlType.ScrollBar => AtSpiRole.ScrollBar,
AutomationControlType.Slider => AtSpiRole.Slider,
AutomationControlType.Spinner => AtSpiRole.SpinButton,
AutomationControlType.StatusBar => AtSpiRole.StatusBar,
AutomationControlType.Tab => AtSpiRole.PageTabList,
AutomationControlType.TabItem => AtSpiRole.PageTab,
AutomationControlType.Text => AtSpiRole.Label,
AutomationControlType.ToolBar => AtSpiRole.ToolBar,
AutomationControlType.ToolTip => AtSpiRole.ToolTip,
AutomationControlType.Tree => AtSpiRole.Tree,
AutomationControlType.TreeItem => AtSpiRole.TreeItem,
AutomationControlType.Custom => AtSpiRole.Unknown,
AutomationControlType.Group => AtSpiRole.Panel,
AutomationControlType.Thumb => AtSpiRole.PushButton,
AutomationControlType.DataGrid => AtSpiRole.TreeTable,
AutomationControlType.DataItem => AtSpiRole.TableCell,
AutomationControlType.Document => AtSpiRole.Document,
AutomationControlType.SplitButton => AtSpiRole.PushButton,
AutomationControlType.Window => AtSpiRole.Frame,
AutomationControlType.Pane => AtSpiRole.Panel,
AutomationControlType.Header => AtSpiRole.Header,
AutomationControlType.HeaderItem => AtSpiRole.ColumnHeader,
AutomationControlType.Table => AtSpiRole.Table,
AutomationControlType.TitleBar => AtSpiRole.TitleBar,
AutomationControlType.Separator => AtSpiRole.Separator,
AutomationControlType.Expander => AtSpiRole.Panel,
_ => AtSpiRole.Unknown,
};
}
public static string ToAtSpiRoleName(AtSpiRole role)
{
return role switch
{
AtSpiRole.Application => "application",
AtSpiRole.Frame => "frame",
AtSpiRole.PushButton => "push button",
AtSpiRole.ToggleButton => "toggle button",
AtSpiRole.CheckBox => "check box",
AtSpiRole.ComboBox => "combo box",
AtSpiRole.Entry => "entry",
AtSpiRole.Label => "label",
AtSpiRole.Image => "image",
AtSpiRole.List => "list",
AtSpiRole.ListItem => "list item",
AtSpiRole.Menu => "menu",
AtSpiRole.MenuBar => "menu bar",
AtSpiRole.MenuItem => "menu item",
AtSpiRole.ProgressBar => "progress bar",
AtSpiRole.RadioButton => "radio button",
AtSpiRole.ScrollBar => "scroll bar",
AtSpiRole.Slider => "slider",
AtSpiRole.SpinButton => "spin button",
AtSpiRole.StatusBar => "status bar",
AtSpiRole.PageTab => "page tab",
AtSpiRole.PageTabList => "page tab list",
AtSpiRole.ToolBar => "tool bar",
AtSpiRole.ToolTip => "tool tip",
AtSpiRole.Tree => "tree",
AtSpiRole.TreeItem => "tree item",
AtSpiRole.Panel => "panel",
AtSpiRole.Separator => "separator",
AtSpiRole.Table => "table",
AtSpiRole.TableCell => "table cell",
AtSpiRole.TreeTable => "tree table",
AtSpiRole.ColumnHeader => "column header",
AtSpiRole.Header => "header",
AtSpiRole.TitleBar => "title bar",
AtSpiRole.Document => "document frame",
AtSpiRole.Link => "link",
AtSpiRole.Calendar => "calendar",
AtSpiRole.Window => "window",
AtSpiRole.Unknown => "unknown",
_ => "unknown",
};
}
}
}

111
src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.StateMapping.cs

@ -0,0 +1,111 @@
using System.Collections.Generic;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
internal partial class AtSpiNode
{
public List<uint> ComputeStates()
{
return ComputeStatesCore();
}
private List<uint> ComputeStatesCore()
{
var states = new HashSet<AtSpiState>();
if (Peer.IsEnabled())
{
states.Add(AtSpiState.Enabled);
states.Add(AtSpiState.Sensitive);
}
if (!Peer.IsOffscreen())
{
states.Add(AtSpiState.Visible);
states.Add(AtSpiState.Showing);
}
if (Peer.IsKeyboardFocusable())
states.Add(AtSpiState.Focusable);
if (Peer.HasKeyboardFocus())
states.Add(AtSpiState.Focused);
// Toggle state
if (Peer.GetProvider<IToggleProvider>() is { } toggle)
{
states.Add(AtSpiState.Checkable);
switch (toggle.ToggleState)
{
case ToggleState.On:
states.Add(AtSpiState.Checked);
break;
case ToggleState.Indeterminate:
states.Add(AtSpiState.Indeterminate);
break;
case ToggleState.Off:
break;
}
}
// Expand/collapse state
if (Peer.GetProvider<IExpandCollapseProvider>() is { } expandCollapse)
{
states.Add(AtSpiState.Expandable);
switch (expandCollapse.ExpandCollapseState)
{
case ExpandCollapseState.Expanded:
states.Add(AtSpiState.Expanded);
break;
case ExpandCollapseState.Collapsed:
states.Add(AtSpiState.Collapsed);
break;
}
}
// Selection item states
if (Peer.GetProvider<ISelectionItemProvider>() is { } selectionItem)
{
states.Add(AtSpiState.Selectable);
if (selectionItem.IsSelected)
states.Add(AtSpiState.Selected);
}
// Multi-selectable container
if (Peer.GetProvider<ISelectionProvider>() is { CanSelectMultiple: true })
states.Add(AtSpiState.MultiSelectable);
// Value provider states (text editable/read-only)
if (Peer.GetProvider<IValueProvider>() is { } valueProvider)
{
if (valueProvider.IsReadOnly)
states.Add(AtSpiState.ReadOnly);
else
states.Add(AtSpiState.Editable);
}
// Range value read-only
if (Peer.GetProvider<IRangeValueProvider>() is { IsReadOnly: true })
states.Add(AtSpiState.ReadOnly);
// Required for form
if (Peer is ControlAutomationPeer controlPeer &&
AutomationProperties.GetIsRequiredForForm(controlPeer.Owner))
states.Add(AtSpiState.Required);
// Window-level active state and text entry states
var controlType = Peer.GetAutomationControlType();
if (controlType == AutomationControlType.Window)
states.Add(AtSpiState.Active);
if (controlType == AutomationControlType.Edit)
states.Add(AtSpiState.SingleLine);
return BuildStateSet(states);
}
}
}

359
src/Avalonia.FreeDesktop.AtSpi/AtSpiNode.cs

@ -0,0 +1,359 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.Handlers;
using Avalonia.Logging;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Represents an element in the AT-SPI tree, backed by an AutomationPeer.
/// </summary>
internal partial class AtSpiNode
{
private protected bool _detached;
private bool _attached;
private bool _childrenDirty = true;
private List<AtSpiNode> _attachedChildren = [];
private readonly string _path;
protected AtSpiNode(AutomationPeer peer, AtSpiServer server)
{
Peer = peer;
Server = server;
_path = server.AllocateNodePath();
}
public AutomationPeer Peer { get; }
public AtSpiServer Server { get; }
public string Path => _path;
internal bool IsAttached => _attached && !_detached;
internal AtSpiNode? Parent { get; private set; }
internal IReadOnlyList<AtSpiNode> AttachedChildren => _attachedChildren;
internal Task<IDisposable>? PathRegistrationTask { get; private set; }
public HashSet<string> GetSupportedInterfaces()
{
var interfaces = new HashSet<string>(StringComparer.Ordinal) { IfaceAccessible, IfaceComponent };
if (ApplicationHandler is not null) interfaces.Add(IfaceApplication);
if (ActionHandler is not null) interfaces.Add(IfaceAction);
if (ValueHandler is not null) interfaces.Add(IfaceValue);
if (SelectionHandler is not null) interfaces.Add(IfaceSelection);
if (TextHandler is not null) interfaces.Add(IfaceText);
if (EditableTextHandler is not null) interfaces.Add(IfaceEditableText);
if (ImageHandler is not null) interfaces.Add(IfaceImage);
return interfaces;
}
internal AtSpiAccessibleHandler? AccessibleHandler { get; private set; }
internal ApplicationNodeApplicationHandler? ApplicationHandler { get; private set; }
internal AtSpiComponentHandler? ComponentHandler { get; private set; }
internal AtSpiActionHandler? ActionHandler { get; private set; }
internal AtSpiValueHandler? ValueHandler { get; private set; }
internal AtSpiSelectionHandler? SelectionHandler { get; private set; }
internal AtSpiTextHandler? TextHandler { get; private set; }
internal AtSpiEditableTextHandler? EditableTextHandler { get; private set; }
internal AtSpiImageHandler? ImageHandler { get; private set; }
internal AtSpiEventObjectHandler? EventObjectHandler { get; private set; }
internal AtSpiEventWindowHandler? EventWindowHandler { get; private set; }
internal void BuildAndRegisterHandlers(
IDBusConnection connection,
SynchronizationContext? synchronizationContext = null)
{
var previousRegistrationTask = PathRegistrationTask;
var targets = new List<object>();
// Accessible - always present
targets.Add(AccessibleHandler = new AtSpiAccessibleHandler(Server, this));
if (Peer.GetProvider<IRootProvider>() is not null)
targets.Add(ApplicationHandler = new ApplicationNodeApplicationHandler());
// Component - all visual elements
targets.Add(ComponentHandler = new AtSpiComponentHandler(Server, this));
if (Peer.GetProvider<IInvokeProvider>() is not null ||
Peer.GetProvider<IToggleProvider>() is not null ||
Peer.GetProvider<IExpandCollapseProvider>() is not null ||
Peer.GetProvider<IScrollProvider>() is not null ||
Peer.GetProvider<ISelectionItemProvider>() is not null)
{
targets.Add(ActionHandler = new AtSpiActionHandler(Server, this));
}
if (Peer.GetProvider<IRangeValueProvider>() is not null)
targets.Add(ValueHandler = new AtSpiValueHandler(Server, this));
if (Peer.GetProvider<ISelectionProvider>() is not null)
targets.Add(SelectionHandler = new AtSpiSelectionHandler(Server, this));
if (Peer.GetProvider<IValueProvider>() is { } valueProvider
&& Peer.GetProvider<IRangeValueProvider>() is null)
{
targets.Add(TextHandler = new AtSpiTextHandler(this));
if (!valueProvider.IsReadOnly)
targets.Add(EditableTextHandler = new AtSpiEditableTextHandler(this));
}
if (Peer.GetAutomationControlType() == AutomationControlType.Image)
targets.Add(ImageHandler = new AtSpiImageHandler(Server, this));
// Event handlers - always present
targets.Add(EventObjectHandler = new AtSpiEventObjectHandler(Server, Path));
if (this is RootAtSpiNode)
targets.Add(EventWindowHandler = new AtSpiEventWindowHandler(Server, Path));
PathRegistrationTask = ReplacePathRegistrationAsync(
previousRegistrationTask,
connection,
targets,
synchronizationContext);
}
internal static AtSpiNode Create(AutomationPeer peer, AtSpiServer server)
{
return peer.GetProvider<IRootProvider>() is not null
? new RootAtSpiNode(peer, server)
: new AtSpiNode(peer, server);
}
internal static string GetAccessibleName(AutomationPeer peer)
{
var name = peer.GetName();
if (!string.IsNullOrWhiteSpace(name))
return name;
var visualTypeName = peer.GetClassName();
return string.IsNullOrWhiteSpace(visualTypeName) ? string.Empty : visualTypeName;
}
internal void Attach(AtSpiNode? parent)
{
if (_detached)
return;
if (_attached)
{
Parent = parent;
return;
}
_attached = true;
_childrenDirty = true;
Parent = parent;
Peer.ChildrenChanged += OnPeerChildrenChanged;
Peer.PropertyChanged += OnPeerPropertyChanged;
if (Server.A11yConnection is { } connection)
BuildAndRegisterHandlers(connection, Server.SyncContext);
}
internal void SetParent(AtSpiNode? parent) => Parent = parent;
internal bool RemoveAttachedChild(AtSpiNode child) => _attachedChildren.Remove(child);
internal IReadOnlyList<AtSpiNode> EnsureChildren()
{
if (!IsAttached)
return Array.Empty<AtSpiNode>();
if (!_childrenDirty)
return _attachedChildren;
var childPeers = Peer.GetChildren();
var nextChildren = new List<AtSpiNode>(childPeers.Count);
var nextChildrenSet = new HashSet<AtSpiNode>();
foreach (var childPeer in childPeers)
{
var childNode = Server.GetOrCreateNode(childPeer);
if (!Server.AttachNode(childNode, this))
continue;
nextChildren.Add(childNode);
nextChildrenSet.Add(childNode);
}
if (_attachedChildren.Count > 0)
{
var removed = _attachedChildren.Where(c => !nextChildrenSet.Contains(c)).ToArray();
foreach (var removedNode in removed)
{
if (ReferenceEquals(removedNode.Parent, this))
Server.DetachSubtreeRecursive(removedNode);
}
}
_attachedChildren = nextChildren;
_childrenDirty = false;
return _attachedChildren;
}
public virtual void Detach()
{
if (_detached)
return;
_detached = true;
_attached = false;
_childrenDirty = true;
_attachedChildren.Clear();
Parent = null;
Peer.ChildrenChanged -= OnPeerChildrenChanged;
Peer.PropertyChanged -= OnPeerPropertyChanged;
DisposePathRegistration();
}
internal async Task DisposePathRegistrationAsync()
{
var registrationTask = PathRegistrationTask;
PathRegistrationTask = null;
await DisposeRegistrationAsync(registrationTask).ConfigureAwait(false);
}
internal void DisposePathRegistration()
{
var registrationTask = PathRegistrationTask;
PathRegistrationTask = null;
if (registrationTask is null)
return;
if (registrationTask.IsCompletedSuccessfully)
{
registrationTask.Result.Dispose();
return;
}
_ = DisposeRegistrationAsync(registrationTask);
}
private async Task<IDisposable> ReplacePathRegistrationAsync(
Task<IDisposable>? previousRegistrationTask,
IDBusConnection connection,
IReadOnlyCollection<object> targets,
SynchronizationContext? synchronizationContext)
{
await DisposeRegistrationAsync(previousRegistrationTask).ConfigureAwait(false);
return await connection.RegisterObjects((DBusObjectPath)Path, targets, synchronizationContext)
.ConfigureAwait(false);
}
private static async Task DisposeRegistrationAsync(Task<IDisposable>? registrationTask)
{
if (registrationTask is null)
return;
try
{
var registration = await registrationTask.ConfigureAwait(false);
registration.Dispose();
}
catch (Exception e)
{
// Best-effort cleanup: path may have failed to register or connection may be gone.
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(null, "AT-SPI node path registration cleanup failed: {Exception}", e);
}
}
private void OnPeerChildrenChanged(object? sender, EventArgs e)
{
if (Server.A11yConnection is null || !IsAttached)
return;
_childrenDirty = true;
var childPeers = Peer.GetChildren();
if (_attachedChildren.Count > 0)
{
var currentPeers = new HashSet<AutomationPeer>(childPeers);
var removedChildren = _attachedChildren
.Where(childNode => !currentPeers.Contains(childNode.Peer))
.ToArray();
foreach (var oldChild in removedChildren)
{
if (ReferenceEquals(oldChild.Parent, this))
Server.DetachSubtreeRecursive(oldChild);
}
if (removedChildren.Length > 0)
{
var removedSet = new HashSet<AtSpiNode>(removedChildren);
_attachedChildren = _attachedChildren
.Where(childNode => !removedSet.Contains(childNode))
.ToList();
}
}
if (!Server.HasEventListeners || EventObjectHandler is not { } eventHandler) return;
var reference = Server.GetReference(this);
var childVariant = new DBusVariant(reference.ToDbusStruct());
eventHandler.EmitChildrenChangedSignal("add", 0, childVariant);
}
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (Server.A11yConnection is null || !Server.HasEventListeners)
return;
if (EventObjectHandler is not { } eventHandler)
return;
if (e.Property == AutomationElementIdentifiers.NameProperty)
{
eventHandler.EmitPropertyChangeSignal(
"accessible-name",
new DBusVariant(GetAccessibleName(Peer)));
}
else if (e.Property == AutomationElementIdentifiers.HelpTextProperty)
{
eventHandler.EmitPropertyChangeSignal(
"accessible-description",
new DBusVariant(e.NewValue?.ToString() ?? string.Empty));
}
else if (e.Property == TogglePatternIdentifiers.ToggleStateProperty)
{
var newState = e.NewValue is ToggleState ts ? ts : ToggleState.Off;
eventHandler.EmitStateChangedSignal(
"checked", newState == ToggleState.On ? 1 : 0, new DBusVariant(0));
eventHandler.EmitStateChangedSignal(
"indeterminate", newState == ToggleState.Indeterminate ? 1 : 0, new DBusVariant(0));
}
else if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty)
{
var newState = e.NewValue is ExpandCollapseState ecs ? ecs : ExpandCollapseState.Collapsed;
eventHandler.EmitStateChangedSignal(
"expanded", newState == ExpandCollapseState.Expanded ? 1 : 0, new DBusVariant(0));
eventHandler.EmitStateChangedSignal(
"collapsed", newState == ExpandCollapseState.Collapsed ? 1 : 0, new DBusVariant(0));
}
else if (e.Property == ValuePatternIdentifiers.ValueProperty)
{
eventHandler.EmitPropertyChangeSignal(
"accessible-value",
new DBusVariant(e.NewValue?.ToString() ?? string.Empty));
}
else if (e.Property == SelectionPatternIdentifiers.SelectionProperty)
{
eventHandler.EmitSelectionChangedSignal();
}
else if (e.Property == AutomationElementIdentifiers.BoundingRectangleProperty)
{
eventHandler.EmitBoundsChangedSignal();
}
}
}
}

159
src/Avalonia.FreeDesktop.AtSpi/AtSpiRegistryEventTracker.cs

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using Avalonia.Logging;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Monitors the AT-SPI registry to determine if any screen reader is listening for events.
/// </summary>
internal sealed class AtSpiRegistryEventTracker : IDisposable
{
private readonly DBusConnection _connection;
private readonly HashSet<string> _registeredEvents = new(StringComparer.Ordinal);
private OrgA11yAtspiRegistryProxy? _registryProxy;
private IDisposable? _registryRegisteredSubscription;
private IDisposable? _registryDeregisteredSubscription;
private IDisposable? _registryOwnerChangedSubscription;
private string? _registryUniqueName;
internal AtSpiRegistryEventTracker(DBusConnection connection)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
}
/// <summary>
/// Indicates whether any screen reader is currently listening for object events.
/// Defaults to true (chatty) until registry tracking confirms otherwise.
/// </summary>
internal bool HasEventListeners { get; private set; } = true;
internal async Task InitializeAsync()
{
try
{
_registryProxy ??= new OrgA11yAtspiRegistryProxy(
_connection, BusNameRegistry, new DBusObjectPath(RegistryPath));
// Seed from current registrations
var events = await _registryProxy.GetRegisteredEventsAsync();
_registeredEvents.Clear();
foreach (var registered in events)
_registeredEvents.Add(registered.EventName);
UpdateHasEventListeners();
// Resolve registry unique name and subscribe to signals
var registryOwner = await _connection.GetNameOwnerAsync(BusNameRegistry);
await SubscribeToRegistrySignalsAsync(registryOwner);
// Watch for registry daemon restarts
_registryOwnerChangedSubscription ??= await _connection.WatchNameOwnerChangedAsync(
(name, oldOwner, newOwner) =>
{
if (!string.Equals(name, BusNameRegistry, StringComparison.Ordinal))
return;
_ = SubscribeToRegistrySignalsAsync(newOwner);
},
emitOnCapturedContext: true);
}
catch (Exception e)
{
// Registry event tracking unavailable - remain chatty.
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(this, "AT-SPI registry event tracker unavailable; remaining chatty: {Exception}", e);
HasEventListeners = true;
}
}
public void Dispose()
{
_registryOwnerChangedSubscription?.Dispose();
_registryOwnerChangedSubscription = null;
_registryRegisteredSubscription?.Dispose();
_registryRegisteredSubscription = null;
_registryDeregisteredSubscription?.Dispose();
_registryDeregisteredSubscription = null;
_registryProxy = null;
_registryUniqueName = null;
_registeredEvents.Clear();
}
private async Task SubscribeToRegistrySignalsAsync(string? registryOwner)
{
if (string.Equals(_registryUniqueName, registryOwner, StringComparison.Ordinal))
return;
// Dispose old subscriptions
_registryRegisteredSubscription?.Dispose();
_registryRegisteredSubscription = null;
_registryDeregisteredSubscription?.Dispose();
_registryDeregisteredSubscription = null;
_registryUniqueName = registryOwner;
var senderFilter = string.IsNullOrWhiteSpace(registryOwner) ? null : registryOwner;
_registryProxy ??= new OrgA11yAtspiRegistryProxy(
_connection, BusNameRegistry, new DBusObjectPath(RegistryPath));
try
{
_registryRegisteredSubscription = await _registryProxy.WatchEventListenerRegisteredAsync(
OnRegistryEventListenerRegistered,
senderFilter,
emitOnCapturedContext: true);
_registryDeregisteredSubscription = await _registryProxy.WatchEventListenerDeregisteredAsync(
OnRegistryEventListenerDeregistered,
senderFilter,
emitOnCapturedContext: true);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(this, "AT-SPI registry signal subscription failed; remaining chatty: {Exception}", e);
_registryRegisteredSubscription?.Dispose();
_registryRegisteredSubscription = null;
_registryDeregisteredSubscription?.Dispose();
_registryDeregisteredSubscription = null;
HasEventListeners = true;
}
}
private void OnRegistryEventListenerRegistered(string bus, string @event, List<string> properties)
{
_registeredEvents.Add(@event);
UpdateHasEventListeners();
}
private void OnRegistryEventListenerDeregistered(string bus, string @event)
{
_registeredEvents.Remove(@event);
UpdateHasEventListeners();
}
private void UpdateHasEventListeners()
{
HasEventListeners = _registeredEvents.Any(IsObjectEventClass);
}
private static bool IsObjectEventClass(string eventName)
{
if (string.IsNullOrWhiteSpace(eventName))
return false;
if (eventName == "*")
return true;
return eventName.StartsWith("object:", StringComparison.OrdinalIgnoreCase)
|| eventName.StartsWith("window:", StringComparison.OrdinalIgnoreCase)
|| eventName.StartsWith("focus:", StringComparison.OrdinalIgnoreCase);
}
}
}

126
src/Avalonia.FreeDesktop.AtSpi/AtSpiRole.cs

@ -0,0 +1,126 @@
namespace Avalonia.FreeDesktop.AtSpi;
/// <summary>
/// AT-SPI2 role IDs
/// </summary>
internal enum AtSpiRole
{
Invalid = 0,
Accelerator = 1,
Alert = 2,
Animation = 3,
Arrow = 4,
Calendar = 5,
Canvas = 6,
CheckBox = 7,
CheckMenuItem = 8,
ColorChooser = 9,
ColumnHeader = 10,
ComboBox = 11,
DateEditor = 12,
DesktopIcon = 13,
DesktopFrame = 14,
Dial = 15,
Dialog = 16,
DirectoryPane = 17,
DrawingArea = 18,
FileChooser = 19,
Filler = 20,
FocusTraversable = 21,
Frame = 23,
GlassPane = 24,
HtmlContainer = 25,
Icon = 26,
Image = 27,
InternalFrame = 28,
Label = 29,
LayeredPane = 30,
List = 31,
ListItem = 32,
Menu = 33,
MenuBar = 34,
MenuItem = 35,
OptionPane = 36,
PageTab = 37,
PageTabList = 38,
Panel = 39,
PasswordText = 40,
PopupMenu = 41,
ProgressBar = 42,
PushButton = 43,
RadioButton = 44,
RadioMenuItem = 45,
RootPane = 46,
RowHeader = 47,
ScrollBar = 48,
ScrollPane = 49,
Separator = 50,
Slider = 51,
SpinButton = 52,
SplitPane = 53,
StatusBar = 54,
Table = 55,
TableCell = 56,
TableColumnHeader = 57,
TableRowHeader = 58,
TearoffMenuItem = 59,
Terminal = 60,
Text = 61,
ToggleButton = 62,
ToolBar = 63,
ToolTip = 64,
Tree = 65,
TreeTable = 66,
Unknown = 67,
Viewport = 68,
Window = 69,
Extended = 70,
Header = 71,
Footer = 72,
Paragraph = 73,
Ruler = 74,
Application = 75,
AutoComplete = 76,
EditBar = 77,
Embedded = 78,
Entry = 79,
Heading = 81,
Page = 82,
Document = 83,
Section = 84,
RedundantObject = 85,
Form = 86,
Link = 87,
InputMethodWindow = 88,
TableRow = 89,
TreeItem = 90,
DocumentSpreadsheet = 91,
DocumentPresentation = 92,
DocumentText = 93,
DocumentWeb = 94,
DocumentEmail = 95,
Comment = 96,
ListBox = 97,
Grouping = 98,
ImageMap = 99,
Notification = 100,
InfoBar = 101,
LevelBar = 102,
TitleBar = 103,
BlockQuote = 104,
Audio = 105,
Video = 106,
Definition = 107,
Article = 108,
Landmark = 109,
Log = 110,
Marquee = 111,
Math = 112,
Rating = 113,
Timer = 114,
Static = 116,
MathFraction = 117,
MathRoot = 118,
Subscript = 119,
Superscript = 120,
}

469
src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs

@ -0,0 +1,469 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using Avalonia.FreeDesktop.AtSpi.Handlers;
using Avalonia.Logging;
using Avalonia.Threading;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// Manages the AT-SPI D-Bus connection, node registration, and event emission.
/// </summary>
internal sealed class AtSpiServer : IAsyncDisposable
{
private readonly Dictionary<string, AtSpiNode> _nodesByPath = new(StringComparer.Ordinal);
private readonly Dictionary<AutomationPeer, AtSpiNode> _nodesByPeer = [];
private readonly object _embedSync = new();
private int _nextNodeId;
private DBusConnection? _a11yConnection;
private string _uniqueName = string.Empty;
private SynchronizationContext? _syncContext;
private ApplicationAtSpiNode? _appRoot;
private AtSpiCacheHandler? _cacheHandler;
private AtSpiEventObjectHandler? _appRootEventHandler;
private AtSpiRegistryEventTracker? _registryTracker;
private IDisposable? _appRootRegistration;
private IDisposable? _cacheRegistration;
private Task? _embedTask;
private bool _isEmbedded;
internal DBusConnection? A11yConnection => _a11yConnection;
internal SynchronizationContext? SyncContext => _syncContext;
internal string UniqueName => _uniqueName;
/// <summary>
/// Indicates whether any screen reader is currently listening for object events.
/// Defaults to true (chatty) until registry tracking confirms otherwise.
/// </summary>
internal bool HasEventListeners => _registryTracker?.HasEventListeners ?? true;
/// <summary>
/// Starts the AT-SPI server.
/// Must be called on the UI thread.
/// <remarks>
/// Call order in this method is important because
/// AT's are sensitive and/or broken.
/// </remarks>
/// </summary>
public async Task StartAsync()
{
lock (_embedSync)
{
_embedTask = null;
_isEmbedded = false;
}
_syncContext = new AvaloniaSynchronizationContext(DispatcherPriority.Normal);
var address = await GetAccessibilityBusAddressAsync();
if (string.IsNullOrWhiteSpace(address))
throw new InvalidOperationException("Failed to resolve the accessibility bus address.");
_a11yConnection = await DBusConnection.ConnectAsync(address);
_uniqueName = await _a11yConnection.GetUniqueNameAsync() ?? string.Empty;
_appRoot = new ApplicationAtSpiNode(null);
_cacheHandler = new AtSpiCacheHandler();
await BuildAndRegisterAppRootAsync();
await RegisterCachePathAsync();
_registryTracker = new AtSpiRegistryEventTracker(_a11yConnection);
_ = InitializeRegistryTrackerAsync(_registryTracker);
}
/// <summary>
/// Adds a window to the AT-SPI tree.
/// </summary>
public void AddWindow(AutomationPeer windowPeer)
{
if (_a11yConnection is null || _appRoot is null)
return;
// Idempotent check
if (TryGetAttachedNode(windowPeer) is RootAtSpiNode)
return;
if (GetOrCreateNode(windowPeer) is not RootAtSpiNode windowNode)
return;
windowNode.AppRoot = _appRoot;
if (!AttachNode(windowNode, parent: null))
return;
if (!_appRoot.WindowChildren.Contains(windowNode))
_appRoot.AddWindowChild(windowNode);
var isEmbedded = false;
lock (_embedSync)
isEmbedded = _isEmbedded;
// GTK-like root registration behavior:
// embed once, then only emit incremental children-changed for later windows.
if (isEmbedded)
{
EmitWindowChildAdded(windowNode);
}
else
{
// Embed may already be in flight from StartAsync. The embed completion path
// emits children-changed for all currently tracked windows.
_ = EnsureEmbeddedAndAnnounceAsync();
}
}
private Task EnsureEmbeddedAndAnnounceAsync()
{
lock (_embedSync)
{
if (_isEmbedded)
return Task.CompletedTask;
// Ignore repeated embed requests while one is already in flight.
if (_embedTask is { IsCompleted: false })
return _embedTask;
_embedTask = EmbedAndAnnounceOnceAsync();
return _embedTask;
}
}
private async Task EmbedAndAnnounceOnceAsync()
{
try
{
await EmbedApplicationAsync();
}
catch (Exception e)
{
// Embed failed - screen reader won't discover us.
// Reset so the next AddWindow retries.
Logger.TryGet(LogEventLevel.Warning, LogArea.FreeDesktopPlatform)?
.Log(this, "AT-SPI embed failed; will retry when windows are added: {Exception}", e);
lock (_embedSync)
_embedTask = null;
return;
}
lock (_embedSync)
{
_isEmbedded = true;
_embedTask = null;
}
// Now that the screen reader knows about us, emit children-changed
// for every window that was added before the embed completed.
if (!HasEventListeners || _appRootEventHandler is not { } eventHandler || _appRoot is null)
return;
var children = _appRoot.WindowChildren.ToArray();
for (var i = 0; i < children.Length; i++)
{
var childRef = GetReference(children[i]);
var childVariant = new DBusVariant(childRef.ToDbusStruct());
eventHandler.EmitChildrenChangedSignal("add", i, childVariant);
}
}
private void EmitWindowChildAdded(RootAtSpiNode windowNode)
{
if (!HasEventListeners || _appRootEventHandler is not { } eventHandler) return;
var childRef = GetReference(windowNode);
var childVariant = new DBusVariant(childRef.ToDbusStruct());
eventHandler.EmitChildrenChangedSignal(
"add", _appRoot!.WindowChildren.Count - 1, childVariant);
}
/// <summary>
/// Removes a window from the AT-SPI tree.
/// </summary>
public void RemoveWindow(AutomationPeer windowPeer)
{
if (_a11yConnection is null || _appRoot is null)
return;
var windowNode = TryGetAttachedNode(windowPeer) as RootAtSpiNode;
if (windowNode is null)
return;
// Emit children-changed("remove") on app root before removal (guarded by event listeners)
if (HasEventListeners && _appRootEventHandler is { } eventHandler)
{
var index = _appRoot.WindowChildren.IndexOf(windowNode);
var childRef = GetReference(windowNode);
var childVariant = new DBusVariant(childRef.ToDbusStruct());
eventHandler.EmitChildrenChangedSignal("remove", index, childVariant);
}
DetachSubtreeRecursive(windowNode);
}
public async ValueTask DisposeAsync()
{
lock (_embedSync)
{
_isEmbedded = false;
_embedTask = null;
}
_registryTracker?.Dispose();
_registryTracker = null;
var nodes = _nodesByPath.Values.ToArray();
// Dispose all D-Bus registrations before the connection.
foreach (var node in nodes)
{
await node.DisposePathRegistrationAsync();
}
_appRootRegistration?.Dispose();
_appRootRegistration = null;
_cacheRegistration?.Dispose();
_cacheRegistration = null;
if (_a11yConnection is not null)
{
await _a11yConnection.DisposeAsync();
_a11yConnection = null;
}
foreach (var node in nodes)
ReleaseNode(node);
_uniqueName = string.Empty;
_cacheHandler = null;
_appRoot = null;
_appRootEventHandler = null;
_nodesByPath.Clear();
_nodesByPeer.Clear();
}
internal AtSpiObjectReference GetReference(AtSpiNode? node)
{
if (node is null || !node.IsAttached || !_nodesByPath.ContainsKey(node.Path))
return GetNullReference();
return new AtSpiObjectReference(_uniqueName, new DBusObjectPath(node.Path));
}
internal AtSpiObjectReference GetNullReference()
{
return new AtSpiObjectReference(string.Empty, new DBusObjectPath(NullPath));
}
internal AtSpiObjectReference GetRootReference()
{
return new AtSpiObjectReference(_uniqueName, new DBusObjectPath(RootPath));
}
internal void EmitWindowActivationChange(RootAtSpiNode windowNode, bool active)
{
if (_a11yConnection is null || !HasEventListeners)
return;
if (windowNode.EventObjectHandler is { } eventHandler)
eventHandler.EmitStateChangedSignal("active", active ? 1 : 0, new DBusVariant(0));
if (windowNode.EventWindowHandler is not { } windowHandler)
return;
if (active)
windowHandler.EmitActivateSignal();
else
windowHandler.EmitDeactivateSignal();
}
internal void EmitFocusChange(AtSpiNode? focusedNode)
{
if (_a11yConnection is null || focusedNode is null || !HasEventListeners)
return;
if (focusedNode.EventObjectHandler is { } eventHandler)
eventHandler.EmitStateChangedSignal("focused", 1, new DBusVariant(0));
}
internal string AllocateNodePath()
{
return $"{AppPathPrefix}/{Interlocked.Increment(ref _nextNodeId)}";
}
internal AtSpiNode GetOrCreateNode(AutomationPeer peer)
{
if (_nodesByPeer.TryGetValue(peer, out var node))
return node;
node = AtSpiNode.Create(peer, this);
_nodesByPeer[peer] = node;
return node;
}
internal AtSpiNode? TryGetNode(AutomationPeer? peer)
{
if (peer is null)
return null;
_nodesByPeer.TryGetValue(peer, out var node);
return node;
}
internal AtSpiNode? TryGetAttachedNode(AutomationPeer? peer)
{
var node = TryGetNode(peer);
return node is { IsAttached: true } && _nodesByPath.ContainsKey(node.Path) ? node : null;
}
internal bool AttachNode(AtSpiNode node, AtSpiNode? parent)
{
if (_a11yConnection is null)
return false;
if (parent is not null && !parent.IsAttached)
return false;
if (node.IsAttached)
{
if (!ReferenceEquals(node.Parent, parent))
{
node.Parent?.RemoveAttachedChild(node);
node.SetParent(parent);
}
node.BuildAndRegisterHandlers(_a11yConnection, _syncContext);
_nodesByPath[node.Path] = node;
return true;
}
node.Attach(parent);
if (!node.IsAttached)
return false;
_nodesByPath[node.Path] = node;
return true;
}
internal void DetachSubtreeRecursive(AtSpiNode rootNode)
{
var toRemove = new List<AtSpiNode>();
CollectSubtree(rootNode, toRemove);
RemoveNodes(toRemove, emitDefunct: true);
}
private static void CollectSubtree(AtSpiNode node, List<AtSpiNode> result)
{
foreach (var child in node.AttachedChildren.ToArray())
CollectSubtree(child, result);
result.Add(node);
}
private void RemoveNodes(IEnumerable<AtSpiNode> nodes, bool emitDefunct)
{
foreach (var node in nodes)
{
if (!node.IsAttached || !_nodesByPath.ContainsKey(node.Path))
continue;
if (emitDefunct && HasEventListeners && node.EventObjectHandler is { } eventHandler)
eventHandler.EmitStateChangedSignal("defunct", 1, new DBusVariant("0"));
_nodesByPath.Remove(node.Path);
node.Parent?.RemoveAttachedChild(node);
if (node is RootAtSpiNode rootNode)
_appRoot?.RemoveWindowChild(rootNode);
ReleaseNode(node);
}
}
private async Task BuildAndRegisterAppRootAsync()
{
if (_a11yConnection is null || _appRoot is null)
return;
_appRootRegistration?.Dispose();
_appRootRegistration = null;
var accessibleHandler = new ApplicationAccessibleHandler(this, _appRoot);
var applicationHandler = new ApplicationNodeApplicationHandler();
var eventHandler = new AtSpiEventObjectHandler(this, _appRoot.Path);
_appRootEventHandler = eventHandler;
var targets = new List<object> { accessibleHandler, applicationHandler, eventHandler };
_appRootRegistration = await _a11yConnection.RegisterObjects(
(DBusObjectPath)RootPath,
targets,
_syncContext);
}
private async Task RegisterCachePathAsync()
{
if (_a11yConnection is null || _cacheHandler is null)
return;
_cacheRegistration?.Dispose();
_cacheRegistration = null;
_cacheRegistration = await _a11yConnection.RegisterObjects(
(DBusObjectPath)CachePath,
[_cacheHandler],
_syncContext);
}
private async Task<string> GetAccessibilityBusAddressAsync()
{
try
{
await using var connection = await DBusConnection.ConnectSessionAsync();
var proxy = new OrgA11yBusProxy(connection, BusNameA11y, new DBusObjectPath(PathA11y));
return await proxy.GetAddressAsync();
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(this, "Failed to resolve AT-SPI accessibility bus address: {Exception}", e);
return string.Empty;
}
}
private async Task EmbedApplicationAsync()
{
if (_a11yConnection is null)
return;
var proxy = new OrgA11yAtspiSocketProxy(_a11yConnection, BusNameRegistry, new DBusObjectPath(RootPath));
await proxy.EmbedAsync(new AtSpiObjectReference(_uniqueName, new DBusObjectPath(RootPath)));
}
private static async Task InitializeRegistryTrackerAsync(AtSpiRegistryEventTracker tracker)
{
try
{
await tracker.InitializeAsync();
}
catch (Exception e)
{
// Registry tracking is best-effort; AT-SPI server remains functional without it.
Logger.TryGet(LogEventLevel.Debug, LogArea.FreeDesktopPlatform)?
.Log(tracker, "AT-SPI registry listener tracking initialization failed: {Exception}", e);
}
}
private void ReleaseNode(AtSpiNode node)
{
node.Detach();
_nodesByPeer.Remove(node.Peer);
}
}
}

52
src/Avalonia.FreeDesktop.AtSpi/AtSpiState.cs

@ -0,0 +1,52 @@
namespace Avalonia.FreeDesktop.AtSpi;
/// <summary>
/// AT-SPI2 state IDs from atspi-constants.h.
/// </summary>
internal enum AtSpiState : uint
{
Invalid = 0,
Active = 1,
Armed = 2,
Busy = 3,
Checked = 4,
Collapsed = 5,
Defunct = 6,
Editable = 7,
Enabled = 8,
Expandable = 9,
Expanded = 10,
Focusable = 11,
Focused = 12,
HasToolTip = 13,
Horizontal = 14,
Iconified = 15,
Modal = 16,
MultiLine = 17,
MultiSelectable = 18,
Opaque = 19,
Pressed = 20,
Resizable = 21,
Selectable = 22,
Selected = 23,
Sensitive = 24,
Showing = 25,
SingleLine = 26,
Stale = 27,
Transient = 28,
Vertical = 29,
Visible = 30,
ManagesDescendants = 31,
Indeterminate = 32,
Required = 33,
Truncated = 34,
Animated = 35,
InvalidEntry = 36,
SupportsAutoCompletion = 37,
SelectableText = 38,
IsDefault = 39,
Visited = 40,
Checkable = 41,
HasPopup = 42,
ReadOnly = 43,
}

58
src/Avalonia.FreeDesktop.AtSpi/Avalonia.FreeDesktop.AtSpi.csproj

@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks)</TargetFrameworks>
<DefineConstants>$(DefineConstants);AVDBUS_INTERNAL</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<AvDBusInternal>true</AvDBusInternal>
<AvDBusSourcePath>../../external/Avalonia.DBus/src/Avalonia.DBus</AvDBusSourcePath>
</PropertyGroup>
<Import Project="../../build/NullableEnable.props" />
<Import Project="../../build/DevAnalyzers.props" />
<Import Project="../../build/TrimmingEnable.props" />
<Import Project="../tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.props" />
<ItemGroup>
<Compile Include="$(AvDBusSourcePath)/**/*.cs"
Exclude="$(AvDBusSourcePath)/obj/**/*.cs"
LinkBase="Avalonia.DBus" />
<Compile Include="../Avalonia.Controls/Utils/StringUtils.cs">
<Link>Shared/StringUtils.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../packages/Avalonia/Avalonia.csproj" />
<ProjectReference Include="../tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="DBusXml/Status.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.a11y.Bus.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/Socket.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/Accessible.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Application.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Component.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Action.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Value.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Event.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Cache.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Image.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Selection.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Text.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/EditableText.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/Collection.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/DeviceEventController.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/Registry.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/Types.xml" />
</ItemGroup>
</Project>

876
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Accessible.xml

@ -0,0 +1,876 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Accessible:
@short_description: Base interface which is implemented by all accessible objects.
-->
<interface name="org.a11y.atspi.Accessible">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
Name: Human-readable, localized, short name for the object.
Normally you need to set this for objects which do not have a labelled-by
relation. Consider a widget to select RGB colors by setting three sliders. The
names for the sliders would be "Red", "Green", "Blue", respectively, or
translations to application's locale. The names would be unnecessary if each
slider had a labelled-by relation to corresponding labels visible in the user
interface.
In general, something is missing from your application if an object that can be
interacted with has no Name or a labelled-by relation.
-->
<property name="Name" type="s" access="read"/>
<!--
Description: Human-readable, localized description of the object in more detail.
While the Name property is meant to be a short string that screen readers say
during normal navigation, the Description property is for when the user asks for
more detail.
-->
<property name="Description" type="s" access="read"/>
<!--
Parent: Accessible parent object of the current object.
The (so) is a string for the application name, and an object path.
Null parent: If the object has no parent (e.g. the application's root object is
being queried), return "" for the application name name and "/org/a11y/atspi/null"
for the object path.
Root object: An application must have a single root object, called
"/org/a11y/atspi/accessible/root". All other objects should have that one as
their highest-level ancestor.
-->
<property name="Parent" type="(so)" access="read">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</property>
<!--
ChildCount: number of accessible children for this object.
-->
<property name="ChildCount" type="i" access="read"/>
<!--
Locale: Unix locale for the current object.
For an application, this may be the locale for the language that the application
shows in its user interface.
For a document being shown in an application, or a paragraph within a document,
the locale may refer to that object exclusively. For example, an application may
be showing itself in English ("en"), but it may be used to display a document in
Spanish ("es"). In the latter case, a screen reader will want to know that it
should switch to Spanish while reading the document.
-->
<property name="Locale" type="s" access="read"/>
<!--
AccessibleId: application-specific identifier for the current object.
This can be used to give a special id to an object for reliable identification by
ATs or for use in tests, for example, "my_widget". Note that there is no way to
directly find an object by its id; ATs may have to recursively get the children to
find a specific id. This is because accessible objects can be created dynamically,
and they do not always correspond to a static view of an application's data.
-->
<property name="AccessibleId" type="s" access="read"/>
<!--
HelpText: help text for the current object.
-->
<property name="HelpText" type="s" access="read"/>
<!--
GetChildAtIndex:
@index: 0-based index of the child to query.
Queries the Nth accessible child of the current object. It is expected that this
will correspond to the order that the GetChildren method would return.
Returns: The (so) is a string for the application name, and an object path.
Notes: implementations vary in their behavior when the index is out of range.
GTK4 returns an error, while atk-adaptor returns the null object path
"/org/a11y/atspi/null". To keep the type system gods happy, you should probably
return a DBus error in that case.
-->
<method name="GetChildAtIndex">
<arg direction="in" name="index" type="i"/>
<arg direction="out" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetChildren:
Returns a list of the object's accessible children.
Each array element (so) is a string for the application name, and an object path.
An implementor may return an error if the object exposes a large
number of children such that returning a list would be prohibitive.
-->
<method name="GetChildren">
<arg direction="out" type="a(so)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiObjectReference"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetIndexInParent:
Returns the 0-based index at which the object would be returned by calling
GetChildren on its parent, or -1 if the object has no containing
parent or on exception.
-->
<method name="GetIndexInParent">
<arg direction="out" type="i"/>
</method>
<!--
GetRelationSet:
Returns a set of relationships between the current object and others. Each
element in the outermost array contains a number that indicates the relation type
(see below), and an array of references to accessible objects to which that
relationship applies. Each element in the inner array is a (so) with a string for
the application name, and an object path.
Each relationship between objects (possibly one-to-many or many-to-one) allows
better semantic identification of how objects are associated with one another.
For instance, the ATSPI_RELATION_LABELLED_BY relationship may be used to identify
labelling information that should accompany the accessible name property when
presenting an object's content or identity to the end user. Similarly,
ATSPI_RELATION_CONTROLLER_FOR can be used to further specify the context in which
a valuator is useful, and/or the other UI components which are directly effected
by user interactions with the valuator. Common examples include association of
scrollbars with the viewport or panel which they control.
Relation types - these are the enum values from AtspiRelationType in atspi-constants.h:
0 - ATSPI_RELATION_NULL: Not a meaningful relationship; clients should not
normally encounter this value.
1 - ATSPI_RELATION_LABEL_FOR: Object is a label for one or more other objects.
2 - ATSPI_RELATION_LABELLED_BY: Object is labelled by one or more other
objects.
3 - ATSPI_RELATION_CONTROLLER_FOR: Object is an interactive object which
modifies the state, onscreen location, or other attributes of one or more
target objects.
4 - ATSPI_RELATION_CONTROLLED_BY: Object state, position, etc. is
modified/controlled by user interaction with one or more other objects.
For instance a viewport or scroll pane may be ATSPI_RELATION_CONTROLLED_BY
scrollbars.
5 - ATSPI_RELATION_MEMBER_OF: Object has a grouping relationship (e.g. 'same
group as') to one or more other objects.
6 - ATSPI_RELATION_TOOLTIP_FOR: Object is a tooltip associated with another
object.
7 - ATSPI_RELATION_NODE_CHILD_OF: Object is a child of the target.
8 - ATSPI_RELATION_NODE_PARENT_OF: Object is a parent of the target.
9 - ATSPI_RELATION_EXTENDED: Used to indicate that a relationship exists, but
its type is not specified in the enumeration.
10 - ATSPI_RELATION_FLOWS_TO: Object renders content which flows logically to
another object. For instance, text in a paragraph may flow to another
object which is not the 'next sibling' in the accessibility hierarchy.
11 - ATSPI_RELATION_FLOWS_FROM: Reciprocal of ATSPI_RELATION_FLOWS_TO.
12 - ATSPI_RELATION_SUBWINDOW_OF: Object is visually and semantically considered
a subwindow of another object, even though it is not the object's child.
Useful when dealing with embedded applications and other cases where the
widget hierarchy does not map cleanly to the onscreen presentation.
13 - ATSPI_RELATION_EMBEDS: Similar to ATSPI_RELATION_SUBWINDOW_OF, but
specifically used for cross-process embedding.
14 - ATSPI_RELATION_EMBEDDED_BY: Reciprocal of ATSPI_RELATION_EMBEDS. Used to
denote content rendered by embedded renderers that live in a separate process
space from the embedding context.
15 - ATSPI_RELATION_POPUP_FOR: Denotes that the object is a transient window or
frame associated with another onscreen object. Similar to ATSPI_TOOLTIP_FOR,
but more general. Useful for windows which are technically toplevels
but which, for one or more reasons, do not explicitly cause their
associated window to lose 'window focus'. Creation of an ATSPI_ROLE_WINDOW
object with the ATSPI_RELATION_POPUP_FOR relation usually requires
some presentation action on the part of assistive technology clients,
even though the previous toplevel ATSPI_ROLE_FRAME object may still be
the active window.
16 - ATSPI_RELATION_PARENT_WINDOW_OF: This is the reciprocal relation to
ATSPI_RELATION_POPUP_FOR.
17 - ATSPI_RELATION_DESCRIPTION_FOR: Reciprocal of ATSPI_RELATION_DESCRIBED_BY.
Indicates that this object provides descriptive information about the target
object(s). See also ATSPI_RELATION_DETAILS_FOR and ATSPI_RELATION_ERROR_FOR.
18 - ATSPI_RELATION_DESCRIBED_BY: Reciprocal of ATSPI_RELATION_DESCRIPTION_FOR.
Indicates that one or more target objects provide descriptive information
about this object. This relation type is most appropriate for information
that is not essential as its presentation may be user-configurable and/or
limited to an on-demand mechanism such as an assistive technology command.
For brief, essential information such as can be found in a widget's on-screen
label, use ATSPI_RELATION_LABELLED_BY. For an on-screen error message, use
ATSPI_RELATION_ERROR_MESSAGE. For lengthy extended descriptive information
contained in an on-screen object, consider using ATSPI_RELATION_DETAILS as
assistive technologies may provide a means for the user to navigate to
objects containing detailed descriptions so that their content can be more
closely reviewed.
19 - ATSPI_RELATION_DETAILS: Reciprocal of ATSPI_RELATION_DETAILS_FOR. Indicates
that this object has a detailed or extended description, the contents of
which can be found in the target object(s). This relation type is most
appropriate for information that is sufficiently lengthy as to make
navigation to the container of that information desirable. For less verbose
information suitable for announcement only, see ATSPI_RELATION_DESCRIBED_BY.
If the detailed information describes an error condition,
ATSPI_RELATION_ERROR_FOR should be used instead. Since 2.26.
20 - ATSPI_RELATION_DETAILS_FOR: Reciprocal of ATSPI_RELATION_DETAILS. Indicates
that this object provides a detailed or extended description about the target
object(s). See also ATSPI_RELATION_DESCRIPTION_FOR and ATSPI_RELATION_ERROR_FOR.
Since 2.26.
21 - ATSPI_RELATION_ERROR_MESSAGE: Reciprocal of ATSPI_RELATION_ERROR_FOR.
Indicates that this object has one or more errors, the nature of which is
described in the contents of the target object(s). Objects that have this
relation type should also contain ATSPI_STATE_INVALID_ENTRY when their
GetState method is called. Since: 2.26.
22 - ATSPI_RELATION_ERROR_FOR: Reciprocal of ATSPI_RELATION_ERROR_MESSAGE.
Indicates that this object contains an error message describing an invalid
condition in the target object(s). Since: 2.26.
-->
<method name="GetRelationSet">
<arg direction="out" type="a(ua(so))">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiRelationEntry"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetRole:
Gets the accessible role that the current object represents. Roles make it
possible for various UI toolkits to expose their controls to assistive
technologies (ATs) with a standard interface, regardless of toolkit. For example,
a widget that acts like a conventional push button (appears unpressed; presses
when acted upon; invokes a certain action when pressed) can expose an
ATSPI_ROLE_BUTTON role.
Role values - these are the enum values from AtspiRole in atspi-constants.h:
- 0 - ATSPI_ROLE_INVALID: A role indicating an error condition, such as
uninitialized Role data.
- 1 - ATSPI_ROLE_ACCELERATOR_LABEL: Object is a label indicating the keyboard
accelerators for the parent.
- 2 - ATSPI_ROLE_ALERT: Object is used to alert the user about something.
- 3 - ATSPI_ROLE_ANIMATION: Object contains a dynamic or moving image of some
kind.
- 4 - ATSPI_ROLE_ARROW: Object is a 2D directional indicator.
- 5 - ATSPI_ROLE_CALENDAR: Object contains one or more dates, usually arranged
into a 2D list.
- 6 - ATSPI_ROLE_CANVAS: Object that can be drawn into and is used to trap
events.
- 7 - ATSPI_ROLE_CHECK_BOX: A choice that can be checked or unchecked and
provides a separate indicator for the current state.
- 8 - ATSPI_ROLE_CHECK_MENU_ITEM: A menu item that behaves like a check box. See
ATSPI_ROLE_CHECK_BOX.
- 9 - ATSPI_ROLE_COLOR_CHOOSER: A specialized dialog that lets the user choose a
color.
- 10 - ATSPI_ROLE_COLUMN_HEADER: The header for a column of data.
- 11 - ATSPI_ROLE_COMBO_BOX: A list of choices the user can select from.
- 12 - ATSPI_ROLE_DATE_EDITOR: An object which allows entry of a date.
- 13 - ATSPI_ROLE_DESKTOP_ICON: An inconified internal frame within a DESKTOP_PANE.
- 14 - ATSPI_ROLE_DESKTOP_FRAME: A pane that supports internal frames and
iconified versions of those internal frames.
- 15 - ATSPI_ROLE_DIAL: An object that allows a value to be changed via rotating a
visual element, or which displays a value via such a rotating element.
- 16 - ATSPI_ROLE_DIALOG: A top level window with title bar and a border.
- 17 - ATSPI_ROLE_DIRECTORY_PANE: A pane that allows the user to navigate through
and select the contents of a directory.
- 18 - ATSPI_ROLE_DRAWING_AREA: An object used for drawing custom user interface
elements.
- 19 - ATSPI_ROLE_FILE_CHOOSER: A specialized dialog that displays the files in
the directory and lets the user select a file, browse a different
directory, or specify a filename.
- 20 - ATSPI_ROLE_FILLER: A object that fills up space in a user interface.
- 21 - ATSPI_ROLE_FOCUS_TRAVERSABLE: Don't use, reserved for future use.
- 22 - ATSPI_ROLE_FONT_CHOOSER: Allows selection of a display font.
- 23 - ATSPI_ROLE_FRAME: A top level window with a title bar, border, menubar,
etc.
- 24 - ATSPI_ROLE_GLASS_PANE: A pane that is guaranteed to be painted on top of
all panes beneath it.
- 25 - ATSPI_ROLE_HTML_CONTAINER: A document container for HTML, whose children
represent the document content.
- 26 - ATSPI_ROLE_ICON: A small fixed size picture, typically used to decorate
components.
- 27 - ATSPI_ROLE_IMAGE: An image, typically static.
- 28 - ATSPI_ROLE_INTERNAL_FRAME: A frame-like object that is clipped by a desktop
pane.
- 29 - ATSPI_ROLE_LABEL: An object used to present an icon or short string in an
interface.
- 30 - ATSPI_ROLE_LAYERED_PANE: A specialized pane that allows its children to be
drawn in layers, providing a form of stacking order.
- 31 - ATSPI_ROLE_LIST: An object that presents a list of objects to the user and
allows the user to select one or more of them.
- 32 - ATSPI_ROLE_LIST_ITEM: An object that represents an element of a list.
- 33 - ATSPI_ROLE_MENU: An object usually found inside a menu bar that contains a
list of actions the user can choose from.
- 34 - ATSPI_ROLE_MENU_BAR: An object usually drawn at the top of the primary
dialog box of an application that contains a list of menus the user can
x choose from.
- 35 - ATSPI_ROLE_MENU_ITEM: An object usually contained in a menu that presents
an action the user can choose.
- 36 - ATSPI_ROLE_OPTION_PANE: A specialized pane whose primary use is inside a
dialog.
- 37 - ATSPI_ROLE_PAGE_TAB: An object that is a child of a page tab list.
- 38 - ATSPI_ROLE_PAGE_TAB_LIST: An object that presents a series of panels (or
page tabs), one at a time,through some mechanism provided by the
object.
- 39 - ATSPI_ROLE_PANEL: A generic container that is often used to group objects.
- 40 - ATSPI_ROLE_PASSWORD_TEXT: A text object uses for passwords, or other places
where the text content is not shown visibly to the user.
- 41 - ATSPI_ROLE_POPUP_MENU: A temporary window that is usually used to offer the
user a list of choices, and then hides when the user selects one of those
choices.
- 42 - ATSPI_ROLE_PROGRESS_BAR: An object used to indicate how much of a task has
been completed.
- 43 - ATSPI_ROLE_BUTTON: An object the user can manipulate to tell the
application to do something.
- 44 - ATSPI_ROLE_RADIO_BUTTON: A specialized check box that will cause other
radio buttons in the same group to become unchecked when this one is
checked.
- 45 - ATSPI_ROLE_RADIO_MENU_ITEM: Object is both a menu item and a "radio button".
See ATSPI_ROLE_RADIO_BUTTON.
- 46 - ATSPI_ROLE_ROOT_PANE: A specialized pane that has a glass pane and a
layered pane as its children.
- 47 - ATSPI_ROLE_ROW_HEADER: The header for a row of data.
- 48 - ATSPI_ROLE_SCROLL_BAR: An object usually used to allow a user to
incrementally view a large amount of data by moving the bounds of a
viewport along a one-dimensional axis.
- 49 - ATSPI_ROLE_SCROLL_PANE: An object that allows a user to incrementally view
a large amount of information. Scroll pane objects are usually
accompanied by ATSPI_ROLE_SCROLL_BAR controllers, on which the
ATSPI_RELATION_CONTROLLER_FOR and ATSPI_RELATION_CONTROLLED_BY
reciprocal relations are set. See the GetRelationSet method.
- 50 - ATSPI_ROLE_SEPARATOR: An object usually contained in a menu to provide a
visible and logical separation of the contents in a menu.
- 51 - ATSPI_ROLE_SLIDER: An object that allows the user to select from a bounded range.
Unlike ATSPI_ROLE_SCROLL_BAR, ATSPI_ROLE_SLIDER objects need not control
'viewport'-like objects.
- 52 - ATSPI_ROLE_SPIN_BUTTON: An object which allows one of a set of choices to
be selected, and which displays the current choice.
- 53 - ATSPI_ROLE_SPLIT_PANE: A specialized panel that presents two other panels
at the same time.
- 54 - ATSPI_ROLE_STATUS_BAR: Object displays non-quantitative status information
(c.f. ATSPI_ROLE_PROGRESS_BAR)
- 55 - ATSPI_ROLE_TABLE: An object used to represent information in terms of rows
and columns.
- 56 - ATSPI_ROLE_TABLE_CELL: A 'cell' or discrete child within a Table. Note:
Table cells need not have ATSPI_ROLE_TABLE_CELL, other
role values are valid as well.
- 57 - ATSPI_ROLE_TABLE_COLUMN_HEADER: An object which labels a particular column
in a Table interface interface.
- 58 - ATSPI_ROLE_TABLE_ROW_HEADER: An object which labels a particular row in a
Table interface. Table rows and columns may also be labelled via the
ATSPI_RELATION_LABEL_FOR/ATSPI_RELATION_LABELLED_BY relationships; see
the GetRelationSet method.
- 59 - ATSPI_ROLE_TEAROFF_MENU_ITEM: Object allows menu to be removed from menubar
and shown in its own window.
- 60 - ATSPI_ROLE_TERMINAL: An object that emulates a terminal.
- 61 - ATSPI_ROLE_TEXT: An interactive widget that supports multiple lines of text
and optionally accepts user input, but whose purpose is not to solicit user
input. Thus ATSPI_ROLE_TEXT is appropriate for the text view in a plain text
editor but inappropriate for an input field in a dialog box or web form. For
widgets whose purpose is to solicit input from the user, see ATSPI_ROLE_ENTRY
and ATSPI_ROLE_PASSWORD_TEXT. For generic objects which display a brief amount
of textual information, see ATSPI_ROLE_STATIC.
- 62 - ATSPI_ROLE_TOGGLE_BUTTON: A specialized push button that can be checked or
unchecked, but does not procide a separate indicator for the current
state.
- 63 - ATSPI_ROLE_TOOL_BAR: A bar or palette usually composed of push buttons or
toggle buttons.
- 64 - ATSPI_ROLE_TOOL_TIP: An object that provides information about another
object.
- 65 - ATSPI_ROLE_TREE: An object used to repsent hierarchical information to the
user.
- 66 - ATSPI_ROLE_TREE_TABLE: An object that presents both tabular and
hierarchical info to the user.
- 67 - ATSPI_ROLE_UNKNOWN: The object contains some accessible information,
but its role is not known.
- 68 - ATSPI_ROLE_VIEWPORT: An object usually used in a scroll pane, or to
otherwise clip a larger object or content renderer to a specific
onscreen viewport.
- 69 - ATSPI_ROLE_WINDOW: A top level window with no title or border.
- 70 - ATSPI_ROLE_EXTENDED: means that the role for this item is known, but not
included in the core enumeration. Deprecated since 2.24.
- 71 - ATSPI_ROLE_HEADER: An object that serves as a document header.
- 72 - ATSPI_ROLE_FOOTER: An object that serves as a document footer.
- 73 - ATSPI_ROLE_PARAGRAPH: An object which is contains a single paragraph of
text content. See also ATSPI_ROLE_TEXT.
- 74 - ATSPI_ROLE_RULER: An object which describes margins and tab stops, etc.
for text objects which it controls (should have
ATSPI_RELATION_CONTROLLER_FOR relation to such).
- 75 - ATSPI_ROLE_APPLICATION: An object corresponding to the toplevel accessible
of an application, which may contain ATSPI_ROLE_FRAME objects or other
accessible objects. Children of objects with the ATSPI_ROLE_DESKTOP_FRAME role
are generally ATSPI_ROLE_APPLICATION objects.
- 76 - ATSPI_ROLE_AUTOCOMPLETE: The object is a dialog or list containing items
for insertion into an entry widget, for instance a list of words for
completion of a text entry.
- 77 - ATSPI_ROLE_EDITBAR: The object is an editable text object in a toolbar.
- 78 - ATSPI_ROLE_EMBEDDED: The object is an embedded component container. This
role is a "grouping" hint that the contained objects share a context
which is different from the container in which this accessible is
embedded. In particular, it is used for some kinds of document embedding,
and for embedding of out-of-process component, "panel applets", etc.
- 79 - ATSPI_ROLE_ENTRY: The object is a component whose textual content may be
entered or modified by the user, provided ATSPI_STATE_EDITABLE is present.
A readonly ATSPI_ROLE_ENTRY object (i.e. where ATSPI_STATE_EDITABLE is
not present) implies a read-only 'text field' in a form, as opposed to a
title, label, or caption.
- 80 - ATSPI_ROLE_CHART: The object is a graphical depiction of quantitative data.
It may contain multiple subelements whose attributes and/or description
may be queried to obtain both the quantitative data and information about
how the data is being presented. The ATSPI_LABELLED_BY relation is
particularly important in interpreting objects of this type, as is the
accessible description property. See ATSPI_ROLE_CAPTION.
- 81 - ATSPI_ROLE_CAPTION: The object contains descriptive information, usually
textual, about another user interface element such as a table, chart, or
image.
- 82 - ATSPI_ROLE_DOCUMENT_FRAME: The object is a visual frame or container which
contains a view of document content. Document frames may occur within
another Document instance, in which case the second document may be
said to be embedded in the containing instance. HTML frames are often
ATSPI_ROLE_DOCUMENT_FRAME: Either this object, or a singleton descendant,
should implement the org.a11y.atspi.Document interface.
- 83 - ATSPI_ROLE_HEADING: The object serves as a heading for content which
follows it in a document. The 'heading level' of the heading, if
available, may be obtained by querying the object's attributes.
- 84 - ATSPI_ROLE_PAGE: The object is a containing instance which encapsulates a
page of information. ATSPI_ROLE_PAGE is used in documents and content which
support a paginated navigation model.
- 85 - ATSPI_ROLE_SECTION: The object is a containing instance of document content
which constitutes a particular 'logical' section of the document. The
type of content within a section, and the nature of the section division
itself, may be obtained by querying the object's attributes. Sections
may be nested.
- 86 - ATSPI_ROLE_REDUNDANT_OBJECT: The object is redundant with another object in
the hierarchy, and is exposed for purely technical reasons. Objects of
this role should be ignored by clients, if they are encountered at all.
- 87 - ATSPI_ROLE_FORM: The object is a containing instance of document content
which has within it components with which the user can interact in order
to input information; i.e. the object is a container for pushbuttons,
comboboxes, text input fields, and other 'GUI' components. ATSPI_ROLE_FORM
should not, in general, be used for toplevel GUI containers or dialogs,
but should be reserved for 'GUI' containers which occur within document
content, for instance within Web documents, presentations, or text
documents. Unlike other GUI containers and dialogs which occur inside
application instances, ATSPI_ROLE_FORM containers' components are
associated with the current document, rather than the current foreground
application or viewer instance.
- 88 - ATSPI_ROLE_LINK: The object is a hypertext anchor, i.e. a "link" in a
hypertext document. Such objects are distinct from 'inline' content
which may also use the Hypertext/Hyperlink interfaces to indicate
the range/location within a text object where an inline or embedded object
lies.
- 89 - ATSPI_ROLE_INPUT_METHOD_WINDOW: The object is a window or similar viewport
which is used to allow composition or input of a 'complex character',
in other words it is an "input method window".
- 90 - ATSPI_ROLE_TABLE_ROW: A row in a table.
- 91 - ATSPI_ROLE_TREE_ITEM: An object that represents an element of a tree.
- 92 - ATSPI_ROLE_DOCUMENT_SPREADSHEET: A document frame which contains a
spreadsheet.
- 93 - ATSPI_ROLE_DOCUMENT_PRESENTATION: A document frame which contains a
presentation or slide content.
- 94 - ATSPI_ROLE_DOCUMENT_TEXT: A document frame which contains textual content,
such as found in a word processing application.
- 95 - ATSPI_ROLE_DOCUMENT_WEB: A document frame which contains HTML or other
markup suitable for display in a web browser.
- 96 - ATSPI_ROLE_DOCUMENT_EMAIL: A document frame which contains email content
to be displayed or composed either in plain text or HTML.
- 97 - ATSPI_ROLE_COMMENT: An object found within a document and designed to
present a comment, note, or other annotation. In some cases, this object
might not be visible until activated.
- 98 - ATSPI_ROLE_LIST_BOX: A non-collapsible list of choices the user can select from.
- 99 - ATSPI_ROLE_GROUPING: A group of related widgets. This group typically has a label.
- 100 - ATSPI_ROLE_IMAGE_MAP: An image map object. Usually a graphic with multiple
hotspots, where each hotspot can be activated resulting in the loading of
another document or section of a document.
- 101 - ATSPI_ROLE_NOTIFICATION: A transitory object designed to present a
message to the user, typically at the desktop level rather than inside a
particular application.
- 102 - ATSPI_ROLE_INFO_BAR: An object designed to present a message to the user
within an existing window.
- 103 - ATSPI_ROLE_LEVEL_BAR: A bar that serves as a level indicator to, for
instance, show the strength of a password or the state of a battery. Since: 2.8
- 104 - ATSPI_ROLE_TITLE_BAR: A bar that serves as the title of a window or a
dialog. Since: 2.12
- 105 - ATSPI_ROLE_BLOCK_QUOTE: An object which contains a text section
that is quoted from another source. Since: 2.12
- 106 - ATSPI_ROLE_AUDIO: An object which represents an audio
element. Since: 2.12
- 107 - ATSPI_ROLE_VIDEO: An object which represents a video
element. Since: 2.12
- 108 - ATSPI_ROLE_DEFINITION: A definition of a term or concept. Since: 2.12
- 109 - ATSPI_ROLE_ARTICLE: A section of a page that consists of a
composition that forms an independent part of a document, page, or
site. Examples: A blog entry, a news story, a forum post. Since: 2.12
- 110 - ATSPI_ROLE_LANDMARK: A region of a web page intended as a
navigational landmark. This is designed to allow Assistive
Technologies to provide quick navigation among key regions within a
document. Since: 2.12
- 111 - ATSPI_ROLE_LOG: A text widget or container holding log content, such
as chat history and error logs. In this role there is a
relationship between the arrival of new items in the log and the
reading order. The log contains a meaningful sequence and new
information is added only to the end of the log, not at arbitrary
points. Since: 2.12
- 112 - ATSPI_ROLE_MARQUEE: A container where non-essential information
changes frequently. Common usages of marquee include stock tickers
and ad banners. The primary difference between a marquee and a log
is that logs usually have a meaningful order or sequence of
important content changes. Since: 2.12
- 113 - ATSPI_ROLE_MATH: A text widget or container that holds a mathematical
expression. Since: 2.12
- 114 - ATSPI_ROLE_RATING: A widget whose purpose is to display a rating,
such as the number of stars associated with a song in a media
player. Objects of this role should also implement
the Value interface. Since: 2.12
- 115 - ATSPI_ROLE_TIMER: An object containing a numerical counter which
indicates an amount of elapsed time from a start point, or the time
remaining until an end point. Since: 2.12
- 116 - ATSPI_ROLE_STATIC: A generic non-container object whose purpose is to display
a brief amount of information to the user and whose role is known by the
implementor but lacks semantic value for the user. Examples in which
ATSPI_ROLE_STATIC is appropriate include the message displayed in a message
box and an image used as an alternative means to display text.
ATSPI_ROLE_STATIC should not be applied to widgets which are traditionally
interactive, objects which display a significant amount of content, or any
object which has an accessible relation pointing to another object. The
displayed information, as a general rule, should be exposed through the
accessible name of the object. For labels which describe another widget, see
ATSPI_ROLE_LABEL. For text views, see ATSPI_ROLE_TEXT. For generic
containers, see ATSPI_ROLE_PANEL. For objects whose role is not known by the
implementor, see ATSPI_ROLE_UNKNOWN. Since: 2.16.
- 117 - ATSPI_ROLE_MATH_FRACTION: An object that represents a mathematical fraction. Since: 2.16.
- 118 - ATSPI_ROLE_MATH_ROOT: An object that represents a mathematical expression
displayed with a radical. Since: 2.16.
- 119 - ATSPI_ROLE_SUBSCRIPT: An object that contains text that is displayed as a
subscript. Since: 2.16.
- 120 - ATSPI_ROLE_SUPERSCRIPT: An object that contains text that is displayed as a
superscript. Since: 2.16.
- 121 - ATSPI_ROLE_DESCRIPTION_LIST: An object that represents a list of term-value
groups. A term-value group represents an individual description and consist
of one or more names (ATSPI_ROLE_DESCRIPTION_TERM) followed by one or more
values (ATSPI_ROLE_DESCRIPTION_VALUE). For each list, there should not be
more than one group with the same term name. Since: 2.26.
- 122 - ATSPI_ROLE_DESCRIPTION_TERM: An object that represents a term or phrase
with a corresponding definition. Since: 2.26.
- 123 - ATSPI_ROLE_DESCRIPTION_VALUE: An object that represents the description,
definition, or value of a term. Since: 2.26.
- 124 - ATSPI_ROLE_FOOTNOTE: An object that contains the text of a footnote. Since: 2.26.
- 125 - ATSPI_ROLE_CONTENT_DELETION: Content previously deleted or proposed to be
deleted, e.g. in revision history or a content view providing suggestions
from reviewers. Since: 2.34.
- 126 - ATSPI_ROLE_CONTENT_INSERTION: Content previously inserted or proposed to be
inserted, e.g. in revision history or a content view providing suggestions
from reviewers. Since: 2.34.
- 127 - ATSPI_ROLE_MARK: A run of content that is marked or highlighted, such as for
reference purposes, or to call it out as having a special purpose. If the
marked content has an associated section in the document elaborating on the
reason for the mark, then an ATSPI_RELATION_DETAILS relation should be used on the mark
to point to that associated section. In addition, the reciprocal relation
ATSPI_RELATION_DETAILS_FOR should be used on the associated content section
to point back to the mark. See the GetRelationSet method. Since: 2.36.
- 128 - ATSPI_ROLE_SUGGESTION: A container for content that is called out as a
proposed change from the current version of the document, such as by a reviewer of
the content. An object with this role should include children with
ATSPI_ROLE_CONTENT_DELETION and/or ATSPI_ROLE_CONTENT_INSERTION, in any order, to
indicate what the actual change is. Since: 2.36
- 129 - ATSPI_ROLE_PUSH_BUTTON_MENU: A specialized push button to open a menu. Since: 2.46
- 130 - ATSPI_ROLE_SWITCH: A switch that can be toggled on/off. Since: 2.56
-->
<method name="GetRole">
<arg direction="out" type="u"/>
</method>
<!--
GetRoleName:
Gets a UTF-8 string corresponding to the name of the role played by an object.
This method will return useful values for roles that fall outside the
enumeration used in the GetRole method. Implementing this method is
optional, and it may be removed in a future version of the API.
Libatspi will only call id in the event of an unknown role.
-->
<method name="GetRoleName">
<arg direction="out" type="s"/>
</method>
<!--
GetLocalizedRoleName:
Gets a UTF-8 string corresponding to the name of the role played by an object, translated
to the current locale.
This method will return useful values for roles that fall outside the
enumeration used in the GetRole method. Implementing this method is
optional, and it may be removed in a future version of the API.
Libatspi will only call id in the event of an unknown role.
-->
<method name="GetLocalizedRoleName">
<arg direction="out" type="s"/>
</method>
<!--
GetState:
Gets the set of states currently held by an object.
Elements in the array are enumeration values from AtspiStateType in atspi-constants.h:
- 0 - ATSPI_STATE_INVALID: Indicates an invalid state - probably an error
condition.
- 1 - ATSPI_STATE_ACTIVE: Indicates a window is currently the active window, or
an object is the active subelement within a container or table. ATSPI_STATE_ACTIVE
should not be used for objects which have ATSPI_STATE_FOCUSABLE or
ATSPI_STATE_SELECTABLE: Those objects should use
ATSPI_STATE_FOCUSED and ATSPI_STATE_SELECTED respectively.
ATSPI_STATE_ACTIVE is a means to indicate that an object which is not
focusable and not selectable is the currently-active item within its
parent container.
- 2 - ATSPI_STATE_ARMED: Indicates that the object is armed.
- 3 - ATSPI_STATE_BUSY: Indicates the current object is busy, i.e. onscreen
representation is in the process of changing, or the object is
temporarily unavailable for interaction due to activity already in progress.
- 4 - ATSPI_STATE_CHECKED: Indicates this object is currently checked.
- 5 - ATSPI_STATE_COLLAPSED: Indicates this object is collapsed.
- 6 - ATSPI_STATE_DEFUNCT: Indicates that this object no longer has a valid
backing widget (for instance, if its peer object has been destroyed).
- 7 - ATSPI_STATE_EDITABLE: Indicates the user can change the contents of this
object.
- 8 - ATSPI_STATE_ENABLED: Indicates that this object is enabled, i.e. that it
currently reflects some application state. Objects that are "greyed out"
may lack this state, and may lack the ATSPI_STATE_SENSITIVE if direct
user interaction cannot cause them to acquire ATSPI_STATE_ENABLED.
See ATSPI_STATE_SENSITIVE.
- 9 - ATSPI_STATE_EXPANDABLE: Indicates this object allows progressive
disclosure of its children.
- 10 - ATSPI_STATE_EXPANDED: Indicates this object is expanded.
- 11 - ATSPI_STATE_FOCUSABLE: Indicates this object can accept keyboard focus,
which means all events resulting from typing on the keyboard will
normally be passed to it when it has focus.
- 12 - ATSPI_STATE_FOCUSED: Indicates this object currently has the keyboard
focus.
- 13 - ATSPI_STATE_HAS_TOOLTIP: Indicates that the object has an associated
tooltip.
- 14 - ATSPI_STATE_HORIZONTAL: Indicates the orientation of this object is
horizontal.
- 15 - ATSPI_STATE_ICONIFIED: Indicates this object is minimized and is
represented only by an icon.
- 16 - ATSPI_STATE_MODAL: Indicates something must be done with this object
before the user can interact with an object in a different window.
- 17 - ATSPI_STATE_MULTI_LINE: Indicates this (text) object can contain multiple
lines of text.
- 18 - ATSPI_STATE_MULTISELECTABLE: Indicates this object allows more than one of
its children to be selected at the same time, or in the case of text
objects, that the object supports non-contiguous text selections.
- 19 - ATSPI_STATE_OPAQUE: Indicates this object paints every pixel within its
rectangular region. It also indicates an alpha value of unity, if it
supports alpha blending.
- 20 - ATSPI_STATE_PRESSED: Indicates this object is currently pressed.
- 21 - ATSPI_STATE_RESIZABLE: Indicates the size of this object's size is not
fixed.
- 22 - ATSPI_STATE_SELECTABLE: Indicates this object is the child of an object
that allows its children to be selected and that this child is one of
those children that can be selected.
- 23 - ATSPI_STATE_SELECTED: Indicates this object is the child of an object that
allows its children to be selected and that this child is one of those
children that has been selected.
- 24 - ATSPI_STATE_SENSITIVE: Indicates this object is sensitive, e.g. to user
interaction. ATSPI_STATE_SENSITIVE usually accompanies.
ATSPI_STATE_ENABLED for user-actionable controls, but may be found in the
absence of ATSPI_STATE_ENABLED if the current visible state of the control
is "disconnected" from the application state. In such cases, direct user
interaction can often result in the object gaining ATSPI_STATE_SENSITIVE,
for instance if a user makes an explicit selection using an object whose
current state is ambiguous or undefined. See ATSPI_STATE_ENABLED,
ATSPI_STATE_INDETERMINATE.
- 25 - ATSPI_STATE_SHOWING: Indicates this object, the object's parent, the
object's parent's parent, and so on, are all 'shown' to the end-user,
i.e. subject to "exposure" if blocking or obscuring objects do not
interpose between this object and the top of the window stack.
- 26 - ATSPI_STATE_SINGLE_LINE: Indicates this (text) object can contain only a
single line of text.
- 27 - ATSPI_STATE_STALE: Indicates that the information returned for this object
may no longer be synchronized with the application state. This can occur
if the object has ATSPI_STATE_TRANSIENT, and can also occur towards the
end of the object peer's lifecycle.
- 28 - ATSPI_STATE_TRANSIENT: Indicates this object is transient.
- 29 - ATSPI_STATE_VERTICAL: Indicates the orientation of this object is vertical;
for example this state may appear on such objects as scrollbars, text
objects (with vertical text flow), separators, etc.
- 30 - ATSPI_STATE_VISIBLE: Indicates this object is visible, e.g. has been
explicitly marked for exposure to the user. ATSPI_STATE_VISIBLE is no
guarantee that the object is actually unobscured on the screen, only that
it is 'potentially' visible, barring obstruction, being scrolled or clipped
out of the field of view, or having an ancestor container that has not yet
made visible. A widget is potentially onscreen if it has both
ATSPI_STATE_VISIBLE and ATSPI_STATE_SHOWING. The absence of
ATSPI_STATE_VISIBLE and ATSPI_STATE_SHOWING is
semantically equivalent to saying that an object is 'hidden'.
- 31 - ATSPI_STATE_MANAGES_DESCENDANTS: Indicates that "active-descendant-changed"
event is sent when children become 'active' (i.e. are selected or
navigated to onscreen). Used to prevent need to enumerate all children
in very large containers, like tables. The presence of
ATSPI_STATE_MANAGES_DESCENDANTS is an indication to the client that the
children should not, and need not, be enumerated by the client.
Objects implementing this state are expected to provide relevant state
notifications to listening clients, for instance notifications of
visibility changes and activation of their contained child objects, without
the client having previously requested references to those children.
- 32 - ATSPI_STATE_INDETERMINATE: Indicates that a check box or other boolean
indicator is in a state other than checked or not checked. This
usually means that the boolean value reflected or controlled by the
object does not apply consistently to the entire current context.
For example, a checkbox for the "Bold" attribute of text may have
ATSPI_STATE_INDETERMINATE if the currently selected text contains a mixture
of weight attributes. In many cases interacting with a
ATSPI_STATE_INDETERMINATE object will cause the context's corresponding
boolean attribute to be homogenized, whereupon the object will lose
ATSPI_STATE_INDETERMINATE and a corresponding state-changed event will be
fired.
- 33 - ATSPI_STATE_REQUIRED: Indicates that user interaction with this object is
'required' from the user, for instance before completing the
processing of a form.
- 34 - ATSPI_STATE_TRUNCATED: Indicates that an object's onscreen content
is truncated, e.g. a text value in a spreadsheet cell.
- 35 - ATSPI_STATE_ANIMATED: Indicates this object's visual representation is
dynamic, not static. This state may be applied to an object during an
animated 'effect' and be removed from the object once its visual
representation becomes static. Some applications, notably content viewers,
may not be able to detect all kinds of animated content. Therefore the
absence of this state should not be taken as
definitive evidence that the object's visual representation is
static; this state is advisory.
- 36 - ATSPI_STATE_INVALID_ENTRY: This object has indicated an error condition
due to failure of input validation. For instance, a form control may
acquire this state in response to invalid or malformed user input.
- 37 - ATSPI_STATE_SUPPORTS_AUTOCOMPLETION: This state indicates that the object
in question implements some form of typeahead or
pre-selection behavior whereby entering the first character of one or more
sub-elements causes those elements to scroll into view or become
selected. Subsequent character input may narrow the selection further as
long as one or more sub-elements match the string. This state is normally
only useful and encountered on objects that implement AtspiSelection.
In some cases the typeahead behavior may result in full or partial
completion of the data in the input field, in which case
these input events may trigger text-changed events from the source.
- 38 - ATSPI_STATE_SELECTABLE_TEXT: This state indicates that the object in
question supports text selection. It should only be exposed on objects
which implement the AtspiText interface, in order to distinguish this state
from ATSPI_STATE_SELECTABLE, which infers that the object in question is a
selectable child of an object which implements AtspiSelection. While
similar, text selection and subelement selection are distinct operations.
- 39 - ATSPI_STATE_IS_DEFAULT: This state indicates that the object in question is
the 'default' interaction object in a dialog, i.e. the one that gets
activated if the user presses "Enter" when the dialog is initially
posted.
- 40 - ATSPI_STATE_VISITED: This state indicates that the object (typically a
hyperlink) has already been activated or invoked, with the result that
some backing data has been downloaded or rendered.
- 41 - ATSPI_STATE_CHECKABLE: Indicates this object has the potential to
be checked, such as a checkbox or toggle-able table cell. Since: 2.12
- 42 - ATSPI_STATE_HAS_POPUP: Indicates that the object has a popup
context menu or sub-level menu which may or may not be
showing. This means that activation renders conditional content.
Note that ordinary tooltips are not considered popups in this
context. Since: 2.12
- 43 - ATSPI_STATE_READ_ONLY: Indicates that an object which is ENABLED and
SENSITIVE has a value which can be read, but not modified, by the
user. Since: 2.16
-->
<method name="GetState">
<arg direction="out" type="au">
<av:TypeDefinition>
<av:List/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetAttributes:
Gets a list of name/value pairs of attributes or annotations for this object. For
typographic, textual, or textually-semantic attributes, see the Text.GetAttributes
method.
FIXME: is there a list of well-known attributes?
-->
<method name="GetAttributes">
<arg direction="out" type="a{ss}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiAttributeSet"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetApplication:
Returns a string for the application name, and an object path for the containing
application object.
-->
<method name="GetApplication">
<arg direction="out" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetInterfaces:
Returns an array of accessible interface names supported by the current object.
-->
<method name="GetInterfaces">
<arg direction="out" type="as"/>
</method>
</interface>
</node>

144
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Action.xml

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Action:
@short_description: Allows exploring and invoking the actions of a user-actionable UI component.
For example, a button may expose a "click" action; a popup menu may expose an "open"
action. Components which are not "passive" providers of UI information should
implement this interface, unless there is a more specialized interface for
interaction like org.a11y.atspi.Text or org.a11y.atspi.Value.
-->
<interface name="org.a11y.atspi.Action">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
NActions: returns the number of available actions.
By convention, if there is more than one action available, the first one is
considered the "default" action of the object.
-->
<property name="NActions" type="i" access="read"/>
<!--
GetDescription:
@index: 0-based index of the action to query.
Returns: The localized description for the action at the specified @index. For
example, a screen reader will read out this description when the user asks for
extra detail on an action. For example, "Clicks the button" for the "click"
action of a button.
-->
<method name="GetDescription">
<arg type="i" name="index" direction="in"/>
<arg type="s" direction="out"/>
</method>
<!--
GetName:
@index: 0-based index of the action to query.
Returns: Machine-readable name for the action at the specified @index.
-->
<method name="GetName">
<arg type="i" name="index" direction="in"/>
<arg type="s" direction="out"/>
</method>
<!--
GetLocalizedName:
@index: 0-based index of the action to query.
Returns: A short, localized name for the action at the specified @index. This is
what screen readers will read out during normal navigation. For example, "Click"
for a button.
-->
<method name="GetLocalizedName">
<arg type="i" name="index" direction="in"/>
<arg type="s" direction="out"/>
</method>
<!--
GetKeyBinding:
@index: 0-based index of the action to query.
Gets the keybinding which can be used to activate this action, if one
exists. The string returned should contain localized, human-readable,
key sequences as they would appear when displayed on screen. It must
be in the format "mnemonic;sequence;shortcut".
- The mnemonic key activates the object if it is presently enabled onscreen.
This typically corresponds to the underlined letter within the widget.
Example: "n" in a traditional "New..." menu item or the "a" in "Apply" for
a button.
- The sequence is the full list of keys which invoke the action even if the
relevant element is not currently shown on screen. For instance, for a menu
item the sequence is the keybindings used to open the parent menus before
invoking. The sequence string is colon-delimited. Example: "Alt+F:N" in a
traditional "New..." menu item.
- The shortcut, if it exists, will invoke the same action without showing
the component or its enclosing menus or dialogs. Example: "Ctrl+N" in a
traditional "New..." menu item.
Example: For a traditional "New..." menu item, the expected return value
would be: "N;Alt+F:N;Ctrl+N" for the English locale and "N;Alt+D:N;Strg+N"
for the German locale. If, hypothetically, this menu item lacked a mnemonic,
it would be represented by ";;Ctrl+N" and ";;Strg+N" respectively.
If there is no key binding for this action, return "".
-->
<method name="GetKeyBinding">
<arg type="i" name="index" direction="in"/>
<arg type="s" direction="out"/>
</method>
<!--
GetActions:
Returns: an array of (localized_name, localized description, keybinding) for the
actions that an object supports. See the GetKeyBinding method for a description
of that field's syntax.
This is equivalent to using the methods GetLocalizedName, GetDescription,
GetKeyBinding for each action, but with a single call and thus less DBus traffic.
By convention, if there is more than one action available, the first one is
considered the "default" action of the object.
-->
<method name="GetActions">
<arg direction="out" type="a(sss)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiAction"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
DoAction:
@index: 0-based index of the action to perform.
Performs the specified action on the object.
Returns: true on success, false otherwise.
-->
<method name="DoAction">
<arg direction="in" name="index" type="i"/>
<arg direction="out" type="b"/>
</method>
</interface>
</node>

86
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Application.xml

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Application:
@short_description: Interface that must be implemented by the root object of an application.
-->
<interface name="org.a11y.atspi.Application">
<!--
ToolkitName: name of the toolkit used to implement the application's user interface.
-->
<property name="ToolkitName" type="s" access="read"/>
<!--
Version: version of the toolkit used to implement the application's user interface.
Deprecated: Use org.a11y.atspi.Application:ToolkitVersion instead.
-->
<property name="Version" type="s" access="read">
<annotation name="org.freedesktop.DBus.Deprecated" value="true"/>
</property>
<!--
Version: version of the toolkit used to implement the application's user interface.
-->
<property name="ToolkitVersion" type="s" access="read"/>
<!--
AtspiVersion: You should return "2.1" here.
This was intended to be the version of the atspi interfaces
that the application supports, but atspi will probably move to
using versioned interface names instead. Just return "2.1" here.
-->
<property name="AtspiVersion" type="s" access="read"/>
<!--
InterfaceVersion: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="InterfaceVersion" type="u" access="read"/>
<!--
Id: set to an arbitrary numerical id when an application registers with the registry.
When a freshly-started application uses the
org.a11y.atspi.Socket.Embed method to register with the
accessibility registry, the registry will set a numerical id
on the application.
Per https://gitlab.gnome.org/GNOME/at-spi2-core/-/issues/82 it
may turn out that this id is not actually used subsequently.
This is a remnant of the time when registryd actually had to
make up identifiers for each application. With DBus, however,
it is the bus that assigns unique names to applications that
connect to it.
Your application or toolkit can remember the Id passed when
the accessibility registry sets this property, and return it
back when the property is read.
-->
<property name="Id" type="i" access="readwrite"/>
<!-- This method is not used.
See https://gitlab.gnome.org/GNOME/orca/-/issues/260
-->
<method name="GetLocale">
<arg direction="in" name="lctype" type="u"/>
<arg direction="out" type="s"/>
</method>
<!--
Get a P2P DBus address for further direct communication from the client.
This allows for communication without being proxied by the daemon.
-->
<method name="GetApplicationBusAddress">
<arg direction="out" type="s"/>
</method>
</interface>
</node>

114
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Cache.xml

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Cache:
@short_description: Interface to query accessible objects in bulk.
The application should expose this interface at the /org/a11y/atspi/cache object
path.
The org.a11y.atspi.Accessible interface has methods like GetChildren and
GetChildAtIndex, but these only transfer an object's DBus id. The caller has to
then query the object's properties individually. Transferring objects one by one and
then their properties produces a lot of traffic in the accessibility bus.
So, this Cache interface can be used to query objects in bulk. Assistive tech
should try to do a bulk query of all the objects in a new window with the GetItems
method, and then update them dynamically from the AddAccessible and RemoveAccessible
signals.
FIXME: Does GetItems only get called if an application implements
GetApplicationBusAddress? GTK4 doesn't implement that, but it implements GetItems -
does that ever get called?
-->
<interface name="org.a11y.atspi.Cache">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
GetItems:
Bulk query an application's accessible objects.
Returns: an array with one element for each available object. Each element's
fields are like this:
- (so): accessible object reference - DBus name and object
path. The rest of the fields refer to this main object.
- (so): application reference - DBus name and object path. This is the owner of
the main object; the root path of the application that registered via
the Embed method of the org.a11y.atspi.Socket interface.
- (so): parent object reference - DBus name and object path.
If the main object has no parent:
- If it is a control, or a window, return the parent application.
- If the object has the application role, return a null reference. FIXME:
atk-adaptor/adaptors/cache-adaptor.c:append_cache_item() returns a
reference to the registry in this case (the one it obtained from the
initial Socket::Embed call); GTK4 returns a null reference.
- Otherwise, return a null reference ("" for the application name name and
"/org/a11y/atspi/null" for the object path).
- i: index in parent, or -1 for transient widgets/menu items. Equivalent to the
GetIndexInParent method of the org.a11y.atspi.Accessible interface.
- i: child count of main object, or -1 for defunct/menus. Equivalent to the
ChildCount property of the org.a11y.atspi.Accessible interface.
- as: array of names of the interfaces that the main object supports. Equivalent
to the GetInterfaces method of the org.a11y.atspi.Accessible interface.
- s: human-readable, localized, short name for the main object. Equivalent to the
Name property of the org.a11y.atspi.Accessible interface.
- u: role. Equivalent to the GetRole method of the org.a11y.atspi.Accessible interface.
- s: human-readable, localized description of the object in more detail.
Equivalent to the Description property of the org.a11y.atspi.Accessible interface.
- au: Set of states currently held by an object. Equivalent to the GetState
method of the org.a11y.atspi.Accessible interface.
Deprecation note: The signature for the return value of this method changed in
2015, in commit b2c8c4c7. It used to be "a((so)(so)(so)a(so)assusau)". The
"a(so)" instead of "ii" is a list of references to child objects. The
implementation in atspi-misc.c can handle either version, although the intention
is to deprecate the code that handles the old version.
-->
<method name="GetItems">
<arg direction="out" name="nodes" type="a((so)(so)(so)iiassusau)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiAccessibleCacheItem"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
AddAccessible: to be emitted when a new object is added.
See the GetItems method for a description of the signature.
-->
<signal name="AddAccessible">
<arg name="nodeAdded" type="((so)(so)(so)iiassusau)">
<av:TypeDefinition>
<av:Struct Type="AtSpiAccessibleCacheItem"/>
</av:TypeDefinition>
</arg>
</signal>
<!--
RemoveAccessible: to be emitted when an object is no longer available.
@nodeRemoved: (so) string for the application name and object path.
-->
<signal name="RemoveAccessible">
<arg name="nodeRemoved" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
</node>

202
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Collection.xml

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Collection:
@short_description: An interface designed to allow accessibles which satisfy a set of
criteria to be returned.
-->
<interface name="org.a11y.atspi.Collection">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
GetMatches:
Return descendants which satisfy a set of criteria specified in the
match rule.
@rule: Criteria that objects to be returned must fulfill.
This structure is composed as follows:
- ai: States (see below for more how they are encoded)
- i: Match type used for matching states
- a{ss}: Attributes. See also the Accessible.GetAttributes method
- i: Match type used for matching attributes
- ai: Roles (see below for how they are encoded)
- i: Match type used for matching roles
- as: Interfaces, by name (like "Action" or "Text").
- i: Match type used for matching interfaces
- b: Whether to invert the criteria. If true, the match rule should
be denied (inverted); if false, it should not. For example, if
the match rule defines that a match is an object of ROLE_HEADING
which has STATE_FOCUSABLE and a click action, inverting it would
match all objects that are not of ROLE_HEADING, focusable and
clickable at the same time.
For all criteria, the value used for the match type corresponds to the
AtspiCollectionMatchType enum values.
The set of states and roles are bitsets encoded as an array of
multiple 32-bit integers.
A state/role with enum value N is set by setting the Nth bit in the bit
set.
The first integer in the array contains the lowest 0-31 bits, the next
integer bits 32-63, etc.
For example, enum value AT_SPI_ROLE_COMBOBOX has a value of 83. In order to
pass a set of roles in which only this role is set, an array of integers
representing (1 << 83) = 0x800000000000000000000 would be
passed: { 0, 0, 0x80000, 0 }
@sortby: The way objects should be sorted.
The values correspond to those defined in the AtspiCollectionSortOrder enum.
@count: Maximum number of objects to return, or 0 for no limit.
@traverse: Whether to return descendants from the accessible subtree
(if #TRUE) or only direct children (if #FALSE).
-->
<method name="GetMatches">
<arg direction="in" name="rule" type="(aiia{ss}iaiiasib)">
<av:TypeDefinition>
<av:Struct Type="AtSpiMatchRule"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="sortby" type="u"/>
<arg direction="in" name="count" type="i"/>
<arg direction="in" name="traverse" type="b"/>
<arg direction="out" type="a(so)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiObjectReference"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetMatchesTo:
@currentObject: The object at which to start searching.
@rule: Criteria that objects to be returned must fulfill.
@sortby: The way objects should be sorted.
@tree: Specifies restrictions on the objects to be traversed. Values
correspond to the AtspiCollectionTreeTraversalType enum:
- ATSPI_Collection_TREE_RESTRICT_CHILDREN (0): Restrict children tree traversal
- 1 (ATSPI_Collection_TREE_RESTRICT_SIBLING): Restrict sibling tree traversal
- 2 (ATSPI_Collection_TREE_INORDER): In-order tree traversal.
@limit_scope: If true, only descendants of @currentObject's parent
will be returned. Otherwise (if false), any accessible may be
returned if it would preceed @currentObject in a flattened
hierarchy.
@count: The maximum number of results to return, or 0 for no limit.
@traverse: Whether to traverse the accessible subtree (in case
of true) or only the direct children (on case of false).
Gets all Accessible objects from the collection, after
@currentObject, matching a given @rule.
Returns: All accessible objects matching the given match rule after
@currentObject.
-->
<method name="GetMatchesTo">
<arg direction="in" name="currentObject" type="o">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="rule" type="(aiia{ss}iaiiasib)">
<av:TypeDefinition>
<av:Struct Type="AtSpiMatchRule"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="sortby" type="u"/>
<arg direction="in" name="tree" type="u"/>
<arg direction="in" name="limit_scope" type="b"/>
<arg direction="in" name="count" type="i"/>
<arg direction="in" name="traverse" type="b"/>
<arg direction="out" type="a(so)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiObjectReference"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetMatchesFrom:
@currentObject: Upon reaching this object, searching should stop.
@rule: Criteria that objects to be returned must fulfill.
@sortby: The way objects should be sorted.
@tree: Specifies restrictions on the objects to be traversed. Values
correspond to the AtspiCollectionTreeTraversalType enum.
@count: The maximum number of results to return, or 0 for no limit.
@traverse: Whether to traverse the accessible subtree (if true)
or only the direct children (if false).
Gets all Accessible objects from the collection, before @currentObject,
matching a given @rule.
Returns: All accessible objects matching the given match rule after
@currentObject.
-->
<method name="GetMatchesFrom">
<arg direction="in" name="currentObject" type="o">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="rule" type="(aiia{ss}iaiiasib)">
<av:TypeDefinition>
<av:Struct Type="AtSpiMatchRule"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="sortby" type="u"/>
<arg direction="in" name="tree" type="u"/>
<arg direction="in" name="count" type="i"/>
<arg direction="in" name="traverse" type="b"/>
<arg direction="out" type="a(so)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiObjectReference"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetActiveDescendant:
Returns the active descendant of the given object. Not currently
implemented in libatspi.
-->
<method name="GetActiveDescendant">
<arg direction="out" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
</interface>
</node>

344
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Component.xml

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Component:
@short_description: Interface for GUI components like widgets or other visible elements.
-->
<interface name="org.a11y.atspi.Component">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
Contains:
@x: X coordinate of point.
@y: Y coordinate of point.
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Queries whether a point (x, y) is inside the component.
The @coord_type values are as follows, and correspond to AtkCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
-->
<method name="Contains">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" type="b"/>
</method>
<!--
GetAccessibleAtPoint:
@x: X coordinate of point.
@y: Y coordinate of point.
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Gets a reference to the accessible object that contains an (x, y) pair of
coordinates.
The @coord_type values are as follows, and correspond to AtkCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
Returns: A DBus name and object reference (so) for the sought object, or a null
object reference "/org/a11y/atspi/null" if there is no object at the specified
coordinates.
-->
<method name="GetAccessibleAtPoint">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetExtents:
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Queries the pixel extents of a component.
The @coord_type values are as follows, and correspond to AtkCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
Returns: a tuple (x, y, width, height) corresponding to the rectangle for the
component's extents.
-->
<method name="GetExtents">
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" type="(iiii)">
<av:TypeDefinition>
<av:Struct Type="AtSpiRect"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetPosition:
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Queries the upper-left position of a component.
The @coord_type values are as follows, and correspond to AtkCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
Returns: (x, y) coordinates of the component's upper-left corner.
-->
<method name="GetPosition">
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" name="x" type="i"/>
<arg direction="out" name="y" type="i"/>
</method>
<!--
GetSize:
Queries the pixel size of a component.
Returns: (width, height) of the component's rectangular area.
-->
<method name="GetSize">
<arg direction="out" name="width" type="i"/>
<arg direction="out" name="height" type="i"/>
</method>
<!--
GetLayer:
Queries the UI layer at which a component is rendered, which can help in
determining when components occlude one another.
The layer of a component indicates its relative stacking order with respect to the
onscreen visual representation of the UI. The layer index, in combination
with the component's extents, can be used to compute the visibility of
all or part of a component. This is important in programmatic determination of
region-of-interest for magnification, and in flat screen review models of the
screen, as well as for other uses. Objects residing in two of the
Layer categories support further z-ordering information, with
respect to their peers in the same layer: namely, WINDOW and
MDI. Relative stacking order for other objects within the same layer
is not available; the recommended heuristic is first child paints first. In other
words, assume that the first siblings in the child list are subject to being
overpainted by later siblings if their bounds intersect. The order of layers, from
bottom to top, is as follows:
- 0 - INVALID: Error condition.
- 1 - BACKGROUND: Reserved for the desktop background; this is the bottom-most
layer, over which everything else is painted.
- 2 - CANVAS: The 'background' layer for most content renderers and UI component containers.
- 3 - WIDGET: The layer in which the majority of ordinary 'foreground' widgets reside.
- 4 - MDI: A special layer between CANVAS and WIDGET, in which the 'pseudo-windows'
(e.g. the Multiple-Document Interface frames) reside. See the GetMDIZOrder
method.
- 5 - POPUP: Layer for popup window content, above WIDGET.
- 6 - OVERLAY: The topmost layer.
- 7 - WINDOW: The layer in which a toplevel window background usually resides.
-->
<method name="GetLayer">
<arg direction="out" type="u"/>
</method>
<!--
GetMDIZOrder:
Queries the Z stacking order of a component which is in the MDI or WINDOW layer,
per the GetLayer method. Bigger z-order numbers are nearer the top.
Returns: The z order of the component, or -1 if it is not in the MDI layer.
-->
<method name="GetMDIZOrder">
<arg direction="out" type="n"/>
</method>
<!--
GrabFocus:
Attempts to set the keyboard input focus to the component.
Returns: true if successful, or false otherwise.
-->
<method name="GrabFocus">
<arg direction="out" type="b"/>
</method>
<!--
GetAlpha:
Gets the opacity/alpha value of a component, if alpha blending is in use.
Returns: opacity value in the [0.0, 1.0] range. 0 is fully transparent and 1 is fully opaque.
-->
<method name="GetAlpha">
<arg direction="out" type="d"/>
</method>
<!--
SetExtents:
@x: the new horizontal position to which the component should be moved.
@y: the new vertical position to which the component should be moved.
@width: the width to which the component should be resized.
@height: the height to which the component should be resized.
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Moves and resizes the component.
The @coord_type values are as follows, and correspond to AtspiCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
Returns: true if successful, or false otherwise.
-->
<method name="SetExtents">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="width" type="i"/>
<arg direction="in" name="height" type="i"/>
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" type="b"/>
</method>
<!--
SetPosition:
@x: the new horizontal position to which the component should be moved.
@y: the new vertical position to which the component should be moved.
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
Moves the component to the specified position.
The @coord_type values are as follows, and correspond to AtkCoordType:
0 - Coordinates are relative to the screen.
1 - Coordinates are relative to the component's toplevel window.
2 - Coordinates are relative to the component's immediate parent.
Returns: true if successful, or false otherwise.
-->
<method name="SetPosition">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="coord_type" type="u"/>
<arg direction="out" type="b"/>
</method>
<!--
SetSize:
@width: the width to which the component should be resized.
@height: the height to which the component should be resized.
Resizes the component to the given pixel dimensions.
Returns: true if successful, or false otherwise.
-->
<method name="SetSize">
<arg direction="in" name="width" type="i"/>
<arg direction="in" name="height" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
ScrollTo:
@type: How to position the component within its parent; see the description.
Makes the component visible on the screen by scrolling all necessary parents.
The @type corresponds to AtkScrollType:
- 0 - TOP_LEFT: Scroll the object vertically and horizontally to bring
its top left corner to the top left corner of the window.
- 1 - BOTTOM_RIGHT: Scroll the object vertically and horizontally to
bring its bottom right corner to the bottom right corner of the window.
- 2 - TOP_EDGE: Scroll the object vertically to bring its top edge to
the top edge of the window.
- 3 - BOTTOM_EDGE: Scroll the object vertically to bring its bottom
edge to the bottom edge of the window.
- 4 - LEFT_EDGE: Scroll the object vertically and horizontally to bring
its left edge to the left edge of the window.
- 5 - RIGHT_EDGE: Scroll the object vertically and horizontally to
bring its right edge to the right edge of the window.
- 6 - ANYWHERE: Scroll the object vertically and horizontally so that
as much as possible of the object becomes visible. The exact placement is
determined by the application.
Returns: true if successful, or false otherwise.
-->
<method name="ScrollTo">
<arg direction="in" name="type" type="u"/>
<arg direction="out" type="b"/>
</method>
<!--
ScrollToPoint:
@coord_type: Whether the coordinates are relative to the screen or to the
component's top level window; see the description.
@x: X coordinate within the component to make visible.
@y: Y coordinate within the component to make visible.
Similar to the ScrollTo method, but makes a specific point from the component
visible in its parent.
The @coord_type values are as follows, and correspond to AtkCoordType:
- 0 - Coordinates are relative to the screen.
- 1 - Coordinates are relative to the component's toplevel window.
- 2 - Coordinates are relative to the component's immediate parent.
Returns: true if successful, or false otherwise.
-->
<method name="ScrollToPoint">
<arg direction="in" name="coord_type" type="u"/>
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="out" type="b"/>
</method>
</interface>
</node>

142
src/Avalonia.FreeDesktop.AtSpi/DBusXml/DeviceEventController.xml

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.DeviceEventController:
@short_description: Legacy interface for keystroke listeners and generation of keyboard/mouse events
This interface is being replaced by the functions in atspi-device-listener.h.
-->
<interface name="org.a11y.atspi.DeviceEventController">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
RegisterKeystrokeListener:
@listener: path of object to be notified when the following keys are pressed
@keys: array of (key_code, key_sym, key_string, unused)
@mask: modifier mask in X11 style (see Xlib.h)
@types: mask of press/release; see the description below.
@mode: struct of flags (synchronous, preemptive, global), see the description below.
The @types can be a mask of the following:
- KEY_PRESS = 1 << 0
- KEY_RELEASE = 1 << 1
Note that Orca always passes (KEY_PRESS | KEY_RELEASE).
The @mode is composed of three flags (see AtspiKeyListenerSyncType):
- synchronous: Events are delivered synchronously, before
the currently focused application sees them. If false,
events may be delivered asynchronously, which means in some
cases they may already have been delivered to the
application before the AT client receives the notification.
- preemptive: (called CANCONSUME in AtspiKeyListenerSyncType)
Events may be consumed by the AT client. Requires the synchronous flag to be set.
- global: (called ALL_WINDOWS in AtspiKeyListenerSyncType)
Events are received not from the application toolkit layer,
but from the device driver or windowing system subsystem.
Returns: boolean indicating whether the operation was successful. This is always
TRUE for non-global listeners (c.f. @mode), and may be FALSE for global listeners
if the underlying XGrabKey() failed (see spi_dec_x11_grab_key).
-->
<method name="RegisterKeystrokeListener">
<arg direction="in" name="listener" type="o"/>
<arg direction="in" name="keys" type="a(iisi)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiKeyDefinition"/>
</av:List>
</av:TypeDefinition>
</arg>
<arg direction="in" name="mask" type="u">
<av:TypeDefinition>
<av:BitFlags Type="AtSpiModifierMask"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="types" type="u">
<av:TypeDefinition>
<av:BitFlags Type="AtSpiEventTypeMask"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="mode" type="(bbb)">
<av:TypeDefinition>
<av:Struct Type="AtSpiEventMode"/>
</av:TypeDefinition>
</arg>
<arg direction="out" type="b"/>
</method>
<method name="DeregisterKeystrokeListener">
<arg direction="in" name="listener" type="o"/>
<arg direction="in" name="keys" type="a(iisi)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiKeyDefinition"/>
</av:List>
</av:TypeDefinition>
</arg>
<arg direction="in" name="mask" type="u">
<av:TypeDefinition>
<av:BitFlags Type="AtSpiModifierMask"/>
</av:TypeDefinition>
</arg>
<arg direction="in" name="type" type="u">
<av:TypeDefinition>
<av:BitFlags Type="AtSpiEventTypeMask"/>
</av:TypeDefinition>
</arg>
</method>
<method name="GetKeystrokeListeners">
<arg direction="out" type="a(souua(iisi)u(bbb))">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiKeystrokeListener"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<method name="GenerateKeyboardEvent">
<arg direction="in" name="keycode" type="i"/>
<arg direction="in" name="keystring" type="s"/>
<arg direction="in" name="type" type="u"/>
</method>
<method name="GenerateMouseEvent">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="eventName" type="s"/>
</method>
<method name="NotifyListenersSync">
<arg direction="in" name="event" type="(uiuuisb)">
<av:TypeDefinition>
<av:Struct Type="AtSpiDeviceEvent"/>
</av:TypeDefinition>
</arg>
<arg direction="out" type="b"/>
</method>
<method name="NotifyListenersAsync">
<arg direction="in" name="event" type="(uiuuisb)">
<av:TypeDefinition>
<av:Struct Type="AtSpiDeviceEvent"/>
</av:TypeDefinition>
</arg>
</method>
</interface>
</node>

134
src/Avalonia.FreeDesktop.AtSpi/DBusXml/EditableText.xml

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.EditableText:
@short_description: An interface that provides methods for modifying
textual content of components which support editing.
-->
<interface name="org.a11y.atspi.EditableText">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
SetTextContents:
@newContents: a character string, encoded in UTF-8, which is to
become the new text contents of the object.
Replace the entire text contents of the accessible with the given text.
Returns: True if successful, false otherwise.
-->
<method name="SetTextContents">
<arg direction="in" name="newContents" type="s"/>
<arg direction="out" type="b"/>
</method>
<!--
InsertText:
@position: an integer indicating the character offset at which to
insert the new text.
@text: a string representing the text to insert, in UTF-8 encoding.
@length: the number of characters of text to insert, in bytes. If the
byte count of text is less than or equal to length, the entire contents
of text will be inserted.
Inserts text into an #AtspiEditableText object.
As with all character offsets, the specified @position may not be the
same as the resulting byte offset, since the text is in a
variable-width encoding.
Returns: True if successful, false otherwise.
-->
<method name="InsertText">
<arg direction="in" name="position" type="i"/>
<arg direction="in" name="text" type="s"/>
<arg direction="in" name="length" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
CopyText:
@startPos: an integer indicating the starting character offset
of the text to copy.
@endPos: an integer indicating the offset of the first character
past the end of the text section to be copied.
Copies text from an accessible object into the system clipboard.
-->
<method name="CopyText">
<arg direction="in" name="startPos" type="i"/>
<arg direction="in" name="endPos" type="i"/>
</method>
<!--
CutText:
@startPos: an integer indicating the starting character offset
of the text to cut.
@endPos: an integer indicating the offset of the first character
past the end of the text section to be cut.
Deletes text from an accessible object, copying the excised portion
into the system clipboard.
Returns: True if successful, false otherwise.
-->
<method name="CutText">
<arg direction="in" name="startPos" type="i"/>
<arg direction="in" name="endPos" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
DeleteText:
@startPos: an integer indicating the starting character offset
of the text to delete.
@endPos: an integer indicating the offset of the first character
past the end of the text section to be deleted.
Deletes text from an accessible object, without copying the
excised portion into the system clipboard.
Returns: True if successful, false otherwise.
-->
<method name="DeleteText">
<arg direction="in" name="startPos" type="i"/>
<arg direction="in" name="endPos" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
PasteText:
@position: an integer indicating the character offset at which to
insert the new text.
Inserts text from the system clipboard into an accessible object.
As with all character offsets, the specified @position may not be the
same as the resulting byte offset, since the text is in a
variable-width encoding.
Returns: True if successful, false otherwise.
-->
<method name="PasteText">
<arg direction="in" name="position" type="i"/>
<arg direction="out" type="b"/>
</method>
</interface>
</node>

742
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Event.xml

@ -0,0 +1,742 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
These interfaces allow applications to send events to notify ATs of
changes that they should be aware of. For historical reasons, all
events include a string, two integers, and a variant. The meaning
of these values will depend on the specific event, and they may be
unused, in which case an empty string or a value of 0 should be
sent. In addition, a dictionary of strings/variants should be
sent. This is intended to allow applications to proactively send
additional properties that an AT might be interested in. This is
reserved for future use. Currently, this dictionary should be empty.
TODO: Need documentation for the signals themselves.
-->
<interface name="org.a11y.atspi.Event.Object">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<signal name="PropertyChange">
<arg name="property" type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg name="value" type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="BoundsChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v" name="bounds"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="LinkSelected">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="StateChanged">
<arg name="state" type="s"/>
<arg name="enabled" type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ChildrenChanged">
<arg name="operation" type="s"/>
<arg name="index_in_parent" type="i"/>
<arg type="i"/>
<arg name="child" type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="VisibleDataChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="SelectionChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ModelChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ActiveDescendantChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg name="child" type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Announcement">
<arg type="s"/>
<!-- politeness should be 1 (ATSPI_LIVE_POLITE) or 2
(ATSPI_LIVE_ASSERTIVE) -->
<arg type="i" name="politeness"/>
<arg type="i"/>
<!-- The variant should enclose a string with the text to be announced -->
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="AttributesChanged">
<!-- name specifies the name of the attribute that has changed, when available -->
<arg name="name" type="s"/>
<arg type="i"/>
<arg type="i"/>
<!-- The variant should include a string with the attribute's new value, when available -->
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<!-- Table events. TODO: move to Table interface? -->
<signal name="RowInserted">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="RowReordered">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="RowDeleted">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ColumnInserted">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ColumnReordered">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ColumnDeleted">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<!-- Text events. TODO: move to Text interface? -->
<signal name="TextBoundsChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="TextSelectionChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="TextChanged">
<arg name="detail" type="s"/>
<arg name="start_pos" type="i"/>
<arg name="length" type="i"/>
<arg name="text" type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="TextAttributesChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="TextCaretMoved">
<arg type="s"/>
<arg name="position" type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Window">
<signal name="PropertyChange">
<arg name="property" type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Minimize">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Maximize">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Restore">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Close">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Create">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Reparent">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="DesktopCreate">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="DesktopDestroy">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Destroy">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Activate">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Deactivate">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Raise">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Lower">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Move">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Resize">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Shade">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="uUshade">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Restyle">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Mouse">
<signal name="Abs">
<arg type="s"/>
<arg name="x" type="i"/>
<arg name="y" type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Rel">
<arg type="s"/>
<arg name="x" type="i"/>
<arg name="y" type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Button">
<arg name="detail" type="s"/>
<arg name="mouse_x" type="i"/>
<arg name="mouse_y" type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Keyboard">
<signal name="Modifiers">
<arg type="s"/>
<arg name="previous_modifiers" type="i"/>
<arg name="current_modifiers" type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Terminal">
<signal name="LineChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ColumncountChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="LinecountChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ApplicationChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="CharwidthChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Document">
<signal name="LoadComplete">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="Reload">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="LoadStopped">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="ContentChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="AttributesChanged">
<!-- name specifies the name of the attribute that has changed, when available -->
<arg name="name" type="s"/>
<arg type="i"/>
<arg type="i"/>
<!-- The variant should include a string with the attribute's new value, when available -->
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
<signal name="PageChanged">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
<interface name="org.a11y.atspi.Event.Focus">
<!-- Focus is deprecated in favor of StateChanged with focus passed as its first argument -->
<signal name="Focus">
<arg type="s"/>
<arg type="i"/>
<arg type="i"/>
<arg type="v"/>
<arg name="properties" type="a{sv}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiEventProperties"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
</node>

86
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Image.xml

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Image:
@short_description: Interface for accessible objects that represent
graphical images.
-->
<interface name="org.a11y.atspi.Image">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
ImageDescription: the description of the image.
-->
<property name="ImageDescription" type="s" access="read"/>
<!--
ImageLocale: a POSIX LC_MESSAGES-style locale value for the image
description and text.
-->
<property name="ImageLocale" type="s" access="read"/>
<!--
GetImageExtents:
@coordType: Specifies the reference point for the coordinates to
be returned. 0 = relative to the screen, 1 = relative to the
application's top-level window, 2 = relative to the accessible's
parent. Requesting coordinates relative to the screen is deprecated
and may not be supported by the application.
Gets the bounding box of the image displayed in a
specified Image implementor.
The returned values are meaningful only if the Image has both
STATE_VISIBLE and STATE_SHOWING.
-->
<method name="GetImageExtents">
<arg direction="in" name="coordType" type="u"/>
<arg direction="out" type="(iiii)">
<av:TypeDefinition>
<av:Struct Type="AtSpiRect"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetImagePosition:
@coordType: Specifies the reference point for the coordinates to
be returned. 0 = relative to the screen, 1 = relative to the
application's top-level window, 2 = relative to the accessible's
parent. Requesting coordinates relative to the screen is deprecated
and may not be supported by the application.
Gets the minimum x and y coordinates of the image displayed in a
specified Image implementor.
The returned values are meaningful only if the Image has both
STATE_VISIBLE and STATE_SHOWING.
-->
<method name="GetImagePosition">
<arg direction="in" name="coordType" type="u"/>
<arg direction="out" name="x" type="i"/>
<arg direction="out" name="y" type="i"/>
</method>
<!--
GetImageSize:
Gets the size of the image displayed in a specified object.
The returned values are meaningful only if the Image has both
STATE_VISIBLE and STATE_SHOWING.
-->
<method name="GetImageSize">
<arg direction="out" name="width" type="i"/>
<arg direction="out" name="height" type="i"/>
</method>
</interface>
</node>

130
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Registry.xml

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Registry:
@short_description: Interface implemented by the AT-SPI registry daemon.
The registry daemon listens on org.a11y.atspi.Registry and exposes an
object at /org/a11y/atspi/registry that implements this interface.
-->
<interface name="org.a11y.atspi.Registry">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
RegisterEvent:
@event_type: a character string indicating the type of events for which
notification is requested. Format is
EventClass:major_type:minor_type:detail
where all subfields other than EventClass are optional.
EventClasses include "object", "window", "mouse",
and toolkit events (e.g. "Gtk", "AWT").
Examples: "focus:", "Gtk:GtkWidget:button_press_event".
@properties: A list of properties that applications should pass
when sending the event specified in @event_type. This is intended
to allow an AT to proactively request the information that it is
interested in and minimize the need to make additional queries in
response to the event. This is currently unimplemented; the
specification would need to be fleshed out here.
@app_bus_name: The application that this request pertains to.
This allows an app to indicate that it is interested in a
particular event only for a certain application. If this string is
empty, then this registration applies to all applications.
This method is to be called by an AT to indicate that it is
interested in receiving a particular event from applications.
Applications can optionally call GetRegisteredEvents and listen for
EventListenerRewgistered and EventListenerDeregistered signals and,
in this way, only send events that an AT is interested in receiving.
-->
<method name="RegisterEvent">
<arg direction="in" name="event" type="s"/>
<arg direction="in" name="properties" type="as"/>
<arg direction="in" name="app_bus_name" type="s"/>
</method>
<!--
DeregisterEvent:
@event_type: A character string indicating the event to be
deregistered. See RegisterEvent for a description of the format of
this string.
Indicates that an AT is no longer interested in listening to a
particular event.
TODO: Add app_bus_name here.
-->
<method name="DeregisterEvent">
<arg direction="in" name="event" type="s"/>
</method>
<!--
GetRegisteredEvents:
@events: A dictionary listing the registered events. For each entry,
the first string gives the bus name of the AT holding the
registration. The second string gives the event. See RegisterEvent
for a description of the format of this string.
-->
<method name="GetRegisteredEvents">
<arg direction="out" name="events" type="a(ss)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiEventListener"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
EventListenerRegistered:
@bus: The bus name of the AT registering the event.
@event: The event being registered. See RegisterEvent for a
description of the format of this string.
@properties: An array of properties that the AT is interested in
receiving along with the event. Currently unimplemented; the
specification for this would need to be fleshed out.
Indicates that an AT has requested to listen for an event.
Applications can optionally listen to this signal to maintain a
list of events that ATs are interested in receiving and only send
events when needed.
-->
<signal name="EventListenerRegistered">
<arg name="bus" type="s"/>
<arg name="event" type="s"/>
<arg name="properties" type="as"/>
</signal>
<!--
EventListenerDeregistered:
@bus: The bus name of the AT registering the event.
@event: The event being deregistered. See RegisterEvent for a
description of the format of this string.
Indicates that an AT is no longer interested in listening for an
event. Applications can optionally listen to this signal to maintain
a list of events that ATs are interested in receiving and only send
events when needed.
-->
<signal name="EventListenerDeregistered">
<arg name="bus" type="s"/>
<arg name="event" type="s"/>
</signal>
</interface>
</node>

148
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Selection.xml

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Selection:
@short_description: An interface which indicates that an object exposes
a 'selection' model, allowing the selection of one or more of its
children.
-->
<interface name="org.a11y.atspi.Selection">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
NSelectedChildren:
The number of children which are currently selected.
-->
<property name="NSelectedChildren" type="i" access="read"/>
<!--
GetSelectedChild:
@selectedChildIndex: an integer indicating which of the selected
children is specified.
Gets the i-th selected child of the accessible.
Note that @selectedChildIndex refers to the index in the list of
selected children and generally differs from that used in
GetChildAtIndex or returned by GetIndexInParent.
@selectedChildIndex must lie between 0 and the value returned by
GetNSelectedChildren - 1, inclusive.
Returns: a reference (bus name + object path) of the selected child
corresponding to the given index. If there is no selected child with
the given index, then the special object path "/org/a11y/atspi/null"
will be returned.
-->
<method name="GetSelectedChild">
<arg direction="in" name="selectedChildIndex" type="i"/>
<arg direction="out" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
SelectChild:
@childIndex: an integer indicating which child of the accessible
is to be selected.
Adds a child to to the accessible's list of selected children.
For implementors that only allow single selections, this
may replace the (single) current selection.
Returns: true if the child was successfully selected, false otherwise.
-->
<method name="SelectChild">
<arg direction="in" name="childIndex" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
DeselectSelectedChild:
@selectedChildIndex: a integer indicating which of the selected
children of the accessible is to be deselected.
Removes a child from an accessible's list of selected children.
Note that @selectedChildIndex is the index in the selected-children list,
not the index in the parent container. @selectedChildIndex in this
method and @childIndex in #atspi_selection_select_child
are asymmetric.
Returns: true if the child was successfully deselected, false otherwise.
-->
<method name="DeselectSelectedChild">
<arg direction="in" name="selectedChildIndex" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
IsChildSelected:
@childIndex: an index into the accessible's list of children.
Determines whether a particular child of the accessible is
currently selected. Note that @childIndex is the index into the
standard accessible container's list of children.
Returns: true if the specified child is currently selected,
false otherwise.
-->
<method name="IsChildSelected">
<arg direction="in" name="childIndex" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
SelectAll:
Attempts to select all of the accessible's children.
Not all implementors support this operation.
Returns: true if successful, false otherwise.
-->
<method name="SelectAll">
<arg direction="out" type="b"/>
</method>
<!--
ClearSelection:
Clears the current selection, removing all selected children from the
specified accessible's selection list.
Returns: true if successful, false otherwise.
-->
<method name="ClearSelection">
<arg direction="out" type="b"/>
</method>
<!--
DeselectChild:
@childIndex: a integer indicating which of the children
of the accessible is to be de-selected.
Deselects a specific child of an accessible.
Note that @childIndex is the index of the child in the parent
container.
Returns: true if the child was successfully deselected, false otherwise.
-->
<method name="DeselectChild">
<arg direction="in" name="childIndex" type="i"/>
<arg direction="out" type="b"/>
</method>
</interface>
</node>

98
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Socket.xml

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Socket:
@short_description: Interface to register an application on the registry.
-->
<interface name="org.a11y.atspi.Socket">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
Embed:
@plug: a string for the unique bus name of the application, and an object path
for the application's' root object.
This is the entry point for an application that wants to register itself against
the accessibility registry. The application's root object, which it passes in
@plug, must support the org.a11y.atspi.Application interface.
When an application calls this method on the registry, the following handshake happens:
* Application calls this method on the registry to identify itself.
* The registry sets the "Id" property on the org.a11y.atspi.Application interface on the @plug object.
* The Embed method returns with the bus name and object path for the registry's root object.
Returns: the bus name and object path of the registry's root object.
-->
<method name="Embed">
<arg direction="in" name="plug" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
<arg direction="out" name="socket" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
Embedded:
@path: the object path of the socket.
This method is called by a socket to inform the plug that it is being
embedded. The plug should register the embedding socket as its parent.
-->
<method name="Embedded">
<arg direction="in" name="path" type="s"/>
</method>
<!--
Unembed:
@plug: a string for the unique bus name of the application, and an object path
for the application's' root object.
Unregisters an application from the accesibility registry. It is not necessary to
call this method; the accessibility registry detects when an application
disconnects from the bus.
-->
<method name="Unembed">
<arg direction="in" name="plug" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</method>
<!--
Available:
@socket: application and object path for the registry's root object.
The accessibility registry emits this signal early during startup, when it has
registered with the DBus daemon and is available for calls from applications.
-->
<signal name="Available">
<arg name="socket" type="(so)">
<av:TypeDefinition>
<av:Struct Type="AtSpiObjectReference"/>
</av:TypeDefinition>
</arg>
</signal>
</interface>
</node>

7
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Status.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:av="http://avaloniaui.net/dbus/1.0">
<interface name="org.a11y.Status">
<property name="IsEnabled" type="b" access="readwrite"/>
<property name="ScreenReaderEnabled" type="b" access="readwrite"/>
</interface>
</node>

688
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Text.xml

@ -0,0 +1,688 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Text:
@short_description: An interface implemented by objects which place
textual information onscreen.
-->
<interface name="org.a11y.atspi.Text">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
CharacterCount: The total number of characters in a text object.
This may differ from the number of bytes that would be returned
if the text is fetched in cases where characters are expressed
using multiple bytes.
-->
<property name="CharacterCount" type="i" access="read"/>
<!--
CaretOffset: The current character offset of the text caret in a
text object.
-->
<property name="CaretOffset" type="i" access="read"/>
<!--
GetStringAtOffset:
@offset: The character offset for which the text is being requested.
@granularity: The text granularity. Can be one of the following:
- 0 (ATSPI_TEXT_GRANULARITY_CHAR): Granularity is defined by the
boundaries between characters (including non-printing characters)
- 1 (ATSPI_TEXT_GRANULARITY_WORD): Granularity is defined by the
boundaries of a word, starting at the beginning of the current
word and finishing at the beginning of the following one, if present.
- 2 (ATSPI_TEXT_GRANULARITY_SENTENCE): Granularity is defined by the
boundaries of a sentence, starting at the beginning of the current
sentence and finishing at the beginning of the following one,
if present.
- 3 (ATSPI_TEXT_GRANULARITY_LINE): Granularity is defined by the
boundaries of a line, starting at the beginning of the current
line and finishing at the beginning of the following one, if present.
- 4 (ATSPI_TEXT_GRANULARITY_PARAGRAPH): Granularity is defined by
the boundaries of a paragraph, starting at the beginning of the
current paragraph and finishing at the beginning of the following one, if present.
@startOffset: The starting character offset of the string being
returned.
@endOffset: The ending character offset of the string being
returned.
Gets a portion of the text exposed through a text object according
to a given @offset and a specific @granularity, along with the
start and end offsets defining the boundaries of such a portion
of text.
If @granularity is ATSPI_TEXT_GRANULARITY_CHAR the character at the
offset is returned.
If @granularity is ATSPI_TEXT_GRANULARITY_WORD the returned string
is from the word start at or before the offset to the word start after
the offset.
The returned string will contain the word at the offset if the offset
is inside a word and will contain the word before the offset if the
offset is not inside a word.
If @granularity is ATSPI_TEXT_GRANULARITY_SENTENCE the returned string
is from the sentence start at or before the offset to the sentence
start after the offset.
The returned string will contain the sentence at the offset if the offset
is inside a sentence and will contain the sentence before the offset
if the offset is not inside a sentence.
If @granularity is ATSPI_TEXT_GRANULARITY_LINE the returned string
is from the line start at or before the offset to the line
start after the offset.
If @granularity is ATSPI_TEXT_GRANULARITY_PARAGRAPH the returned strin
is from the start of the paragraph at or before the offset to the start
of the following paragraph after the offset.
-->
<method name="GetStringAtOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="granularity" type="u"/>
<arg direction="out" name="text" type="s"/>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GetText:
@startOffset: a integer indicating the start of the desired text
range.
@endOffset: a integer indicating the first character past the desired
range.
Gets a range of text from a text object. The number of bytes in
the returned string may exceed either endOffset or
start_offset, since UTF-8 is a variable-width encoding.
Returns: a text string containing characters from @startOffset
to @endOffset-1, inclusive, encoded as UTF-8.
-->
<method name="GetText">
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="out" type="s"/>
</method>
<!--
SetCaretOffset:
@offset: the offset to which the text caret is to be moved.
Moves the text caret to a given position.
Returns: true if successful, false otherwise.
-->
<method name="SetCaretOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
GetTextBeforeOffset:
@offset: a integer indicating the offset from which the delimiter
search is based.
@type: Indicates whether the desired text string is a word, sentence,
line, or attribute run. See GetTextAtOffset for more information.
@startOffset: (out) The beginning character offset of the returned
string.
@endOffset: (out) The ending character offset of the returned
string.
Gets delimited text from a text object which precedes a given text
offset.
Returns: a UTF-8 string representing the delimited text, both of
whose delimiting boundaries are before the current offset, or an
empty string if no such text exists.
Deprecated: Please use GetStringAtOffset instead.
-->
<method name="GetTextBeforeOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="type" type="u"/>
<arg direction="out" name="text" type="s"/>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GetTextAtOffset:
@offset: a integer indicating the offset from which the delimiter
search is based.
@type: Indicates whether the desired text string is a word, sentence,
line, or attribute run. Should have one of the following values:
0 (ATSPI_TEXT_BOUNDARY_CHAR): The text instance is bounded by this
character only. Start and end offsets differ by one, by definition,
for this value.
1 (ATSPI_TEXT_BOUNDARY_WORD_START): Boundary condition is start of
a word; i.e. range is from start of one word to the start of another
word.
2 (ATSPI_TEXT_BOUNDARY_WORD_END): Boundary condition is the end of
a word; i.e. range is from the end of one word to the end of another.
Some locales may not distinguish between words and characters or
glyphs. In particular, those locales which use wholly or partially
ideographic character sets. In these cases, characters may be
returned in lieu of multi-character substrings.
3 (ATSPI_TEXT_BOUNDARY_SENTENCE_START: Boundary condition is start
of a sentence, as determined by the application. Some locales or
character sets may not include explicit sentence delimiters, so this
boundary type can not always be honored. Some locales will return lines
of text instead of grammatical sentences.
4 (ATSPI_TEXT_BOUNDARY_SENTENCE_END): Boundary condition is end of
a sentence, as determined by the application, including the
sentence-delimiting character, for instance '.' Some locales or
character sets may not include explicit sentence delimiters, so
this boundary type can not always be honored. Some locales will
return lines of text instead of grammatical sentences.
5 (ATSPI_TEXT_BOUNDARY_LINE_START): Boundary condition is the start
of a line; i.e. range is from start of one line to the start of
another. This generally means that an end-of-line character will
appear at the end of the range.
6 (ATSPI_TEXT_BOUNDARY_LINE_END): Boundary condition is the end of
a line; i.e. range is from start of one line to the start of another.
This generally means that an end-of-line character will be the
first character of the range.
@startOffset: (out) The beginning character offset of the returned
string.
@endOffset: (out) The ending character offset of the returned
string.
Gets delimited text from a text object which includes a given text
offset.
Returns: a UTF-8 string representing the delimited text, whose
delimiting boundaries bracket the current offset, or an empty string
if no such text exists.
Deprecated: Please use GetStringAtOffset instead.
-->
<method name="GetTextAtOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="type" type="u"/>
<arg direction="out" name="text" type="s"/>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GetTextAfterOffset:
@offset: a integer indicating the offset from which the delimiter
search is based.
@type: Indicates whether the desired text string is a word, sentence,
line, or attribute run. See GetTextAtOffset for more information.
@startOffset: (out) The beginning character offset of the returned
string.
@endOffset: (out) The ending character offset of the returned
string.
Gets delimited text from a text object which follows a given text
offset.
Returns: a UTF-8 string representing the delimited text, both of
whose delimiting boundaries are after or inclusive of the current
offset, or an empty string if no such text exists.
Deprecated: Please use GetStringAtOffset instead.
-->
<method name="GetTextAfterOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="type" type="u"/>
<arg direction="out" name="text" type="s"/>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GecharacterAtOffset:
@offset: an integer indicating the text offset where the desired
character is located.
Gets the character at a given offset for a text object.
Returns: an integer representing the UCS-4 unicode code point of
the given character, or 0xFFFFFFFF if the character in question
cannot be represented in the UCS-4 encoding.
-->
<method name="GetCharacterAtOffset">
<arg direction="in" name="offset" type="i"/>
<arg direction="out" type="i"/>
</method>
<!--
GetAttributeValue:
@offset: The character offset at which to query the attribute.
@attributeName: The attribute to query.
Gets the value of a named attribute at a given offset.
Returns: the value of a given attribute at the given offset, or an
empty string if not present.
-->
<method name="GetAttributeValue">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="attributeName" type="s"/>
<arg direction="out" type="s"/>
</method>
<!--
GetAttributes:
@offset: an integer indicating the offset from which the attribute
search is based.
@startOffset: (out): an integer pointer indicating the start of
the desired text range.
@endOffset: (out): an integer pointer indicating the first character
past the desired range.
Gets the attributes applied to a range of text from a text object.
The text attributes correspond to CSS attributes where possible.
Returns: a dictionary of attribute name/value pairs representing
the attributes at the given offset, encoded as UTF-8.
-->
<method name="GetAttributes">
<arg direction="in" name="offset" type="i"/>
<arg direction="out" name="attributes" type="a{ss}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiAttributeSet"/>
</av:TypeDefinition>
</arg>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GetDefaultAttributes:
Gets the default attributes applied to a text object. The text
attributes correspond to CSS attributes where possible. The
combination of this attribute set and the attributes reported by
GetAttributes describes the entire set of text attributes over a
range.
Returns: A dictionary of key/value pairs containing the default
attributes applied to a text object, (exclusive of explicitly-set
attributes), encoded as UTF-8.
-->
<method name="GetDefaultAttributes">
<arg direction="out" type="a{ss}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiAttributeSet"/>
</av:TypeDefinition>
</arg>
</method>
<!--
GetCharacterExtents:
@offset: an integer indicating the offset of the text character for
whom boundary information is requested.
@coordType: Specifies the coordinate system to use for the return value.
See the org.a11y.Atspi.Component documentation for a description
of the values that can be specified.
Gets a bounding box containing the glyph representing
the character at a particular text offset.
The returned values are meaningful only if the Text has both
STATE_VISIBLE and STATE_SHOWING.
-->
<method name="GetCharacterExtents">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="coordType" type="u"/>
<arg direction="out" name="x" type="i"/>
<arg direction="out" name="y" type="i"/>
<arg direction="out" name="width" type="i"/>
<arg direction="out" name="height" type="i"/>
</method>
<!--
GetOffsetAtPoint:
@x: the x coordinate of the point to be queried.
@y: the y coordinate of the point to be queried.
@coordType: Specifies the coordinate system to use for the return value.
See the org.a11y.Atspi.Component documentation for a description
of the values that can be specified.
Gets the character offset into the text at a given point.
Returns: the offset (as an integer) at the point (@x, @y)
in the specified coordinate system.
-->
<method name="GetOffsetAtPoint">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="coordType" type="u"/>
<arg direction="out" type="i"/>
</method>
<!--
GetNSelections:
Gets the number of active non-contiguous selections for a text
object.
Returns: an integer indicating the current number of non-contiguous
text selections active within a text object.
-->
<method name="GetNSelections">
<arg direction="out" type="i"/>
</method>
<!--
GetSelection:
@selectionNum: an integer indicating which selection to query.
@startOffset: (out): The starting character offset of the given
selection.
@endOffset: (out): The ending character offset of the given
selection.
Gets the bounds of the @selectionNum-th active text selection for
a Text object.
-->
<method name="GetSelection">
<arg direction="in" name="selectionNum" type="i"/>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
AddSelection:
@startOffset: the starting character offset of the desired new
selection.
@endOffset: the offset of the first character after the new
selection.
Selects some text (adds a text selection) in a text object.
Returns: true if successful, false otherwise.
-->
<method name="AddSelection">
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
RemoveSelection:
@selectionNum: a integer indicating which text selection to remove.
De-selects a text selection.
*
Returns: true if successful, false otherwise.
-->
<method name="RemoveSelection">
<arg direction="in" name="selectionNum" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
SetSelection:
@selectionNum: a zero-offset index indicating which text selection
to modify.
@startOffset: an integer indicating the new starting offset for the
selection.
@endOffset: an integer indicating the desired new offset of the
first character after the selection.
Changes the bounds of an existing text selection.
Returns: true if successful, false otherwise.
-->
<method name="SetSelection">
<arg direction="in" name="selectionNum" type="i"/>
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="out" type="b"/>
</method>
<!--
GetRangeExtents:
@startOffset: an integer indicating the offset of the first text
character for whom boundary information is requested.
@endOffset: a integer indicating the offset of the text character
after the last character for whom boundary information is requested.
@coordType: Specifies the coordinate system to use for the return value.
See the org.a11y.Atspi.Component documentation for a description
of the values that can be specified.
Gets the bounding box for text within a range in a text object.
The returned values are meaningful only if the Text has both
STATE_VISIBLE and STATE_SHOWING.
-->
<method name="GetRangeExtents">
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="in" name="coordType" type="u"/>
<arg direction="out" name="x" type="i"/>
<arg direction="out" name="y" type="i"/>
<arg direction="out" name="width" type="i"/>
<arg direction="out" name="height" type="i"/>
</method>
<!--
GetBoundedRanges:
@x: the 'starting' x coordinate of the bounding box.
@y: the 'starting' y coordinate of the bounding box.
@width: the x extent of the bounding box.
@height: the y extent of the bounding box.
@coordType: Specifies the coordinate system that should be used to
interpret @x and @y. See the org.a11y.Atspi.Component documentation
for a description of the values that can be specified.
@xClipType: Indicates how to treat characters that intersect the
bounding box's x extents.
@yClipType: Indicates how to treat characters that intersect the
bounding box's y extents.
@xClipType and yClipType can have the following values:
- 0 (ATSPI_TEXT_CLIP_NONE): No characters/glyphs are omitted.
- 1 (ATSPI_TEXT_CLIP_MIN): Characters/glyphs clipped by the minimum
coordinate are omitted.
- 2 (ATSPI_TEXT_CLIP_MAX): Characters/glyphs which intersect the
maximum coordinate are omitted.
- 3 (ATSPI_TEXT_CLIP_BOTH): Only glyphs falling entirely within the
region bounded by min and max are retained.
Gets the ranges of text from an Text object which lie within the
bounds defined by (@x, @y) and (@x+@width, @y+@height).
Returns an array of structures with the following elements:
- The starting character offset of the range.
- The ending character offset of the range.
- The contents of the range.
- A variant. Currently unused (if this specification is revisited,
then this should probably be removed).
-->
<method name="GetBoundedRanges">
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="in" name="width" type="i"/>
<arg direction="in" name="height" type="i"/>
<arg direction="in" name="coordType" type="u"/>
<arg direction="in" name="xClipType" type="u"/>
<arg direction="in" name="yClipType" type="u"/>
<arg direction="out" type="a(iisv)">
<av:TypeDefinition>
<av:List>
<av:Struct Type="AtSpiTextRange"/>
</av:List>
</av:TypeDefinition>
</arg>
</method>
<!--
GetAttributeRun:
@offset: an integer indicating the offset from which the attribute
search is based.
@includeDefaults: a #bool that, when set as false, indicates the
call should only return those attributes which are explicitly set
on the current attribute run, omitting any attributes which are
inherited from the default values.
Gets a set of attributes applied to a range of text from a text
object, optionally including its 'default' attributes.
Returns a dictionary of key/value pairs specifying the
attributes.
-->
<method name="GetAttributeRun">
<arg direction="in" name="offset" type="i"/>
<arg direction="in" name="includeDefaults" type="b"/>
<arg direction="out" name="attributes" type="a{ss}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiAttributeSet"/>
</av:TypeDefinition>
</arg>
<arg direction="out" name="startOffset" type="i"/>
<arg direction="out" name="endOffset" type="i"/>
</method>
<!--
GetDefaultAttributeSet:
Returns the default attributes for the text object.
-->
<method name="GetDefaultAttributeSet">
<arg direction="out" type="a{ss}">
<av:TypeDefinition>
<av:Dictionary Type="AtSpiAttributeSet"/>
</av:TypeDefinition>
</arg>
</method>
<!--
ScrollSubstringTo:
@startOffset: a integer indicating the start of the desired text
range.
@endOffset: a integer indicating the first character past the
desired range.
@type: Indicates where the object should be placed on the screen,
as follows:
- 0 (ATSPI_SCROLL_TOP_LEFT): Scroll the object to the top left corner
of the window.
- 1 (ATSPI_SCROLL_BOTTOM_RIGHT): Scroll the object to the bottom right
corner of the window.
- 2 (ATSPI_SCROLL_TOP_EDGE): Scroll the object to the top edge of
the window.
- 3 (ATSPI_SCROLL_BOTTOM_EDGE): Scroll the object to the bottom edge
of the window.
- 4 (ATSPI_SCROLL_LEFT_EDGE: Scroll the object to the left edge of the
window.
- 5 (ATSPI_SCROLL_RIGHT_EDGE): Scroll the object to the right edge of
the window.
- 6 (ATSPI_SCROLL_ANYWHERE): Scroll the object to application-dependent
position on the window.
Scrolls whatever container of the Text text range so it becomes
visible on the screen.
Returns: true if successful, false otherwise.
-->
<method name="ScrollSubstringTo">
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="in" name="type" type="u"/>
<arg direction="out" type="b"/>
</method>
<!--
ScrollSubstringToPoint:
@startOffset: an integer indicating the start of the desired text
range.
@endOffset: an integer indicating the first character past the
desired range.
@coordType: a CoordType indicating whether the coordinates are relative to
the screen, to the window, or to the parent object. See the
documentation for org.a11y.Atspi.Component for a description of
the allowed values.
@x: the x coordinate of the point to reach
@y: the y coordinate of the point to reach
Scrolls whatever container of the text range so it becomes
visible on the screen at a given position.
Returns: true if successful, false otherwise.
-->
<method name="ScrollSubstringToPoint">
<arg direction="in" name="startOffset" type="i"/>
<arg direction="in" name="endOffset" type="i"/>
<arg direction="in" name="coordType" type="u"/>
<arg direction="in" name="x" type="i"/>
<arg direction="in" name="y" type="i"/>
<arg direction="out" type="b"/>
</method>
</interface>
</node>

158
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Types.xml

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://avaloniaui.net/dbus/1.0">
<Struct Name="AtSpiObjectReference">
<Property Name="Service"/>
<Property Name="Path"/>
</Struct>
<Struct Name="AtSpiAction">
<Property Name="LocalizedName"/>
<Property Name="LocalizedDescription"/>
<Property Name="KeyBinding"/>
</Struct>
<Struct Name="AtSpiAccessibleCacheItem">
<Property Name="Path" Type="AtSpiObjectReference"/>
<Property Name="Application" Type="AtSpiObjectReference"/>
<Property Name="Parent" Type="AtSpiObjectReference"/>
<Property Name="IndexInParent"/>
<Property Name="ChildCount"/>
<Property Name="SupportedInterfaces">
<List/>
</Property>
<Property Name="Name"/>
<Property Name="Role"/>
<Property Name="Description"/>
<Property Name="State">
<List/>
</Property>
</Struct>
<Struct Name="AtSpiEventListener">
<Property Name="ListenerAddress"/>
<Property Name="EventName"/>
</Struct>
<Struct Name="AtSpiRelationEntry">
<Property Name="RelationType"/>
<Property Name="Targets">
<List>
<Struct Type="AtSpiObjectReference"/>
</List>
</Property>
</Struct>
<Struct Name="AtSpiTextRange">
<Property Name="StartOffset"/>
<Property Name="EndOffset"/>
<Property Name="Contents"/>
<Property Name="Value"/>
</Struct>
<Dictionary Name="AtSpiAttributeSet">
<Key Name="Key"/>
<Value Name="Value"/>
</Dictionary>
<Dictionary Name="AtSpiEventProperties">
<Key Name="Key"/>
<Value Name="Value"/>
</Dictionary>
<Struct Name="AtSpiDeviceEvent">
<Property Name="Type"/>
<Property Name="Id"/>
<Property Name="HardwareCode"/>
<Property Name="Modifiers">
<BitFlags Type="AtSpiModifierMask"/>
</Property>
<Property Name="Timestamp"/>
<Property Name="Text"/>
<Property Name="IsText"/>
</Struct>
<Struct Name="AtSpiMatchRule">
<Property Name="States">
<List/>
</Property>
<Property Name="StateMatchType"/>
<Property Name="Attributes" Type="AtSpiAttributeSet"/>
<Property Name="AttributeMatchType"/>
<Property Name="Roles">
<List/>
</Property>
<Property Name="RoleMatchType"/>
<Property Name="Interfaces">
<List/>
</Property>
<Property Name="InterfaceMatchType"/>
<Property Name="Invert"/>
</Struct>
<Struct Name="AtSpiKeyDefinition">
<Property Name="KeyCode"/>
<Property Name="KeySym"/>
<Property Name="KeyString"/>
<Property Name="Unused"/>
</Struct>
<Struct Name="AtSpiEventMode">
<Property Name="Synchronous"/>
<Property Name="Preemptive"/>
<Property Name="Global"/>
</Struct>
<BitFlags Name="AtSpiEventTypeMask">
<BitFlag Name="KeyPress" Value="1"/>
<BitFlag Name="KeyRelease" Value="2"/>
</BitFlags>
<BitFlags Name="AtSpiModifierMask">
<BitFlag Name="Shift" Value="1"/>
<BitFlag Name="Lock" Value="2"/>
<BitFlag Name="Control" Value="4"/>
<BitFlag Name="Mod1" Value="8"/>
<BitFlag Name="Mod2" Value="16"/>
<BitFlag Name="Mod3" Value="32"/>
<BitFlag Name="Mod4" Value="64"/>
<BitFlag Name="Mod5" Value="128"/>
</BitFlags>
<Struct Name="AtSpiKeystrokeListener">
<Property Name="BusName"/>
<Property Name="Path"/>
<Property Name="Type"/>
<Property Name="Types">
<BitFlags Type="AtSpiEventTypeMask"/>
</Property>
<Property Name="Keys">
<List>
<Struct Type="AtSpiKeyDefinition"/>
</List>
</Property>
<Property Name="Mask">
<BitFlags Type="AtSpiModifierMask"/>
</Property>
<Property Name="Mode" Type="AtSpiEventMode"/>
</Struct>
<Struct Name="AtSpiTextSelection">
<Property Name="StartObject" Type="AtSpiObjectReference"/>
<Property Name="StartOffset"/>
<Property Name="EndObject" Type="AtSpiObjectReference"/>
<Property Name="EndOffset"/>
<Property Name="IsStartActive"/>
</Struct>
<Struct Name="AtSpiRect">
<Property Name="X"/>
<Property Name="Y"/>
<Property Name="Width"/>
<Property Name="Height"/>
</Struct>
<Struct Name="AtSpiPoint">
<Property Name="X"/>
<Property Name="Y"/>
</Struct>
</Types>

47
src/Avalonia.FreeDesktop.AtSpi/DBusXml/Value.xml

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd" xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<!--
org.a11y.atspi.Value:
@short_description: An interface supporting a one-dimensional scalar
to be modified, or which reflects its value.
-->
<interface name="org.a11y.atspi.Value">
<!--
Version: The version of this interface.
This property is incremented by one every time a new method, signal, or property
is added to this interface.
-->
<property name="version" type="u" access="read"/>
<!--
MinimumValue: The minimum allowed value.
-->
<property name="MinimumValue" type="d" access="read"/>
<!--
MaximumValue: The maximum allowed value.
-->
<property name="MaximumValue" type="d" access="read"/>
<!--
MinimumIncrement: The minimum increment by which a value can be
adjusted.
-->
<property name="MinimumIncrement" type="d" access="read"/>
<!--
CurrentValue: The current value.
-->
<property name="CurrentValue" type="d" access="readwrite"/>
<!--
Text: a human readable text alternative associated with the value,
if available.
-->
<property name="Text" type="s" access="read"/>
</interface>
</node>

9
src/Avalonia.FreeDesktop.AtSpi/DBusXml/org.a11y.Bus.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<node xmlns:av="http://avaloniaui.net/dbus/1.0">
<av:ImportTypes>Types.xml</av:ImportTypes>
<interface name="org.a11y.Bus">
<method name="GetAddress">
<arg direction="out" type="s"/>
</method>
</interface>
</node>

152
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Accessible interface for an AutomationPeer-backed node.
/// </summary>
internal sealed class AtSpiAccessibleHandler(AtSpiServer server, AtSpiNode node) : IOrgA11yAtspiAccessible
{
public uint Version => AccessibleVersion;
public string Name => AtSpiNode.GetAccessibleName(node.Peer);
public string Description => node.Peer.GetHelpText();
public AtSpiObjectReference Parent
{
get
{
// For window nodes, return the ApplicationAtSpiNode as parent
if (node is RootAtSpiNode { AppRoot: { } appRoot })
return new AtSpiObjectReference(
server.UniqueName, new DBusObjectPath(appRoot.Path));
return server.GetReference(node.Parent);
}
}
public int ChildCount => node.EnsureChildren().Count;
public string Locale => ResolveLocale();
public string AccessibleId => node.Peer.GetAutomationId() ?? string.Empty;
public string HelpText => node.Peer.GetHelpText();
public ValueTask<AtSpiObjectReference> GetChildAtIndexAsync(int index)
{
var children = node.EnsureChildren();
if (index < 0 || index >= children.Count)
return ValueTask.FromResult(server.GetNullReference());
return ValueTask.FromResult(server.GetReference(children[index]));
}
public ValueTask<List<AtSpiObjectReference>> GetChildrenAsync()
{
var children = node.EnsureChildren();
var refs = new List<AtSpiObjectReference>(children.Count);
foreach (var child in children)
{
refs.Add(server.GetReference(child));
}
return ValueTask.FromResult(refs);
}
public ValueTask<int> GetIndexInParentAsync()
{
var parent = node.Parent;
if (parent is null)
return ValueTask.FromResult(-1);
var siblings = parent.EnsureChildren();
for (var i = 0; i < siblings.Count; i++)
{
if (ReferenceEquals(siblings[i], node))
return ValueTask.FromResult(i);
}
return ValueTask.FromResult(-1);
}
public ValueTask<List<AtSpiRelationEntry>> GetRelationSetAsync()
{
var relations = new List<AtSpiRelationEntry>();
var labeledBy = node.Peer.GetLabeledBy();
if (labeledBy is not null)
{
var labelNode = server.TryGetAttachedNode(labeledBy);
if (labelNode is not null)
{
// Relation type 2 = LABELLED_BY
relations.Add(new AtSpiRelationEntry(2, [server.GetReference(labelNode)]));
}
}
return ValueTask.FromResult(relations);
}
public ValueTask<uint> GetRoleAsync()
{
var role = AtSpiNode.ToAtSpiRole(node.Peer.GetAutomationControlType(), node.Peer);
return ValueTask.FromResult((uint)role);
}
public ValueTask<string> GetRoleNameAsync()
{
var role = AtSpiNode.ToAtSpiRole(node.Peer.GetAutomationControlType(), node.Peer);
return ValueTask.FromResult(AtSpiNode.ToAtSpiRoleName(role));
}
public ValueTask<string> GetLocalizedRoleNameAsync()
{
var role = AtSpiNode.ToAtSpiRole(node.Peer.GetAutomationControlType(), node.Peer);
return ValueTask.FromResult(AtSpiNode.ToAtSpiRoleName(role));
}
public ValueTask<List<uint>> GetStateAsync()
{
return ValueTask.FromResult(node.ComputeStates());
}
public ValueTask<AtSpiAttributeSet> GetAttributesAsync()
{
var attrs = new AtSpiAttributeSet { ["toolkit"] = "Avalonia" };
var name = node.Peer.GetName();
if (!string.IsNullOrEmpty(name))
attrs["explicit-name"] = "true";
var acceleratorKey = node.Peer.GetAcceleratorKey();
if (!string.IsNullOrEmpty(acceleratorKey))
attrs["accelerator-key"] = acceleratorKey;
var accessKey = node.Peer.GetAccessKey();
if (!string.IsNullOrEmpty(accessKey))
attrs["access-key"] = accessKey;
return ValueTask.FromResult(attrs);
}
public ValueTask<AtSpiObjectReference> GetApplicationAsync()
{
return ValueTask.FromResult(server.GetRootReference());
}
public ValueTask<List<string>> GetInterfacesAsync()
{
var interfaces = node.GetSupportedInterfaces();
var sorted = interfaces.OrderBy(static i => i, StringComparer.Ordinal).ToList();
return ValueTask.FromResult(sorted);
}
}
}

172
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiActionHandler.cs

@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Automation;
using Avalonia.Automation.Provider;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Action interface (invoke, toggle, expand/collapse, scroll).
/// </summary>
internal sealed class AtSpiActionHandler : IOrgA11yAtspiAction
{
private readonly AtSpiNode _node;
private readonly List<ActionEntry> _actions;
public AtSpiActionHandler(AtSpiServer server, AtSpiNode node)
{
_ = server;
_node = node;
_actions = BuildActionList();
}
public uint Version => ActionVersion;
public int NActions => _actions.Count;
public ValueTask<string> GetDescriptionAsync(int index)
{
if (index >= 0 && index < _actions.Count)
return ValueTask.FromResult(_actions[index].Description);
return ValueTask.FromResult(string.Empty);
}
public ValueTask<string> GetNameAsync(int index)
{
if (index >= 0 && index < _actions.Count)
return ValueTask.FromResult(_actions[index].ActionName);
return ValueTask.FromResult(string.Empty);
}
public ValueTask<string> GetLocalizedNameAsync(int index)
{
if (index >= 0 && index < _actions.Count)
return ValueTask.FromResult(_actions[index].LocalizedName);
return ValueTask.FromResult(string.Empty);
}
public ValueTask<string> GetKeyBindingAsync(int index)
{
if (index >= 0 && index < _actions.Count)
return ValueTask.FromResult(_actions[index].KeyBinding);
return ValueTask.FromResult(string.Empty);
}
public ValueTask<List<AtSpiAction>> GetActionsAsync()
{
var result = new List<AtSpiAction>(_actions.Count);
result.AddRange(_actions.Select(entry => new AtSpiAction(entry.LocalizedName, entry.Description, entry.KeyBinding)));
return ValueTask.FromResult(result);
}
public ValueTask<bool> DoActionAsync(int index)
{
if (index < 0 || index >= _actions.Count)
return ValueTask.FromResult(false);
var action = _actions[index];
ExecuteAction(action.ActionName);
return ValueTask.FromResult(true);
}
private void ExecuteAction(string actionName)
{
switch (actionName)
{
case "click":
_node.Peer.GetProvider<IInvokeProvider>()?.Invoke();
break;
case "toggle":
_node.Peer.GetProvider<IToggleProvider>()?.Toggle();
break;
case "expand or collapse":
if (_node.Peer.GetProvider<IExpandCollapseProvider>() is { } expandCollapseAction)
{
if (expandCollapseAction.ExpandCollapseState == ExpandCollapseState.Collapsed)
expandCollapseAction.Expand();
else
expandCollapseAction.Collapse();
}
break;
case "scroll up":
_node.Peer.GetProvider<IScrollProvider>()?.Scroll(
ScrollAmount.NoAmount, ScrollAmount.SmallDecrement);
break;
case "scroll down":
_node.Peer.GetProvider<IScrollProvider>()?.Scroll(
ScrollAmount.NoAmount, ScrollAmount.SmallIncrement);
break;
case "scroll left":
_node.Peer.GetProvider<IScrollProvider>()?.Scroll(
ScrollAmount.SmallDecrement, ScrollAmount.NoAmount);
break;
case "scroll right":
_node.Peer.GetProvider<IScrollProvider>()?.Scroll(
ScrollAmount.SmallIncrement, ScrollAmount.NoAmount);
break;
// Provisional: activate selectable items (TabItem, ListBoxItem) via Action.
case "select":
_node.Peer.GetProvider<ISelectionItemProvider>()?.Select();
break;
}
}
// TODO: Proper mapping of ActionList keybindings to AutomationPeers.
private List<ActionEntry> BuildActionList()
{
var actions = new List<ActionEntry>();
if (_node.Peer.GetProvider<IInvokeProvider>() is not null)
{
var acceleratorKey = _node.Peer.GetAcceleratorKey() ?? string.Empty;
actions.Add(new ActionEntry("click", "Click", "Performs the default action", acceleratorKey));
}
if (_node.Peer.GetProvider<IToggleProvider>() is not null)
actions.Add(new ActionEntry("toggle", "Toggle", "Toggles the control state", string.Empty));
if (_node.Peer.GetProvider<IExpandCollapseProvider>() is not null)
actions.Add(new ActionEntry("expand or collapse", "Expand or Collapse", "Expands or collapses the control", string.Empty));
if (_node.Peer.GetProvider<IScrollProvider>() is { } scroll)
{
if (scroll.VerticallyScrollable)
{
actions.Add(new ActionEntry("scroll up", "Scroll Up", "Scrolls the view up", string.Empty));
actions.Add(new ActionEntry("scroll down", "Scroll Down", "Scrolls the view down", string.Empty));
}
if (scroll.HorizontallyScrollable)
{
actions.Add(new ActionEntry("scroll left", "Scroll Left", "Scrolls the view left", string.Empty));
actions.Add(new ActionEntry("scroll right", "Scroll Right", "Scrolls the view right", string.Empty));
}
}
if (_node.Peer.GetProvider<ISelectionItemProvider>() is not null
&& _node.Peer.GetProvider<IInvokeProvider>() is null)
{
actions.Add(new ActionEntry("select", "Select", "Selects this item", string.Empty));
}
return actions;
}
/// <summary>
/// Describes a single AT-SPI action exposed by a node.
/// </summary>
private readonly struct ActionEntry(
string actionName,
string localizedName,
string description,
string keyBinding)
{
public string ActionName { get; } = actionName;
public string LocalizedName { get; } = localizedName;
public string Description { get; } = description;
public string KeyBinding { get; } = keyBinding;
}
}
}

272
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiCollectionHandler.cs

@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Collection interface for match-based node queries.
/// </summary>
internal sealed class AtSpiCollectionHandler(AtSpiServer server, AtSpiNode node) : IOrgA11yAtspiCollection
{
private enum MatchType
{
Invalid = 0,
All = 1,
Any = 2,
None = 3,
Empty = 4,
}
public uint Version => CollectionVersion;
public ValueTask<List<AtSpiObjectReference>> GetMatchesAsync(
AtSpiMatchRule rule, uint sortby, int count, bool traverse)
{
var results = new List<AtSpiObjectReference>();
CollectMatches(node, rule, count, traverse, results, skipSelf: true);
return ValueTask.FromResult(results);
}
public ValueTask<List<AtSpiObjectReference>> GetMatchesToAsync(
DBusObjectPath currentObject, AtSpiMatchRule rule, uint sortby, uint tree,
bool limitScope, int count, bool traverse)
{
// GetMatchesTo: find matches after currentObject in tree order
var results = new List<AtSpiObjectReference>();
var found = false;
CollectMatchesOrdered(node, rule, count, traverse, results,
currentObject.ToString(), ref found, after: true);
return ValueTask.FromResult(results);
}
public ValueTask<List<AtSpiObjectReference>> GetMatchesFromAsync(
DBusObjectPath currentObject, AtSpiMatchRule rule, uint sortby, uint tree,
int count, bool traverse)
{
// GetMatchesFrom: find matches before currentObject in tree order
var results = new List<AtSpiObjectReference>();
var found = false;
CollectMatchesOrdered(node, rule, count, traverse, results,
currentObject.ToString(), ref found, after: false);
return ValueTask.FromResult(results);
}
public ValueTask<AtSpiObjectReference> GetActiveDescendantAsync()
{
// Not implemented in most toolkits
return ValueTask.FromResult(server.GetNullReference());
}
private void CollectMatches(
AtSpiNode parent, AtSpiMatchRule rule, int count, bool traverse,
List<AtSpiObjectReference> results, bool skipSelf)
{
if (count > 0 && results.Count >= count)
return;
if (!skipSelf && MatchesRule(parent, rule))
{
results.Add(server.GetReference(parent));
if (count > 0 && results.Count >= count)
return;
}
var children = parent.EnsureChildren();
foreach (var childNode in children)
{
if (MatchesRule(childNode, rule))
{
results.Add(server.GetReference(childNode));
if (count > 0 && results.Count >= count)
return;
}
if (traverse)
CollectMatches(childNode, rule, count, traverse, results, skipSelf: true);
}
}
private void CollectMatchesOrdered(
AtSpiNode parent, AtSpiMatchRule rule, int count, bool traverse,
List<AtSpiObjectReference> results, string targetPath, ref bool pastTarget,
bool after)
{
if (count > 0 && results.Count >= count)
return;
var children = parent.EnsureChildren();
foreach (var childNode in children)
{
if (count > 0 && results.Count >= count)
return;
if (string.Equals(childNode.Path, targetPath, StringComparison.Ordinal))
{
pastTarget = true;
if (traverse)
CollectMatchesOrdered(childNode, rule, count, traverse, results,
targetPath, ref pastTarget, after);
continue;
}
var shouldInclude = after ? pastTarget : !pastTarget;
if (shouldInclude && MatchesRule(childNode, rule))
{
results.Add(server.GetReference(childNode));
if (count > 0 && results.Count >= count)
return;
}
if (traverse)
CollectMatchesOrdered(childNode, rule, count, traverse, results,
targetPath, ref pastTarget, after);
}
}
private static bool MatchesRule(AtSpiNode node, AtSpiMatchRule rule)
{
var match = MatchesStates(node, rule.States, (MatchType)rule.StateMatchType)
&& MatchesRoles(node, rule.Roles, (MatchType)rule.RoleMatchType)
&& MatchesInterfaces(node, rule.Interfaces, (MatchType)rule.InterfaceMatchType)
&& MatchesAttributes(node, rule.Attributes, (MatchType)rule.AttributeMatchType);
return rule.Invert ? !match : match;
}
private static bool MatchesStates(AtSpiNode node, List<int> ruleStates, MatchType matchType)
{
if (matchType is MatchType.Invalid or MatchType.Empty)
return matchType != MatchType.Empty || IsEmptyBitSet(ruleStates);
if (IsEmptyBitSet(ruleStates))
return true;
var nodeStates = node.ComputeStates();
var nodeLow = nodeStates.Count > 0 ? nodeStates[0] : 0u;
var nodeHigh = nodeStates.Count > 1 ? nodeStates[1] : 0u;
var ruleLow = ruleStates.Count > 0 ? (uint)ruleStates[0] : 0u;
var ruleHigh = ruleStates.Count > 1 ? (uint)ruleStates[1] : 0u;
return matchType switch
{
MatchType.All => (nodeLow & ruleLow) == ruleLow && (nodeHigh & ruleHigh) == ruleHigh,
MatchType.Any => (nodeLow & ruleLow) != 0 || (nodeHigh & ruleHigh) != 0,
MatchType.None => (nodeLow & ruleLow) == 0 && (nodeHigh & ruleHigh) == 0,
_ => true,
};
}
private static bool MatchesRoles(AtSpiNode node, List<int> ruleRoles, MatchType matchType)
{
if (matchType is MatchType.Invalid or MatchType.Empty)
return matchType != MatchType.Empty || IsEmptyBitSet(ruleRoles);
if (IsEmptyBitSet(ruleRoles))
return true;
var role = (uint)AtSpiNode.ToAtSpiRole(node.Peer.GetAutomationControlType(), node.Peer);
var bucket = (int)(role / 32);
var bit = (int)(role % 32);
var isSet = bucket < ruleRoles.Count && ((uint)ruleRoles[bucket] & (1u << bit)) != 0;
return matchType switch
{
MatchType.All or MatchType.Any => isSet,
MatchType.None => !isSet,
_ => true,
};
}
private static bool MatchesInterfaces(AtSpiNode node, List<string> ruleInterfaces, MatchType matchType)
{
if (matchType is MatchType.Invalid or MatchType.Empty)
return matchType != MatchType.Empty || ruleInterfaces.Count == 0;
if (ruleInterfaces.Count == 0)
return true;
var nodeInterfaces = node.GetSupportedInterfaces();
return matchType switch
{
MatchType.All => AllInterfacesPresent(nodeInterfaces, ruleInterfaces),
MatchType.Any => AnyInterfacePresent(nodeInterfaces, ruleInterfaces),
MatchType.None => !AnyInterfacePresent(nodeInterfaces, ruleInterfaces),
_ => true,
};
}
private static bool AllInterfacesPresent(HashSet<string> nodeInterfaces, List<string> required)
{
return required.All(iface => nodeInterfaces.Contains(ResolveInterfaceName(iface)));
}
private static bool AnyInterfacePresent(HashSet<string> nodeInterfaces, List<string> required)
{
return required.Any(iface => nodeInterfaces.Contains(ResolveInterfaceName(iface)));
}
private static string ResolveInterfaceName(string name)
{
// ATs may pass short names like "Action" or full names like "org.a11y.atspi.Action"
return name.Contains('.') ? name : $"org.a11y.atspi.{name}";
}
private static bool MatchesAttributes(AtSpiNode node, Dictionary<string, string>? ruleAttrs, MatchType matchType)
{
if (matchType is MatchType.Invalid or MatchType.Empty)
return matchType != MatchType.Empty || (ruleAttrs == null || ruleAttrs.Count == 0);
if (ruleAttrs == null || ruleAttrs.Count == 0)
return true;
// Build node attributes (same as AccessibleHandler.GetAttributesAsync)
var nodeAttrs = new Dictionary<string, string>(StringComparer.Ordinal) { ["toolkit"] = "Avalonia" };
var name = node.Peer.GetName();
if (!string.IsNullOrEmpty(name))
nodeAttrs["explicit-name"] = "true";
return matchType switch
{
MatchType.All => AllAttributesMatch(nodeAttrs, ruleAttrs),
MatchType.Any => AnyAttributeMatches(nodeAttrs, ruleAttrs),
MatchType.None => !AnyAttributeMatches(nodeAttrs, ruleAttrs),
_ => true,
};
}
private static bool AllAttributesMatch(Dictionary<string, string> nodeAttrs, Dictionary<string, string> required)
{
foreach (var kv in required)
{
if (!nodeAttrs.TryGetValue(kv.Key, out var value) ||
!string.Equals(value, kv.Value, StringComparison.Ordinal))
return false;
}
return true;
}
private static bool AnyAttributeMatches(Dictionary<string, string> nodeAttrs, Dictionary<string, string> required)
{
foreach (var kv in required)
{
if (nodeAttrs.TryGetValue(kv.Key, out var value) &&
string.Equals(value, kv.Value, StringComparison.Ordinal))
return true;
}
return false;
}
private static bool IsEmptyBitSet(List<int>? values)
{
if (values == null || values.Count == 0)
return true;
return values.All(v => v == 0);
}
}
}

133
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiComponentHandler.cs

@ -0,0 +1,133 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Automation.Provider;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using Avalonia.Platform;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Component interface (geometry and focus).
/// </summary>
internal sealed class AtSpiComponentHandler(AtSpiServer server, AtSpiNode node) : IOrgA11yAtspiComponent
{
public uint Version => ComponentVersion;
public ValueTask<bool> ContainsAsync(int x, int y, uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(node);
var point = TranslateToScreen(x, y, coordType);
var contains = rect.ContainsExclusive(new Point(point.x, point.y));
return ValueTask.FromResult(contains);
}
public ValueTask<AtSpiObjectReference> GetAccessibleAtPointAsync(int x, int y, uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(node);
var point = TranslateToScreen(x, y, coordType);
if (rect.ContainsExclusive(new Point(point.x, point.y)))
{
return ValueTask.FromResult(server.GetReference(node));
}
return ValueTask.FromResult(server.GetNullReference());
}
public ValueTask<AtSpiRect> GetExtentsAsync(uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(node);
var translated = AtSpiCoordinateHelper.TranslateRect(node, rect, coordType);
return ValueTask.FromResult(new AtSpiRect(
(int)translated.X, (int)translated.Y,
(int)translated.Width, (int)translated.Height));
}
public ValueTask<(int X, int Y)> GetPositionAsync(uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(node);
var translated = AtSpiCoordinateHelper.TranslateRect(node, rect, coordType);
return ValueTask.FromResult(((int)translated.X, (int)translated.Y));
}
public ValueTask<(int Width, int Height)> GetSizeAsync()
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(node);
return ValueTask.FromResult(((int)rect.Width, (int)rect.Height));
}
public ValueTask<uint> GetLayerAsync()
{
var controlType = node.Peer.GetAutomationControlType();
var layer = controlType == Automation.Peers.AutomationControlType.Window
? WindowLayer
: WidgetLayer;
return ValueTask.FromResult(layer);
}
public ValueTask<short> GetMDIZOrderAsync() => ValueTask.FromResult((short)-1);
public ValueTask<bool> GrabFocusAsync()
{
node.Peer.SetFocus();
return ValueTask.FromResult(true);
}
public ValueTask<double> GetAlphaAsync() => ValueTask.FromResult(1.0);
public ValueTask<bool> SetExtentsAsync(int x, int y, int width, int height, uint coordType)
{
// Only support moving (not resizing) for now
return SetPositionAsync(x, y, coordType);
}
public ValueTask<bool> SetPositionAsync(int x, int y, uint coordType)
{
if (node.Peer.GetProvider<IRootProvider>() is not { PlatformImpl: IWindowImpl windowImpl })
return ValueTask.FromResult(false);
var screenPos = TranslateToScreen(x, y, coordType);
windowImpl.Move(new PixelPoint(screenPos.x, screenPos.y));
return ValueTask.FromResult(true);
}
public ValueTask<bool> SetSizeAsync(int width, int height)
{
return ValueTask.FromResult(false);
}
public ValueTask<bool> ScrollToAsync(uint type)
{
return ValueTask.FromResult(false);
}
public ValueTask<bool> ScrollToPointAsync(uint coordType, int x, int y)
{
return ValueTask.FromResult(false);
}
private (int x, int y) TranslateToScreen(int x, int y, uint coordType)
{
var ct = (AtSpiCoordType)coordType;
switch (ct)
{
case AtSpiCoordType.Screen:
return (x, y);
case AtSpiCoordType.Window:
{
var windowRect = AtSpiCoordinateHelper.GetWindowRect(node);
return (x + (int)windowRect.X, y + (int)windowRect.Y);
}
case AtSpiCoordType.Parent:
{
var parentRect = AtSpiCoordinateHelper.GetParentScreenRect(node);
return (x + (int)parentRect.X, y + (int)parentRect.Y);
}
default:
return (x, y);
}
}
}
}

80
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiCoordinateHelper.cs

@ -0,0 +1,80 @@
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Coordinate translation utilities between screen, window, and parent frames.
/// </summary>
internal static class AtSpiCoordinateHelper
{
public static Rect GetScreenExtents(AtSpiNode node)
{
var bounds = node.Peer.GetBoundingRectangle();
if (node is RootAtSpiNode rootNode)
return rootNode.ToScreen(bounds);
var root = node.Peer.GetVisualRoot();
if (root is null)
return bounds;
if (node.Server.TryGetAttachedNode(root) is RootAtSpiNode rootNode2)
return rootNode2.ToScreen(bounds);
return bounds;
}
public static Rect TranslateRect(AtSpiNode node, Rect screenRect, uint coordType)
{
var ct = (AtSpiCoordType)coordType;
switch (ct)
{
case AtSpiCoordType.Screen:
return screenRect;
case AtSpiCoordType.Window:
{
var windowRect = GetWindowRect(node);
return new Rect(
screenRect.X - windowRect.X,
screenRect.Y - windowRect.Y,
screenRect.Width,
screenRect.Height);
}
case AtSpiCoordType.Parent:
{
var parentRect = GetParentScreenRect(node);
return new Rect(
screenRect.X - parentRect.X,
screenRect.Y - parentRect.Y,
screenRect.Width,
screenRect.Height);
}
default:
return screenRect;
}
}
public static Rect GetWindowRect(AtSpiNode node)
{
var root = node.Peer.GetVisualRoot();
if (root is null) return default;
if (node.Server.TryGetAttachedNode(root) is RootAtSpiNode rootNode)
return rootNode.ToScreen(root.GetBoundingRectangle());
return default;
}
public static Rect GetParentScreenRect(AtSpiNode node)
{
var parent = node.Peer.GetParent();
if (parent is null)
return default;
var bounds = parent.GetBoundingRectangle();
var root = parent.GetVisualRoot();
if (root is null)
return bounds;
var rootNode = node.Server.TryGetAttachedNode(root) as RootAtSpiNode;
return rootNode?.ToScreen(bounds) ?? bounds;
}
}
}

72
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEditableTextHandler.cs

@ -0,0 +1,72 @@
using System;
using System.Threading.Tasks;
using Avalonia.Automation.Provider;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI EditableText interface for writable text content.
/// </summary>
internal sealed class AtSpiEditableTextHandler(AtSpiNode node) : IOrgA11yAtspiEditableText
{
public uint Version => EditableTextVersion;
public ValueTask<bool> SetTextContentsAsync(string newContents)
{
if (node.Peer.GetProvider<IValueProvider>() is not { IsReadOnly: false } provider)
return ValueTask.FromResult(false);
provider.SetValue(newContents);
return ValueTask.FromResult(true);
}
public ValueTask<bool> InsertTextAsync(int position, string text, int length)
{
if (node.Peer.GetProvider<IValueProvider>() is not { IsReadOnly: false } provider)
return ValueTask.FromResult(false);
var current = provider.Value ?? string.Empty;
position = Math.Max(0, Math.Min(position, current.Length));
var toInsert = length >= 0 && length < text.Length ? text.Substring(0, length) : text;
var newValue = current.Insert(position, toInsert);
provider.SetValue(newValue);
return ValueTask.FromResult(true);
}
public ValueTask CopyTextAsync(int startPos, int endPos)
{
// Clipboard operations not supported via IValueProvider
return ValueTask.CompletedTask;
}
public ValueTask<bool> CutTextAsync(int startPos, int endPos)
{
// Clipboard operations not supported via IValueProvider
return ValueTask.FromResult(false);
}
public ValueTask<bool> DeleteTextAsync(int startPos, int endPos)
{
if (node.Peer.GetProvider<IValueProvider>() is not { IsReadOnly: false } provider)
return ValueTask.FromResult(false);
var current = provider.Value ?? string.Empty;
startPos = Math.Max(0, Math.Min(startPos, current.Length));
endPos = Math.Max(startPos, Math.Min(endPos, current.Length));
if (startPos >= endPos)
return ValueTask.FromResult(false);
var newValue = current.Remove(startPos, endPos - startPos);
provider.SetValue(newValue);
return ValueTask.FromResult(true);
}
public ValueTask<bool> PasteTextAsync(int position)
{
// Clipboard operations not supported via IValueProvider
return ValueTask.FromResult(false);
}
}
}

64
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEventObjectHandler.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Emits AT-SPI Event.Object signals (children-changed, state-changed, property-change).
/// </summary>
internal sealed class AtSpiEventObjectHandler(AtSpiServer server, string path) : IOrgA11yAtspiEventObject
{
public uint Version => EventObjectVersion;
public void EmitChildrenChangedSignal(string operation, int indexInParent, DBusVariant child)
{
EmitSignal("ChildrenChanged", operation, indexInParent, 0, child, EmptyProperties());
}
public void EmitPropertyChangeSignal(string propertyName, DBusVariant value)
{
EmitSignal("PropertyChange", propertyName, 0, 0, value, EmptyProperties());
}
public void EmitStateChangedSignal(string stateName, int detail1, DBusVariant value)
{
EmitSignal("StateChanged", stateName, detail1, 0, value, EmptyProperties());
}
public void EmitSelectionChangedSignal()
{
EmitSignal("SelectionChanged", string.Empty, 0, 0, new DBusVariant(0), EmptyProperties());
}
public void EmitBoundsChangedSignal()
{
EmitSignal("BoundsChanged", string.Empty, 0, 0, new DBusVariant(0), EmptyProperties());
}
private void EmitSignal(string member, params object[] body)
{
if (!server.HasEventListeners)
return;
var connection = server.A11yConnection;
if (connection is null)
return;
var message = DBusMessage.CreateSignal(
(DBusObjectPath)path,
IfaceEventObject,
member,
body);
_ = connection.SendMessageAsync(message);
}
private static Dictionary<string, DBusVariant> EmptyProperties()
{
return new Dictionary<string, DBusVariant>(StringComparer.Ordinal);
}
}
}

47
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiEventWindowHandler.cs

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Emits AT-SPI Event.Window signals (activate, deactivate).
/// </summary>
internal sealed class AtSpiEventWindowHandler(AtSpiServer server, string path) : IOrgA11yAtspiEventWindow
{
public void EmitActivateSignal()
{
EmitSignal("Activate", string.Empty, 0, 0, new DBusVariant("0"), EmptyProperties());
}
public void EmitDeactivateSignal()
{
EmitSignal("Deactivate", string.Empty, 0, 0, new DBusVariant("0"), EmptyProperties());
}
private void EmitSignal(string member, params object[] body)
{
if (!server.HasEventListeners)
return;
var connection = server.A11yConnection;
if (connection is null)
return;
var message = DBusMessage.CreateSignal(
(DBusObjectPath)path,
IfaceEventWindow,
member,
body);
_ = connection.SendMessageAsync(message);
}
private static Dictionary<string, DBusVariant> EmptyProperties()
{
return new Dictionary<string, DBusVariant>(StringComparer.Ordinal);
}
}
}

48
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiImageHandler.cs

@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Image interface for image controls.
/// </summary>
internal sealed class AtSpiImageHandler : IOrgA11yAtspiImage
{
private readonly AtSpiNode _node;
public AtSpiImageHandler(AtSpiServer server, AtSpiNode node)
{
_ = server;
_node = node;
}
public uint Version => ImageVersion;
public string ImageDescription => _node.Peer.GetHelpText();
public string ImageLocale => ResolveLocale();
public ValueTask<AtSpiRect> GetImageExtentsAsync(uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(_node);
var translated = AtSpiCoordinateHelper.TranslateRect(_node, rect, coordType);
return ValueTask.FromResult(new AtSpiRect(
(int)translated.X, (int)translated.Y,
(int)translated.Width, (int)translated.Height));
}
public ValueTask<(int X, int Y)> GetImagePositionAsync(uint coordType)
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(_node);
var translated = AtSpiCoordinateHelper.TranslateRect(_node, rect, coordType);
return ValueTask.FromResult(((int)translated.X, (int)translated.Y));
}
public ValueTask<(int Width, int Height)> GetImageSizeAsync()
{
var rect = AtSpiCoordinateHelper.GetScreenExtents(_node);
return ValueTask.FromResult(((int)rect.Width, (int)rect.Height));
}
}
}

138
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiSelectionHandler.cs

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Automation.Provider;
using Avalonia.DBus;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Selection interface for list-like containers.
/// </summary>
internal sealed class AtSpiSelectionHandler(AtSpiServer server, AtSpiNode node) : IOrgA11yAtspiSelection
{
public uint Version => SelectionVersion;
public int NSelectedChildren
{
get
{
var provider = node.Peer.GetProvider<ISelectionProvider>();
return provider?.GetSelection().Count ?? 0;
}
}
public ValueTask<AtSpiObjectReference> GetSelectedChildAsync(int selectedChildIndex)
{
node.EnsureChildren();
var provider = node.Peer.GetProvider<ISelectionProvider>();
if (provider is null)
return ValueTask.FromResult(server.GetNullReference());
var selection = provider.GetSelection();
if (selectedChildIndex < 0 || selectedChildIndex >= selection.Count)
return ValueTask.FromResult(server.GetNullReference());
var selectedPeer = selection[selectedChildIndex];
var childNode = server.TryGetAttachedNode(selectedPeer);
if (childNode is null || !ReferenceEquals(childNode.Parent, node))
return ValueTask.FromResult(server.GetNullReference());
return ValueTask.FromResult(server.GetReference(childNode));
}
public ValueTask<bool> SelectChildAsync(int childIndex)
{
var children = node.Peer.GetChildren();
if (childIndex < 0 || childIndex >= children.Count)
return ValueTask.FromResult(false);
var childPeer = children[childIndex];
if (childPeer.GetProvider<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
selectionItem.Select();
return ValueTask.FromResult(true);
}
public ValueTask<bool> DeselectSelectedChildAsync(int selectedChildIndex)
{
var provider = node.Peer.GetProvider<ISelectionProvider>();
if (provider is null)
return ValueTask.FromResult(false);
var selection = provider.GetSelection();
if (selectedChildIndex < 0 || selectedChildIndex >= selection.Count)
return ValueTask.FromResult(false);
var selectedPeer = selection[selectedChildIndex];
if (selectedPeer.GetProvider<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
selectionItem.RemoveFromSelection();
return ValueTask.FromResult(true);
}
public ValueTask<bool> IsChildSelectedAsync(int childIndex)
{
var children = node.Peer.GetChildren();
if (childIndex < 0 || childIndex >= children.Count)
return ValueTask.FromResult(false);
var childPeer = children[childIndex];
if (childPeer.GetProvider<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
return ValueTask.FromResult(selectionItem.IsSelected);
}
public ValueTask<bool> SelectAllAsync()
{
var provider = node.Peer.GetProvider<ISelectionProvider>();
if (provider is null || !provider.CanSelectMultiple)
return ValueTask.FromResult(false);
var children = node.Peer.GetChildren();
foreach (var child in children)
{
if (child.GetProvider<ISelectionItemProvider>() is { } selectionItem)
selectionItem.AddToSelection();
}
return ValueTask.FromResult(true);
}
public ValueTask<bool> ClearSelectionAsync()
{
var provider = node.Peer.GetProvider<ISelectionProvider>();
if (provider is null)
return ValueTask.FromResult(false);
var selection = provider.GetSelection();
foreach (var selectedPeer in selection)
{
if (selectedPeer.GetProvider<ISelectionItemProvider>() is { } selectionItem)
selectionItem.RemoveFromSelection();
}
return ValueTask.FromResult(true);
}
public ValueTask<bool> DeselectChildAsync(int childIndex)
{
var children = node.Peer.GetChildren();
if (childIndex < 0 || childIndex >= children.Count)
return ValueTask.FromResult(false);
var childPeer = children[childIndex];
if (childPeer.GetProvider<ISelectionItemProvider>() is not { } selectionItem)
return ValueTask.FromResult(false);
selectionItem.RemoveFromSelection();
return ValueTask.FromResult(true);
}
}
}

296
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiTextHandler.cs

@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Automation.Provider;
using Avalonia.Controls.Utils;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Text interface for read-only text content.
/// </summary>
internal sealed class AtSpiTextHandler(AtSpiNode node) : IOrgA11yAtspiText
{
private enum TextGranularity : uint
{
Char = 0,
Word = 1,
Sentence = 2,
Line = 3,
Paragraph = 4,
}
private enum TextBoundaryType : uint
{
Char = 0,
WordStart = 1,
WordEnd = 2,
SentenceStart = 3,
SentenceEnd = 4,
LineStart = 5,
LineEnd = 6,
}
public uint Version => TextVersion;
public int CharacterCount => GetText().Length;
public int CaretOffset => 0;
public ValueTask<(string Text, int StartOffset, int EndOffset)> GetStringAtOffsetAsync(
int offset, uint granularity)
{
var text = GetText();
if (text.Length == 0)
return ValueTask.FromResult((string.Empty, 0, 0));
offset = Math.Max(0, Math.Min(offset, text.Length - 1));
var g = (TextGranularity)granularity;
// For CHAR granularity, return single character
if (g == TextGranularity.Char)
{
var ch = text.Substring(offset, 1);
return ValueTask.FromResult((ch, offset, offset + 1));
}
// For WORD granularity, find word boundaries
if (g == TextGranularity.Word)
{
var start = StringUtils.PreviousWord(text, offset + 1);
if (start >= text.Length || !StringUtils.IsStartOfWord(text, start))
return ValueTask.FromResult((string.Empty, 0, 0));
var end = Math.Min(StringUtils.NextWord(text, start), text.Length);
if (end <= start)
return ValueTask.FromResult((string.Empty, 0, 0));
return ValueTask.FromResult((text.Substring(start, end - start), start, end));
}
// For SENTENCE, LINE, PARAGRAPH - return full text
return ValueTask.FromResult((text, 0, text.Length));
}
public ValueTask<string> GetTextAsync(int startOffset, int endOffset)
{
var text = GetText();
if (text.Length == 0)
return ValueTask.FromResult(string.Empty);
startOffset = Math.Max(0, startOffset);
if (endOffset < 0 || endOffset > text.Length)
endOffset = text.Length;
if (startOffset >= endOffset)
return ValueTask.FromResult(string.Empty);
return ValueTask.FromResult(text.Substring(startOffset, endOffset - startOffset));
}
public ValueTask<bool> SetCaretOffsetAsync(int offset)
{
return ValueTask.FromResult(false);
}
public ValueTask<(string Text, int StartOffset, int EndOffset)> GetTextBeforeOffsetAsync(
int offset, uint type)
{
// TODO: This method is a bit sketchy. Might need to wired up to
// our text handling logic in core.
var text = GetText();
if (offset <= 0 || text.Length == 0)
return ValueTask.FromResult((string.Empty, 0, 0));
offset = Math.Min(offset, text.Length);
var bt = (TextBoundaryType)type;
// CHAR boundary
if (bt == TextBoundaryType.Char)
{
var charOffset = offset - 1;
return ValueTask.FromResult((text.Substring(charOffset, 1), charOffset, charOffset + 1));
}
// WORD_START or WORD_END boundary
if (bt is TextBoundaryType.WordStart or TextBoundaryType.WordEnd)
{
var end = offset;
var start = StringUtils.PreviousWord(text, end);
if (start >= end)
start = StringUtils.PreviousWord(text, start);
if (start < 0 || start >= text.Length || !StringUtils.IsStartOfWord(text, start))
return ValueTask.FromResult((string.Empty, 0, 0));
end = Math.Min(StringUtils.NextWord(text, start), end);
if (end <= start)
return ValueTask.FromResult((string.Empty, 0, 0));
return ValueTask.FromResult((text.Substring(start, end - start), start, end));
}
// SENTENCE/LINE/PARAGRAPH - return all text before offset
var result = text.Substring(0, offset);
return ValueTask.FromResult((result, 0, offset));
}
public ValueTask<(string Text, int StartOffset, int EndOffset)> GetTextAtOffsetAsync(
int offset, uint type)
{
return GetStringAtOffsetAsync(offset, type);
}
public ValueTask<(string Text, int StartOffset, int EndOffset)> GetTextAfterOffsetAsync(
int offset, uint type)
{
var text = GetText();
if (offset >= text.Length - 1 || text.Length == 0)
return ValueTask.FromResult((string.Empty, text.Length, text.Length));
var bt = (TextBoundaryType)type;
// CHAR boundary
if (bt == TextBoundaryType.Char)
{
var charOffset = offset + 1;
if (charOffset >= text.Length)
return ValueTask.FromResult((string.Empty, text.Length, text.Length));
return ValueTask.FromResult((text.Substring(charOffset, 1), charOffset, charOffset + 1));
}
// WORD_START or WORD_END boundary
if (bt is TextBoundaryType.WordStart or TextBoundaryType.WordEnd)
{
var start = offset + 1;
while (start < text.Length &&
StringUtils.IsEndOfWord(text, start) &&
!StringUtils.IsStartOfWord(text, start))
{
start++;
}
if (start >= text.Length)
return ValueTask.FromResult((string.Empty, text.Length, text.Length));
var end = Math.Min(StringUtils.NextWord(text, start), text.Length);
if (end <= start)
return ValueTask.FromResult((string.Empty, text.Length, text.Length));
return ValueTask.FromResult((text.Substring(start, end - start), start, end));
}
// SENTENCE/LINE/PARAGRAPH - return all text after offset
var afterOffset = Math.Max(0, offset + 1);
var result = text.Substring(afterOffset);
return ValueTask.FromResult((result, afterOffset, text.Length));
}
public ValueTask<int> GetCharacterAtOffsetAsync(int offset)
{
var text = GetText();
if (offset < 0 || offset >= text.Length)
return ValueTask.FromResult(unchecked((int)0xFFFFFFFF));
return ValueTask.FromResult((int)text[offset]);
}
public ValueTask<string> GetAttributeValueAsync(int offset, string attributeName)
{
return ValueTask.FromResult(string.Empty);
}
public ValueTask<(AtSpiAttributeSet Attributes, int StartOffset, int EndOffset)> GetAttributesAsync(int offset)
{
var text = GetText();
return ValueTask.FromResult((new AtSpiAttributeSet(), 0, text.Length));
}
public ValueTask<AtSpiAttributeSet> GetDefaultAttributesAsync()
{
return ValueTask.FromResult(new AtSpiAttributeSet());
}
public ValueTask<(int X, int Y, int Width, int Height)> GetCharacterExtentsAsync(
int offset, uint coordType)
{
return ValueTask.FromResult((0, 0, 0, 0));
}
public ValueTask<int> GetOffsetAtPointAsync(int x, int y, uint coordType)
{
return ValueTask.FromResult(-1);
}
public ValueTask<int> GetNSelectionsAsync()
{
return ValueTask.FromResult(0);
}
public ValueTask<(int StartOffset, int EndOffset)> GetSelectionAsync(int selectionNum)
{
return ValueTask.FromResult((0, 0));
}
public ValueTask<bool> AddSelectionAsync(int startOffset, int endOffset)
{
return ValueTask.FromResult(false);
}
public ValueTask<bool> RemoveSelectionAsync(int selectionNum)
{
return ValueTask.FromResult(false);
}
public ValueTask<bool> SetSelectionAsync(int selectionNum, int startOffset, int endOffset)
{
return ValueTask.FromResult(false);
}
public ValueTask<(int X, int Y, int Width, int Height)> GetRangeExtentsAsync(
int startOffset, int endOffset, uint coordType)
{
return ValueTask.FromResult((0, 0, 0, 0));
}
public ValueTask<List<AtSpiTextRange>> GetBoundedRangesAsync(
int x, int y, int width, int height, uint coordType, uint xClipType, uint yClipType)
{
return ValueTask.FromResult(new List<AtSpiTextRange>());
}
public ValueTask<(AtSpiAttributeSet Attributes, int StartOffset, int EndOffset)> GetAttributeRunAsync(
int offset, bool includeDefaults)
{
var text = GetText();
return ValueTask.FromResult((new AtSpiAttributeSet(), 0, text.Length));
}
public ValueTask<AtSpiAttributeSet> GetDefaultAttributeSetAsync()
{
return ValueTask.FromResult(new AtSpiAttributeSet());
}
public ValueTask<bool> ScrollSubstringToAsync(int startOffset, int endOffset, uint type)
{
return ValueTask.FromResult(false);
}
public ValueTask<bool> ScrollSubstringToPointAsync(
int startOffset, int endOffset, uint coordType, int x, int y)
{
return ValueTask.FromResult(false);
}
private string GetText()
{
return node.Peer.GetProvider<IValueProvider>()?.Value ?? string.Empty;
}
}
}

46
src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiValueHandler.cs

@ -0,0 +1,46 @@
using System;
using Avalonia.Automation.Provider;
using Avalonia.FreeDesktop.AtSpi.DBusXml;
using static Avalonia.FreeDesktop.AtSpi.AtSpiConstants;
namespace Avalonia.FreeDesktop.AtSpi.Handlers
{
/// <summary>
/// Implements the AT-SPI Value interface for range-based controls.
/// </summary>
internal sealed class AtSpiValueHandler : IOrgA11yAtspiValue
{
private readonly AtSpiNode _node;
public AtSpiValueHandler(AtSpiServer server, AtSpiNode node)
{
_ = server;
_node = node;
}
public uint Version => ValueVersion;
public double MinimumValue =>
_node.Peer.GetProvider<IRangeValueProvider>() is { } p ? p.Minimum : 0;
public double MaximumValue =>
_node.Peer.GetProvider<IRangeValueProvider>() is { } p ? p.Maximum : 0;
public double MinimumIncrement =>
_node.Peer.GetProvider<IRangeValueProvider>() is { } p ? p.SmallChange : 0;
public string Text => string.Empty;
public double CurrentValue
{
get => _node.Peer.GetProvider<IRangeValueProvider>() is { } p ? p.Value : 0;
set
{
if (_node.Peer.GetProvider<IRangeValueProvider>() is not { } p)
return;
var clamped = Math.Max(p.Minimum, Math.Min(p.Maximum, value));
p.SetValue(clamped);
}
}
}
}

88
src/Avalonia.FreeDesktop.AtSpi/RootAtSpiNode.cs

@ -0,0 +1,88 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using Avalonia.Platform;
namespace Avalonia.FreeDesktop.AtSpi
{
/// <summary>
/// AT-SPI node for a top-level window with coordinate translation support.
/// </summary>
internal sealed class RootAtSpiNode : AtSpiNode
{
public RootAtSpiNode(AutomationPeer peer, AtSpiServer server)
: base(peer, server)
{
RootProvider = peer.GetProvider<IRootProvider>() ?? throw new InvalidOperationException(
"Attempt to create RootAtSpiNode from peer which does not implement IRootProvider.");
RootProvider.FocusChanged += OnRootFocusChanged;
if (WindowImpl is not { } impl)
return;
impl.Activated += OnWindowActivated;
impl.Deactivated += OnWindowDeactivated;
}
public IRootProvider RootProvider { get; }
public IWindowBaseImpl? WindowImpl => RootProvider.PlatformImpl as IWindowBaseImpl;
public ApplicationAtSpiNode? AppRoot { get; set; }
public Rect ToScreen(Rect rect)
{
if (WindowImpl is null)
return default;
return new PixelRect(
WindowImpl.PointToScreen(rect.TopLeft),
WindowImpl.PointToScreen(rect.BottomRight))
.ToRect(1);
}
public Point PointToClient(PixelPoint point)
{
if (WindowImpl is null)
return default;
return WindowImpl.PointToClient(point);
}
private void OnRootFocusChanged(object? sender, EventArgs e)
{
var focused = RootProvider.GetFocus();
var focusedNode = Server.TryGetAttachedNode(focused);
if (focusedNode is null)
{
// Focus can shift before children are queried;
// refresh visible root children lazily.
EnsureChildren();
focusedNode = Server.TryGetAttachedNode(focused);
}
Server.EmitFocusChange(focusedNode);
}
private void OnWindowActivated()
{
Server.EmitWindowActivationChange(this, true);
}
private void OnWindowDeactivated()
{
Server.EmitWindowActivationChange(this, false);
}
public override void Detach()
{
if (_detached)
return;
RootProvider.FocusChanged -= OnRootFocusChanged;
if (WindowImpl is { } impl)
{
impl.Activated -= OnWindowActivated;
impl.Deactivated -= OnWindowDeactivated;
}
base.Detach();
}
}
}

1
src/Avalonia.X11/Avalonia.X11.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
<ProjectReference Include="..\Avalonia.FreeDesktop.AtSpi\Avalonia.FreeDesktop.AtSpi.csproj" />
<Compile Include="..\Shared\RawEventGrouping.cs" />
</ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" />

173
src/Avalonia.X11/X11AtSpiAccessibility.cs

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.FreeDesktop.AtSpi;
using Avalonia.Logging;
using Avalonia.Threading;
namespace Avalonia.X11
{
internal sealed class X11AtSpiAccessibility
{
private readonly AvaloniaX11Platform _platform;
private readonly List<X11Window> _trackedWindows = new();
private AtSpiAccessibilityWatcher? _watcher;
private AtSpiServer? _server;
private bool _serverStartedUnconditionally;
internal X11AtSpiAccessibility(AvaloniaX11Platform platform)
{
_platform = platform;
}
internal AtSpiServer? Server => _server;
internal void Initialize()
{
_watcher = new AtSpiAccessibilityWatcher();
_ = InitializeAsync();
}
internal void TrackWindow(X11Window window) => _trackedWindows.Add(window);
internal void UntrackWindow(X11Window window) => _trackedWindows.Remove(window);
private async Task InitializeAsync()
{
try
{
await WaitForUiThreadSettleAsync();
// Path A: try unconditional connection first (GTK4 approach).
// This avoids delaying startup on watcher/session-bus property calls.
if (await TryStartServerAsync())
{
_serverStartedUnconditionally = true;
return;
}
// Path A failed - fall back to watcher-driven enablement.
await _watcher!.InitAsync();
_watcher.IsEnabledChanged += OnAccessibilityEnabledChanged;
if (_watcher.IsEnabled)
await EnableAccessibilityAsync();
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?
.Log(_platform, "AT-SPI initialization failed and will be disabled: {Exception}", e);
}
}
private async Task WaitForUiThreadSettleAsync()
{
try
{
// Wait until UI work is drained to context-idle so AT-SPI handlers
// are responsive when clients start querying immediately after embed.
var settle = Dispatcher.UIThread
.InvokeAsync(() => { }, DispatcherPriority.ContextIdle)
.GetTask();
// Keep startup bounded in case the UI thread never reaches idle
// (e.g., continuous high-priority work).
await settle.WaitAsync(TimeSpan.FromMilliseconds(100));
}
catch (TimeoutException e)
{
Logger.TryGet(LogEventLevel.Debug, LogArea.X11Platform)?
.Log(_platform, "AT-SPI startup wait timed out before UI thread reached idle: {Exception}", e);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Debug, LogArea.X11Platform)?
.Log(_platform, "AT-SPI startup wait failed, continuing without idle settle: {Exception}", e);
}
}
private async void OnAccessibilityEnabledChanged(object? sender, bool enabled)
{
try
{
if (enabled)
{
await EnableAccessibilityAsync();
}
else if (!_serverStartedUnconditionally)
{
// Only tear down if server wasn't started unconditionally.
// When started unconditionally, event listener tracking handles suppression.
await DisableAccessibilityAsync();
}
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?
.Log(_platform, "AT-SPI dynamic enable/disable toggle failed: {Exception}", e);
}
}
private async Task<bool> TryStartServerAsync()
{
if (_server is not null)
return true;
try
{
var server = new AtSpiServer();
await server.StartAsync();
_server = server;
// Register any already-tracked windows.
foreach (var window in _trackedWindows)
{
var peer = TryGetWindowPeer(window);
if (peer is not null)
server.AddWindow(peer);
}
return true;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?
.Log(_platform, "AT-SPI server startup attempt failed: {Exception}", e);
return false;
}
}
private static AutomationPeer? TryGetWindowPeer(X11Window window)
{
try
{
if (window.InputRoot.RootElement is Control control)
return ControlAutomationPeer.CreatePeerForElement(control);
}
catch (Exception e)
{
// Window can be tracked before input root is available.
Logger.TryGet(LogEventLevel.Debug, LogArea.X11Platform)?
.Log(window, "AT-SPI could not resolve window automation peer yet: {Exception}", e);
}
return null;
}
private async Task EnableAccessibilityAsync()
{
await TryStartServerAsync();
}
private async Task DisableAccessibilityAsync()
{
if (_server is null)
return;
var server = _server;
_server = null;
await server.DisposeAsync();
}
}
}

9
src/Avalonia.X11/X11Platform.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop;
using Avalonia.FreeDesktop.AtSpi;
using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -27,6 +28,8 @@ namespace Avalonia.X11
internal class AvaloniaX11Platform : IWindowingPlatform
{
private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
private X11AtSpiAccessibility? _accessibility;
internal AtSpiServer? AtSpiServer => _accessibility?.Server;
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, X11EventDispatcher.EventHandler> Windows { get; } = new ();
public XI2Manager? XI2 { get; private set; }
@ -106,8 +109,14 @@ namespace Avalonia.X11
Compositor = new Compositor(graphics);
AvaloniaLocator.CurrentMutable.Bind<Compositor>().ToConstant(Compositor);
_accessibility = new X11AtSpiAccessibility(this);
_accessibility.Initialize();
}
internal void TrackWindow(X11Window window) => _accessibility?.TrackWindow(window);
internal void UntrackWindow(X11Window window) => _accessibility?.UntrackWindow(window);
public IntPtr DeferredDisplay { get; set; }
public IntPtr Display { get; set; }

19
src/Avalonia.X11/X11Window.cs

@ -1026,6 +1026,16 @@ namespace Avalonia.X11
return;
_cleaningUp = true;
// Remove from AT-SPI tree before closing
_platform.UntrackWindow(this);
if (_platform.AtSpiServer is { } atSpiServer
&& _inputRoot?.RootElement is Control atSpiControl)
{
var atSpiPeer = atSpiControl.GetAutomationPeer();
if (atSpiPeer is not null)
atSpiServer.RemoveWindow(atSpiPeer);
}
// Before doing anything else notify the TopLevel that ITopLevelImpl is no longer valid
if (_handle != IntPtr.Zero)
Closed?.Invoke();
@ -1112,6 +1122,15 @@ namespace Avalonia.X11
public void Show(bool activate, bool isDialog)
{
_mode.Show(activate, isDialog);
_platform.TrackWindow(this);
if (_platform.AtSpiServer is { } server
&& _inputRoot?.RootElement is Control c)
{
var peer = Avalonia.Automation.Peers.ControlAutomationPeer.CreatePeerForElement(c);
if (peer is not null)
server.AddWindow(peer);
}
}
public void Hide() => _mode.Hide();

28
src/tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DebugType>embedded</DebugType>
<IncludeSymbols>false</IncludeSymbols>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<AvDBusSourceGenPath>../../../external/Avalonia.DBus/src/Avalonia.DBus.SourceGen</AvDBusSourceGenPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(AvDBusSourceGenPath)/**/*.cs"
Exclude="$(AvDBusSourceGenPath)/obj/**/*.cs"
LinkBase="Avalonia.DBus.SourceGen" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)/$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<Import Project="../../../build/AnalyzerProject.targets" />
</Project>

6
src/tools/Avalonia.DBus.Generators/Avalonia.DBus.Generators.props

@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="DBusGeneratorMode" />
<CompilerVisibleProperty Include="AvDBusInternal" />
</ItemGroup>
</Project>
Loading…
Cancel
Save