Down and down, exploring the implementation of the accessibility subsystem

Normally, a visually impaired user never encounters the implementation details of the pieces which allow a screen reader to know about accessibility related events and objects. Even a developer writing, say, a GTK application, does not need to care. Actually, even Firefox and Chromium developers are just using the ATK library. Of course, when you are writing a screen reader, you begin to care a little, but you will be most likely using the AT-SPI2 library, which abstracts all the communication and related details, so you have only your trees of accessibility objects, and, yes, the events, which start to be a little bit rough, because they start looking like the low-level ones. But, for all the curious readers, let’s look what lies below these layers.

Getting in

As is somewhat common on modern Linux systems, the communication is carried through DBus calls.

However, there’s a twist. The accessible objects live on a separate bus, not on the system or session one. Fortunately, finding the correct address is not so hard, or at least is not when you have a DBus library at hand. For example, using the dasbus library, it looks like this (note we’ll use the same library for all the following examples as well):

from dasbus.connection import SessionMessageBus

session_bus = SessionMessageBus()
a11y_bus_info = session_bus.get_proxy("org.a11y.Bus", "/org/a11y/bus")
address = a11y_bus_info.GetAddress()

Getting the tree

Now, when we know where to go next, we can create another DBus connection, now to the accessibility bus proper:

from dasbus.connection import AddressedMessageBus

a11y_bus = AddressedMessageBus(address)

And now, we get to the concept of an AT-SPI registry. It handles the org.a11y.Bus object on the session bus, and provides information about the root of the accessibility tree. It also handles event registration, and related things. Unfortunately, getting the root of the accessibility tree requires knowing its object path, which can be gleaned only from the at-spi2-core sources, so, yes, it’s an implementation detail. But the search was not so hard, so, you get to the root of the accessible tree as follows:

root = a11y_bus.get_proxy("org.a11y.atspi.Registry", "/org/a11y/atspi/accessible/root")

Yes, the path is returned when your register you application as part of the accessibility tree, but you might not want to do that for a simple information retrieval.

Exploring the tree

At this point, we’ve got the root of the whole tree. The object definitely implements the org.a11y.atspi.Accessible interface. What we will do with it depends only on our imagination, so we’ll, for example, print the names of all applications, e. g. its immediate children.

for idx in range(root.ChildCount):
    (owner, path) = root.GetChildAtIndex(idx)
    child = a11y_bus.get_proxy(owner, path)
    print(f"Application #{idx + 1}: {child.Name}")

The Accessible interfacehas also aGetInterfacesmethod, which tells you what other interfaces are supported by the particular accessible object. You can encouter theComponent` interface in most cases, and others based on what kind of object you actually got.

Becoming an AT-SPI2 information provider

This section will not be as detailed as the previous ones, namely, there will not be an example, but I suppose that the really curious ones can easily implement the needed stuff, it is really not so hard, after you know what you are doing.

We already saw how the AT-SPI2 accessibility tree looks like from the point of view of a screen reader author. Providing these information is not so different from consuming, though, you only have to put the objects on the a11y bus and you have to implement the necessary interfaces.

You must implement the org.a11y.atspi.Accessible interface for each of the objects. You likely want the org.a11y.atspi.Component interface as well, so a screen reader can make sense of how the controls are layd-out in your application in terms of their sizes and positions.

For the top-level object of your application, it is necessary to implement the org.a11y.atspi.Application interface as well, but you likely don’t need the Component interface in this particular context.

Becoming part of the tree

When you have at least the application object ready, you can register it. This is done through the org.a11y.atspi.Socket interface, namely the Embed method, which receives the unique bus name and the object path of the application object as arguments. It also sets the Id property of your Application interface, so you better be ready for that, and then it returns the object path of the accessible tree root.

There’s also an Unembed method, which allows you to deregister your application, but it is not strictly necessary to call it, as the application is deregistered automatically when its process exits.


And, that’s basically it. Of course, there are much more interfaces to use or implement for specific objects, like the Text interface for text controls, Table interface for tables, etc., but the principles of these interfaces are the same, just a normal DBus programming with all it entails.





Leave a Reply

Your email address will not be published. Required fields are marked *