mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-19 03:53:57 +03:00
Add accessible text, relations and live regions; keep dormant a11y free
Text:
- Ranged text access in code points (length, range, line bounds) that
never copies more than requested, plus a text revision (document UUID
and modification) so UIA and macOS cache their UTF-16 copies.
- Report document edits as exact change records, up to four per widget
and frame, then collapse to one whole-text change. Resets, loads and
document swaps report a whole-text change.
- The removed text is available only while an edit is being notified
(TextDocument::getNotifiedRemovedText()); DocumentContentChange stays
{ range, text }, so queued records (LSP) copy nothing extra.
- Suppressed text changes are keyed by node ref, so a document shared
with another window still reports its edits.
- UITextView reports NameChanged; UITextInput overrides onTextChanged()
to report ValueChanged.
Relations, live regions and shortcuts:
- aria-labelledby, aria-describedby and aria-live (API and CSS). Ids a
client has read are remembered; renaming, adding or removing their
widgets notifies the dependents. setId() copies the old id only when
it is remembered.
- Live regions and UISceneNode::announceForAccessibility(). A newer
change from the same widget replaces its pending announcement, and
hidden sources are dropped.
- Menu item shortcuts (std::string text and key) exposed as AT-SPI key
bindings, the UIA accelerator key and macOS command characters.
- ScrollTo for anything inside a scroll view
(UIScrollView::scrollIntoView()). Icon-only buttons are named from
their tooltip.
Dormant path (no client attached):
- Widget notifications are inline: one relaxed atomic load of a
process-wide count of active root scenes, no call.
- Scene destruction and scene or host changes never create a manager.
Without a client, notify() queues nothing and widget deletion does
not register parents to announce it. While only the root has an
identity, deletion skips the ancestor walk unless a remembered
relation target may be affected.
- When the last client disconnects, model-view sources release their
queried rows. Persistent handles count their registrations, so
handles held elsewhere survive (PersistentModelIndex::release()).
- Spin boxes no longer hide their input explicitly, and
AccessibilityProperties shrinks from 144 to 136 bytes.
Backends:
- AT-SPI: object:announcement, word and line granularity, ranged
reads, SetTextContents reported as its minimal diff. Watch
disconnects per client (NameOwnerChanged arg0 plus a non-blocking
NameHasOwner check) instead of for every bus name. Without a scene
thread pool, initialize on a detached thread that shares ownership,
so teardown never waits on D-Bus timeouts.
- UIA: notification events (resolved at runtime, Windows 10 1709+),
text ranges served from the revision cache, and a per-frame provider
drain that skips the lock when nothing is queued.
- macOS: announcements, a UTF-16 text cache per revision, and command
key attributes.
- EEPP_ACCESSIBILITY_TRACE=1 logs client activity transitions.
Fixes:
- UICodeEditor::isType() no longer dispatches to a subclass's
getType(), so a UITextEdit is a code editor again.
- ecode: dropped files open only in document editors, not in other
code editors such as a commit message box.
Tools, tests and docs:
- Widget inspector: role names, shortcut, relation and live rows, and
an accessibility audit (unnamed controls, broken relations).
- Accessibility example: live status, an icon-only button, and edit and
reset actions that test_atspi.py checks end to end.
- Regression tests for text records, relations, live regions,
disconnect and reconnect, scene lifecycle and id changes.
- Add the AccessibilityDormant benchmark (C++ new/new[] counted on the
measuring thread) and benchmark_accessibility_dormant.py, which fails
on missing measurements and compares against other builds with
--compare LABEL=PATH.
- Document the dormant-path rules, what survives a disconnection, and
the remaining platform limitations.
This commit is contained in:
@@ -75,9 +75,26 @@ need to be materialized as `UIWidget` objects merely for accessibility.
|
||||
While a cell is being edited, its editor widget is exposed as the only child of the virtual row
|
||||
(lists and trees) or cell (tables) it edits, and keyboard focus is reported on the editor.
|
||||
|
||||
Text editing support currently covers `UITextInput` and `UITextEdit`, including caret and selection
|
||||
queries/actions and native change notifications. `UICodeEditor` and an end-to-end screen-reader
|
||||
workflow in ecode are not part of this initial foundation.
|
||||
Text editing support covers `UITextInput`, `UITextEdit` and `UICodeEditor`, including caret and
|
||||
selection queries/actions and native change notifications. A code editor with a file is named after
|
||||
that file unless it has a label. Setting its text through an assistive client replaces the document
|
||||
contents as one undoable edit, so the file path and undo history are kept.
|
||||
|
||||
Text queries read only the range a client asks for, through a line index that is rebuilt once per
|
||||
edit, so moving the caret in a large file does not copy the file. Edits reach Linux clients as exact
|
||||
insert and delete records taken from the document. A wholesale replacement (a reset, a reload, or
|
||||
many edits in one frame, such as replace-all) is reported once as "everything removed, everything
|
||||
inserted". Windows and macOS need UTF-16 offsets, so they convert the text once per edit and reuse
|
||||
that copy for every query until the next edit.
|
||||
|
||||
Controls whose content is their own name, value and state (buttons, check boxes, radio buttons,
|
||||
labels, tabs, menu items, sliders, spin boxes, progress bars and images) do not expose their
|
||||
internal widgets, so a button's text is read once, as its name, and not again as a child label.
|
||||
|
||||
Menu items expose their keyboard shortcut (AT-SPI key binding, UIA `AcceleratorKey`, macOS menu
|
||||
command character and modifiers). Any widget inside a `UIScrollView` offers a scroll-into-view
|
||||
action, which scrolls every enclosing scroll view the least distance needed. The same behavior is
|
||||
available to applications as `UIScrollView::scrollIntoView( node )`.
|
||||
|
||||
## Accessible names and descriptions
|
||||
|
||||
@@ -90,9 +107,26 @@ Use `aria-label` when a widget's visible content does not provide a useful name.
|
||||
|
||||
<TextEdit id="description"
|
||||
aria-label="Description"
|
||||
aria-description="Multiline editor. Press Mod Tab to move to the next control." />
|
||||
aria-description="Multiline editor. Press Control Tab to move to the next control." />
|
||||
```
|
||||
|
||||
When the name is visible elsewhere, point at it instead of repeating it. `aria-labelledby` names a
|
||||
widget after the widget with that id, and `aria-describedby` uses another widget's text as the
|
||||
description. Both ids are resolved from the scene root. When the referenced widget's text changes,
|
||||
or it is added, removed or given another id, assistive clients are told that the dependent widget's
|
||||
name or description changed:
|
||||
|
||||
```xml
|
||||
<TextView id="user-label" text="User name" />
|
||||
<TextInput aria-labelledby="user-label" aria-describedby="user-hint" />
|
||||
<TextView id="user-hint" text="The name you sign in with" />
|
||||
```
|
||||
|
||||
Names resolve in this order: `aria-labelledby`, `aria-label`, the widget's own text (button text,
|
||||
label text, window title, code editor file name), then its tooltip. An icon-only button with a
|
||||
tooltip is therefore named by the tooltip. A tooltip that did not become the name is used as the
|
||||
description when no other description is set.
|
||||
|
||||
Use `aria-hidden="true"` for decorative or redundant widgets that should not appear in the native
|
||||
accessibility hierarchy:
|
||||
|
||||
@@ -107,8 +141,12 @@ These properties are also available through C++:
|
||||
widget->setAccessibilityLabel( "Project name" );
|
||||
widget->setAccessibilityDescription( "Enter the project display name." );
|
||||
widget->setAccessibilityHidden( false );
|
||||
widget->setAccessibilityLabelledBy( "user-label" );
|
||||
widget->setAccessibilityDescribedBy( "user-hint" );
|
||||
```
|
||||
|
||||
Setting a property to its current value does nothing, so re-applying styles is free.
|
||||
|
||||
Prefer a concise label that identifies the control. A description should add information rather
|
||||
than repeat the label or visible text.
|
||||
|
||||
@@ -147,7 +185,35 @@ Standard widgets notify the accessibility manager when relevant UI state changes
|
||||
- model and child hierarchy changes.
|
||||
|
||||
Bounds are queried on demand. Routine position and size changes do not emit per-widget native
|
||||
layout notifications; this avoids event floods while scrolling or running layout.
|
||||
layout notifications; this avoids event floods while scrolling or running layout. A window resize is
|
||||
announced once, on the window root.
|
||||
|
||||
### Announcements and live regions
|
||||
|
||||
Use an announcement for an event the user should hear without moving focus, such as a finished
|
||||
build:
|
||||
|
||||
```cpp
|
||||
sceneNode->announceForAccessibility( "Build finished" );
|
||||
sceneNode->announceForAccessibility( "Build failed", AccessibilityLive::Assertive );
|
||||
```
|
||||
|
||||
`Polite` waits until the screen reader finishes speaking; `Assertive` interrupts it. Announcements
|
||||
use UIA notifications (Windows 10 1709 and later), the AT-SPI `object:announcement` signal
|
||||
(at-spi2-core 2.46 and later, read by Orca) and `NSAccessibilityAnnouncementRequestedNotification`
|
||||
on macOS. Older systems ignore them.
|
||||
|
||||
For text that changes in place, such as a status bar, mark it as a live region instead:
|
||||
|
||||
```xml
|
||||
<TextView id="status" text="Ready" aria-live="polite" />
|
||||
```
|
||||
|
||||
Name and value changes of a widget inside a live region (`polite` or `assertive`) are announced,
|
||||
unless the widget or one of its ancestors is `aria-hidden`.
|
||||
Several changes to the same widget in one frame produce one announcement with the final text, so a
|
||||
progress label updated every frame does not flood the screen reader. Both APIs are free while no
|
||||
assistive client is active.
|
||||
|
||||
Applications using the standard widget APIs receive these notifications automatically. A custom
|
||||
control that changes semantic state outside the standard paths may notify its scene accessibility
|
||||
@@ -162,9 +228,9 @@ destroyed.
|
||||
Accessible controls should be keyboard focusable and have a logical focus order. Standard Tab and
|
||||
Shift+Tab navigation moves between controls.
|
||||
|
||||
Multiline text editors reserve Tab for inserting indentation. Use Mod+Tab to move to the next
|
||||
focusable widget and Mod+Shift+Tab to move to the previous one. `Mod` means Command on macOS and
|
||||
Control on other desktop platforms.
|
||||
Multiline text editors reserve Tab for inserting indentation. Use Control+Tab to move to the next
|
||||
focusable widget and Control+Shift+Tab to move to the previous one. This matches AppKit on macOS,
|
||||
where Command+Tab is reserved for switching applications.
|
||||
|
||||
Screen readers may provide an additional browse or navigation mode. Their commands and speech
|
||||
settings are controlled by the screen reader and are separate from eepp's keyboard focus handling.
|
||||
@@ -172,8 +238,14 @@ settings are controlled by the screen reader and are separate from eepp's keyboa
|
||||
## Testing an application
|
||||
|
||||
The `eepp-ui-accessibility` example contains representative controls, model-backed views, dynamic
|
||||
metadata, and multiple native windows. Build and run the optimized release target before using a
|
||||
platform accessibility tool.
|
||||
metadata, a live region, and multiple native windows. Build and run the optimized release target
|
||||
before using a platform accessibility tool.
|
||||
|
||||
The widget inspector (`UIWidgetInspector`) has an Accessibility tab with the selected widget's
|
||||
resolved role, name, description, value, shortcut, relations, states and actions, and an Audit tab
|
||||
that lists focusable controls without an accessible name and `aria-labelledby` /
|
||||
`aria-describedby` ids that match nothing. Selecting an issue selects the widget in the tree. The
|
||||
same check is available to tests as `AccessibilityWidgetResolver::audit( root )`.
|
||||
|
||||
### Linux
|
||||
|
||||
@@ -222,8 +294,13 @@ and destruction.
|
||||
## Performance behavior
|
||||
|
||||
Native accessibility backends are query-centered and create provider objects lazily. When no
|
||||
native client has interacted with the application, widget notifications take a cached inactive
|
||||
fast path and avoid resolving accessibility data or walking the widget hierarchy.
|
||||
native client has interacted with the application, widget notifications take an inline inactive
|
||||
fast path: one atomic load, no call, no allocation, and no accessibility data resolved. Editing
|
||||
text, building widgets (spin boxes included), deleting widget hierarchies and updating models
|
||||
allocate no more than they would without accessibility, as measured by C++ `new` on the UI thread
|
||||
(see the architecture guide's benchmark notes). When the last client disconnects, rows it
|
||||
queried are released, so model updates stop paying for them; a client connecting later starts
|
||||
fresh.
|
||||
|
||||
Model child counts and individual child queries do not instantiate every accessible row. Queried
|
||||
rows retain identities across insertion and removal, and model sibling indexes are resolved
|
||||
@@ -245,7 +322,10 @@ must not depend on rendering, and native queries or notifications must never sta
|
||||
|
||||
The first implementation focuses on the semantics used by standard eepp desktop widgets. Native
|
||||
platform APIs contain additional advanced interfaces that are not yet exposed uniformly, including
|
||||
some rich-document formatting, complex table metadata, and container-specific scrolling APIs.
|
||||
some rich-document formatting, complex table metadata, and container scroll patterns (scroll
|
||||
position and paging, as opposed to scrolling an element into view). HTML content (`UIRichText`,
|
||||
headings, links, lists) is not yet exposed as semantic elements. Relations are used to compute names
|
||||
and descriptions but are not published as native relation sets.
|
||||
|
||||
Do not advertise a native pattern or capability unless its required behavior is implemented.
|
||||
Assistive technologies can still navigate the semantic hierarchy and operate the supported
|
||||
|
||||
@@ -135,10 +135,47 @@ materializing a whole child list.
|
||||
view) or drops it (implementation children, detached widgets).
|
||||
4. `AccessibilityManager::notify()` invalidates caches, appends the event to the frame's queue and
|
||||
hands it to the backend immediately through `AccessibilityBackend::onEvent()`.
|
||||
5. The backend emits the native notification. AT-SPI emits D-Bus signals right away, except text
|
||||
changes, which are diffed once per frame. UIA and macOS queue the event and raise it from their
|
||||
next `update()`, on the UI thread and outside any widget or model callback. The manager's queue
|
||||
is cleared at the end of every `AccessibilityManager::update()`.
|
||||
5. The backend emits the native notification. AT-SPI emits D-Bus signals right away (caret and
|
||||
selection changes are compared once per frame). UIA and macOS queue the event and raise it from
|
||||
their next `update()`, on the UI thread and outside any widget or model callback. The manager's
|
||||
queue is cleared at the end of every `AccessibilityManager::update()`.
|
||||
|
||||
Text edits take an extra step before their `ValueChanged`. `UICodeEditor` and `UITextInput` pass
|
||||
the document's `DocumentContentChange` (range and inserted text) to
|
||||
`UIWidget::notifyAccessibilityTextChanged()`. The removed text is not part of the change record,
|
||||
so consumers that queue records (LSP clients) never copy it: the manager reads it, while the
|
||||
document is still notifying, from `TextDocument::getNotifiedRemovedText()`. `AccessibilityManager::onTextChanged()` turns the
|
||||
first four per element and frame into exact `AccessibilityTextChange` records (code-point offset,
|
||||
removed, inserted). After that, and for resets, loads, reloads and document swaps, it sends one
|
||||
whole-text change for the rest of the frame, so a replace-all costs one notification.
|
||||
`AccessibilityBackend::onTextChanged()` receives them; AT-SPI turns exact ones into `text-changed`
|
||||
signals immediately and a whole-text one into delete-all plus insert-all at its next update. A
|
||||
backend returns false for a change it ignores, and an ignored change does not count toward the
|
||||
frame's four. AT-SPI's `SetTextContents` reports its replacement as one minimal diff, so it
|
||||
suppresses the element's records through `AccessibilityManager::setSuppressedTextChanges()` while
|
||||
the action runs. Node refs are manager-local, so an editor in another window sharing the document
|
||||
still reports the change.
|
||||
|
||||
Announcements take a parallel path. `UISceneNode::announceForAccessibility()` and live regions (a
|
||||
`NameChanged` or `ValueChanged` from a widget with an `aria-live` ancestor and no `aria-hidden`
|
||||
one) append to `AccessibilityManager`'s announcement queue. A live-region entry remembers its
|
||||
source widget, and a later change from the same widget in the same frame replaces its text instead
|
||||
of queueing another entry. `AccessibilityManager::update()` hands the queue to
|
||||
`AccessibilityBackend::announce()` after the backend's own `update()`, so the frame's focus and
|
||||
state changes reach the client first. Live-region entries are checked again at delivery and
|
||||
dropped when their source was destroyed, hidden or made `aria-hidden` in the meantime.
|
||||
|
||||
Names and descriptions that come from `aria-labelledby` / `aria-describedby` depend on another
|
||||
widget. When a client reads such a widget's info, the manager remembers the referenced id (ids,
|
||||
not pointers, so no entry can dangle). A name change of a widget with a remembered id, an id
|
||||
change, or the arrival, removal or deletion of a subtree containing one, makes the manager scan
|
||||
the scene for dependents and notify `NameChanged` or `DescriptionChanged` for them. While no
|
||||
client has read a dependent, the check is one lookup in an empty set. `UIWidget::setId()` asks
|
||||
`AccessibilityManager::isRelationTarget()` (an allocation-free hash lookup) before renaming, and
|
||||
keeps a copy of the old id only when it is remembered. Dependents are notified after the id has
|
||||
changed, for the old id and then the new one (which may complete a relation that pointed at a
|
||||
missing widget), so a backend that reads the node from inside the notification sees the new
|
||||
relation.
|
||||
|
||||
## Rules the implementation relies on
|
||||
|
||||
@@ -148,17 +185,46 @@ These are the invariants that past bugs came from. Keep them when changing the c
|
||||
|
||||
Applications without an assistive client must pay close to nothing.
|
||||
|
||||
- `UIWidget::notifyAccessibilityEvent()` first checks `UISceneNode::hasActiveAccessibilityClients()`,
|
||||
a cached bit (`AccessibilityClientActive`). Nothing else runs while it is clear.
|
||||
- `UIWidget::notifyAccessibilityEvent()`, `notifyAccessibilityTextChanged()` and
|
||||
`notifyAccessibilityWholeTextChanged()` are inline. They test one process-wide counter of root
|
||||
scenes with an active client (`UIWidget::isAccessibilityActive()`, a relaxed atomic load) and
|
||||
only then call the out-of-line `deliver*()` implementation, which checks the scene's own bit
|
||||
(`UISceneNode::hasActiveAccessibilityClients()`, `AccessibilityClientActive`). Callers that
|
||||
compute a payload before notifying test `isAccessibilityActive()` first. Pick the event by
|
||||
overriding, not by checking the type at the call site: `UITextView::onTextChanged()` reports
|
||||
`NameChanged` and `UITextInput` overrides it to report `ValueChanged`. Release code for a dormant
|
||||
`UITextView::onTextChanged()` is one load, one test and a jump.
|
||||
- The bit is set when a backend reports a client (`hasActiveNativeClients()`, refreshed in
|
||||
`UISceneNode::update()`) or when a native query arrives (`onNativeClientObserved()`).
|
||||
`UISceneNode::update()`) or when a native query arrives (`onNativeClientObserved()`). Every
|
||||
change goes through `UISceneNode::setAccessibilityClientActive()`, which keeps the process-wide
|
||||
counter in sync and, when the last client goes away, calls
|
||||
`AccessibilityManager::onClientsDisconnected()` (see [rule 6](#6-what-survives-a-disconnection)).
|
||||
- Backends decide what "active" means. AT-SPI tracks client bus names and clears the bit when they
|
||||
disconnect. UIA requires window-specific activity. macOS has no disconnect signal, so the bit
|
||||
stays set after the first query.
|
||||
- Without a client, `AccessibilityManager::notify()` queues nothing: only identity invalidation
|
||||
(`Destroyed` without a related node) still reaches the backend, which detaches native wrappers
|
||||
on it. Removing a widget a disconnected client had seen invalidates its identity without
|
||||
resolving or registering its parent to announce the removal. While only the scene root has an
|
||||
identity, deleting any other widget returns before walking its ancestors, unless a client is
|
||||
active and has read a relation (`aria-labelledby`, `aria-describedby`) whose target may be in
|
||||
the deleted subtree.
|
||||
- Lifecycle bookkeeping never creates a manager: destruction, scene and host changes use
|
||||
`UISceneNode::getExistingAccessibilityManager()`, and the const `getAccessibilityManager()` does
|
||||
not create one at any nesting level.
|
||||
- Do not add per-frame or per-layout work that runs before this check. Bounds are queried on
|
||||
demand; there is deliberately no per-widget bounds notification.
|
||||
- `UISceneNode::announceForAccessibility()` returns before doing anything while no client is
|
||||
active, but a caller that formats its message first still pays for that formatting.
|
||||
|
||||
`benchmark_accessibility_inactive.py` guards this path in CI.
|
||||
`benchmark_accessibility_inactive.py` compares the enabled and environment-disabled backends of the
|
||||
same build: both run the same compiled-in hooks, so it cannot show costs they share.
|
||||
`benchmark_accessibility_dormant.py` measures those directly and can compare against the same
|
||||
benchmark built against another tree (`--compare LABEL=PATH`), such as an exported develop
|
||||
revision. Its allocation counts and bytes cover ordinary C++ `new`/`new[]` on the measuring thread
|
||||
only: not `malloc`, other threads or background work such as AT-SPI initialization. "No more
|
||||
allocations than without accessibility" means exactly that measurement. A run passes only when
|
||||
every sample exits cleanly and reports every expected measurement, guarded scenarios included.
|
||||
|
||||
### 2. Teardown happens once, top-down
|
||||
|
||||
@@ -187,7 +253,8 @@ sequence that happened. Only adjacent query-again hints (`ModelChanged`, `Childr
|
||||
events: dropping the second half of add/remove/add or on/off/on leaves clients with stale state.
|
||||
Backends that need batching do it themselves, per frame:
|
||||
|
||||
- AT-SPI diffs each changed text once.
|
||||
- AT-SPI compares each text's caret and selection once, and reports a whole-text replacement
|
||||
once.
|
||||
- UIA raises one focus change, for the final keyboard focus.
|
||||
- macOS keeps only the last focus change, and posts every other non-structural notification at
|
||||
most once per (event, element, related element). AppKit notifications carry no state, so a
|
||||
@@ -213,7 +280,28 @@ The manager, the resolver and every widget are UI-thread only.
|
||||
- Never walk the whole widget tree or the whole model per event or per query. Prefer the event
|
||||
dispatcher's focus node, `getIndexInParent()` and source lookups over scanning children.
|
||||
- Building `AccessibilityNodeInfo` with the value copies the text of a text widget; request it only
|
||||
where the value is needed.
|
||||
where the value is needed. Text interfaces use `getTextLength()`, `getTextRange()` and
|
||||
`getTextLineBounds()`, which read only what they return. Offsets convert through a line index of
|
||||
the document (`lineStarts()` in the resolver), keyed by `getTextRevision()`: the document's UUID
|
||||
and modification id, which every content change bumps. Backends that need a converted copy
|
||||
(UTF-16 for UIA and AppKit) cache it per revision.
|
||||
- Leaf controls (`AccessibilityWidgetResolver::isLeafRole()`) have no exposed children. Events and
|
||||
focus from inside one are reported on the control (`getLeafOwner()`), so the inner widgets never
|
||||
get identities.
|
||||
|
||||
### 6. What survives a disconnection
|
||||
|
||||
When a scene's last client disconnects, `onClientsDisconnected()` resets every model-view source:
|
||||
queried rows are dropped together with their persistent model registrations, and backends detach
|
||||
their native wrappers (`onSourceInvalidated()`). Without that, the model would keep updating one
|
||||
persistent handle per queried row on every insert, delete and move for as long as it lives.
|
||||
Identities are never reused, so a reconnecting client starts from fresh row identities and a stale
|
||||
one stays invalid. Widget identities and metadata (labels, relations, live regions) are kept.
|
||||
|
||||
The model only drops a persistent handle when all its registrations are released
|
||||
(`PersistentModelIndex::release()`); handles other consumers registered are unaffected. A queried
|
||||
row's registration is released when its node is dropped, so pruned, reset and destroyed sources do
|
||||
not leave handles behind either.
|
||||
|
||||
## Backends
|
||||
|
||||
@@ -223,11 +311,17 @@ One `AtSpi::AtSpiApplication` is shared by all windows of the process and owns a
|
||||
connection to the accessibility bus. libdbus is loaded at runtime (`DBusLibrary`), so there is no
|
||||
build-time D-Bus dependency. Object paths encode `/org/eepp/a11y/<manager>/<source>/<id>`.
|
||||
|
||||
A peer other than the registry that calls the application becomes a client. Its disconnection is
|
||||
watched with a `NameOwnerChanged` match on its own unique name, not a bus-wide match that would
|
||||
wake the application for every program joining or leaving the bus. A non-blocking `NameHasOwner`
|
||||
check sent after the match covers a client that left before the match was installed: the bus
|
||||
processes both requests in order.
|
||||
|
||||
| File | Contents |
|
||||
| --- | --- |
|
||||
| `accessibilitybackendatspi.hpp` | `DBusLibrary` and the `AtSpiApplication` declaration, with members grouped by implementing file. |
|
||||
| `accessibilitybackendatspi.cpp` | Connection setup and registry embedding, I/O thread, per-frame dispatch, client tracking, `AtSpiAccessibilityBackend`. |
|
||||
| `accessibilitybackendatspievents.cpp` | Manager events to `org.a11y.atspi.Event.*` signals; text snapshots and once-per-frame text diffs (`text-changed`, `text-caret-moved`, `text-selection-changed`). |
|
||||
| `accessibilitybackendatspievents.cpp` | Manager events to `org.a11y.atspi.Event.*` signals; text change records to `text-changed`, and once-per-frame caret and selection comparison (`text-caret-moved`, `text-selection-changed`). Keeps no text contents. |
|
||||
| `accessibilitybackendatspitree.cpp` | Path/ref conversion, navigation, role and state mapping, action names, D-Bus marshalling helpers. |
|
||||
| `accessibilitybackendatspimethods.cpp` | Incoming calls: `handleMessage()` routes to one handler per interface (`Accessible`, `Component`, `Action`, `Text`, `EditableText`, `Application`, `Properties`, `Cache`). |
|
||||
|
||||
@@ -267,6 +361,11 @@ every backend: `role()` and `roleName()` in `accessibilitybackendatspitree.cpp`,
|
||||
AT-SPI `appendStates()` / `actionName()`, UIA patterns in the provider's `QueryInterface()` and
|
||||
`GetPatternProvider()`, macOS action names and attributes.
|
||||
|
||||
**Announce something.** Call `UISceneNode::announceForAccessibility()`, or mark the widget that
|
||||
changes with `aria-live`. A backend implements `AccessibilityBackend::announce()`; AT-SPI emits
|
||||
`Announcement` on the root object, UIA calls `UiaRaiseNotificationEvent` (looked up at runtime),
|
||||
macOS posts `NSAccessibilityAnnouncementRequestedNotification` on the window.
|
||||
|
||||
**Answer a new AT-SPI method.** Add it to the matching `handle<Interface>()` in
|
||||
`accessibilitybackendatspimethods.cpp`. Return `1` for "not handled" so the dispatcher replies with
|
||||
`UnknownMethod`.
|
||||
@@ -284,22 +383,42 @@ or wrong.
|
||||
| macOS backend semantics | `src/tests/unit_tests/accessibility_macos_tests.mm` | macOS unit tests. |
|
||||
| AT-SPI end to end | `projects/scripts/test_atspi.py` (default, `--multi-window`, `--close-primary`) and `benchmark_atspi.py` | Linux CI, inside `dbus-run-session` and Xvfb. |
|
||||
| Inactive overhead | `projects/scripts/benchmark_accessibility_inactive.py` | Linux CI. |
|
||||
| Dormant allocations and costs | `src/benchmarks/accessibility_dormant_benchmark.cpp` (`eepp-benchmarks --filter='AccessibilityDormant.*'`), summarized by `projects/scripts/benchmark_accessibility_dormant.py [--compare LABEL=PATH]` | Locally, Release builds. Allocation counts are deterministic and asserted where they guard a fix; times are indicative. |
|
||||
| UIA end to end | `src/tests/windows_accessibility/main.cpp` (`eepp-windows-accessibility-tests`) | Windows CI. Under Wine it builds and finds the window, but Wine's UIA client lacks several APIs, so most checks report `E_NOTIMPL`. |
|
||||
| NSAccessibility end to end | `projects/scripts/test_macos_accessibility.swift`; `benchmark_macos_accessibility.swift` locally | macOS CI. |
|
||||
|
||||
Useful tools:
|
||||
|
||||
- the runtime inspector's *Accessibility* tab shows exactly what a widget projects;
|
||||
- the runtime inspector's *Accessibility* tab shows exactly what a widget projects, and its *Audit*
|
||||
tab (`AccessibilityWidgetResolver::audit()`) lists unnamed focusable controls and broken
|
||||
`aria-labelledby` / `aria-describedby` ids;
|
||||
- `EEPP_DISABLE_ACCESSIBILITY=1` runs with the null backend, the baseline for performance and
|
||||
behavior comparisons;
|
||||
- Accerciser (Linux), Accessibility Inspector (macOS) and Inspect / Accessibility Insights
|
||||
(Windows) show the native tree;
|
||||
- to run the AT-SPI scripts without touching your desktop session, wrap them in
|
||||
`dbus-run-session` as CI does.
|
||||
`dbus-run-session` as CI does;
|
||||
- `EEPP_ACCESSIBILITY_TRACE=1` prints client activity transitions, and AT-SPI client connections
|
||||
and disconnections, to stderr. It shows what activates an application that has no screen reader
|
||||
running.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Enumerating all children of a model view is O(rows).
|
||||
- macOS cannot detect that a client went away, so its active bit stays set after the first query.
|
||||
- `UICodeEditor` is not exposed; only `UITextInput` and `UITextEdit` provide text.
|
||||
Any process that queries one of the application's accessibility elements (not only screen
|
||||
readers) activates it for the rest of its life.
|
||||
- Windows refreshes `UiaClientsAreListening()` on every scene update; a cheaper cadence needs
|
||||
native validation that listener changes are still observed promptly.
|
||||
- Linux connects to the accessibility bus at startup, whether or not a client will ever attach,
|
||||
so applications are discoverable by tools started later and by clients that never set the
|
||||
`org.a11y.Status` flags. Gating the connection on those flags (as Qt does when no X11 root
|
||||
window atom publishes the bus) would trade that discovery for startup work; it is a policy
|
||||
choice, not a transparent optimization. Initialization runs off the UI thread: on the scene's
|
||||
thread pool, or on a detached thread when the scene has none. Either way the task shares
|
||||
ownership of the AT-SPI application, so tearing the backend down never waits for pending D-Bus
|
||||
calls or their timeouts; whichever owner releases it last destroys it.
|
||||
- The line index is rebuilt in full after each edit (about 3 ms for a 100,000-line document),
|
||||
paid only while a client is querying that document.
|
||||
- HTML content (`UIRichText`, headings, links, lists) is not exposed as semantic elements.
|
||||
- Only one cell editor (the view's active editor) is exposed inside a model view.
|
||||
|
||||
Reference in New Issue
Block a user