CI fixes for the previous commit: - Windows: announce one BoundsChanged on the window root when the scene resizes, so UIA raises LayoutInvalidated again. Widgets still do not report their own bounds changes. - macOS: collapse identical pending notifications per element within one update, so one text selection change posts a single AXSelectedTextChanged. Created/Destroyed keep every occurrence, in order. - MinGW: link the UIA test harness with -municode and uuid, which fixes the nightly MinGW build. Backends: - Split the AT-SPI backend into a private header plus connection, events, tree/marshalling and method files. Break the 600-line D-Bus dispatcher into one handler per interface. - Split the UIA backend into a private header plus core, element provider and text range files. - Share hasState/hasAction in accessibilitybackend.hpp instead of three per-backend copies. CI and docs: - Run test_atspi.py --multi-window on Linux: closing a secondary window while an AT-SPI client stays connected. - Add docs/articles/uiaccessibilityarchitecture.md (layer map, files, invariants, per-backend layout, common tasks, test matrix) and link it from the articles index and the accessibility guide. - Add a unit test for the single layout notification on scene resize.
11 KiB
UI Accessibility
eepp can expose its UI widget hierarchy to the native accessibility services on Linux, macOS, and Windows. This allows screen readers, accessibility inspectors, and automation clients to discover widgets, read their state and values, follow focus, and perform supported actions.
The native backends are:
- AT-SPI on Linux;
NSAccessibilityon macOS;- Microsoft UI Automation (UIA) on Windows.
Other platforms currently use the null backend. Accessibility does not change how widgets are rendered and does not require the application to redraw continuously.
To work on the implementation itself, see UI accessibility architecture.
Enabling accessibility
Native accessibility is enabled by default on supported platforms. Applications using
UIApplication can select the policy in UIApplication::Settings:
UIApplication::Settings settings;
settings.accessibilityPolicy = AccessibilityPolicy::Auto;
UIApplication app( { 1280, 720, "Accessible application" }, settings );
The available policies are:
AccessibilityPolicy::Auto: use the platform default, which currently enables the native backend on supported desktop platforms;AccessibilityPolicy::Enabled: explicitly request the native backend;AccessibilityPolicy::Disabled: use the null backend and avoid native integration.
The EEPP_DISABLE_ACCESSIBILITY environment variable overrides Auto and Enabled and forces the
null backend. This is useful for diagnostics and performance comparisons:
EEPP_DISABLE_ACCESSIBILITY=1 ./my-application
A root UISceneNode can also be configured directly:
sceneNode->setAccessibilityPolicy( AccessibilityPolicy::Disabled );
Only root UI scenes own a native accessibility manager. Nested scenes participate through their host root scene, and every application-owned top-level window is exposed through its own native window representation.
Automatic widget semantics
Standard eepp widgets receive accessibility semantics automatically. This includes common controls such as:
- buttons;
- checkboxes and radio buttons;
- text labels, text inputs, and multiline text editors;
- combo boxes;
- sliders, spin boxes, and progress bars;
- tabs;
- menus and menu items;
- lists, tables, and trees;
- UI windows.
The framework resolves each widget's role, name, state, value, available actions, range, and text selection from the live UI. Applications normally do not need to construct or synchronize a separate accessibility tree.
Model-backed list, table, and tree rows are represented as virtual accessible nodes, so they do not
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.
Accessible names and descriptions
Use aria-label when a widget's visible content does not provide a useful name. Use
aria-description for additional instructions or context:
<TextInput id="project-name"
aria-label="Project name" />
<TextEdit id="description"
aria-label="Description"
aria-description="Multiline editor. Press Mod Tab to move to the next control." />
Use aria-hidden="true" for decorative or redundant widgets that should not appear in the native
accessibility hierarchy:
<Image src="decorative-separator"
aria-hidden="true" />
These properties are also available through C++:
widget->setAccessibilityLabel( "Project name" );
widget->setAccessibilityDescription( "Enter the project display name." );
widget->setAccessibilityHidden( false );
Prefer a concise label that identifies the control. A description should add information rather than repeat the label or visible text.
Custom widgets
A custom widget derived from a standard control normally inherits that control's automatic semantics. The resolver uses widget-family checks, so subclasses do not need to duplicate accessibility implementations.
For a new semantic control, set an explicit role and supply a useful label when one cannot be derived from visible content:
widget->setAccessibilityRole( AccessibilityRole::Button );
widget->setAccessibilityLabel( "Refresh projects" );
An explicit AccessibilityRole::None can keep a custom widget out of the semantic hierarchy. Use
setAccessibilityHidden( true ) when the widget and its accessible descendants should be hidden
from assistive technology.
Custom widgets should use the normal eepp state and action mechanisms whenever possible. Native backends query the shared accessibility resolver; platform-specific accessibility behavior should not be implemented inside individual widget classes.
Dynamic state and notifications
Standard widgets notify the accessibility manager when relevant UI state changes, including:
- keyboard focus;
- names and descriptions;
- values and text selection;
- enabled and visible state;
- checked, selected, and expanded state;
- widget creation and destruction;
- 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.
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 manager explicitly, but it should do so only after the new state is queryable.
Accessibility information is resolved on demand. Native provider objects identify live nodes and become unavailable safely after the corresponding widget, model item, scene, or window is destroyed.
Keyboard navigation
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.
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.
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.
Linux
Use Orca for a screen-reader pass. Accerciser and desktop accessibility inspectors can inspect the AT-SPI hierarchy and events. The repository provides automated validation and latency measurement:
python3 projects/scripts/test_atspi.py --executable bin/eepp-ui-accessibility
python3 projects/scripts/benchmark_atspi.py --executable bin/eepp-ui-accessibility
These commands need an active AT-SPI session bus and a usable graphical session.
Pass --hidden to test_atspi.py to keep all example windows hidden. Add --multi-window to
validate secondary-window closure, or --close-primary to validate that the surviving window
continues responding after the primary window is destroyed.
The --close-primary validation also closes the remaining window with its AT-SPI client still
connected and checks that the application exits cleanly.
macOS
Use VoiceOver and Accessibility Inspector. The repository's native semantic and lifecycle harness requires Accessibility permission for the process running Swift:
swift projects/scripts/test_macos_accessibility.swift \
--executable bin/eepp-ui-accessibility
The harness validates native discovery, properties, actions, text, notifications, model changes, stale elements, and multiple-window lifecycle behavior.
Windows
Use Narrator and Accessibility Insights for Windows. Windows SDK Inspect and AccEvent are useful secondary diagnostics. The native UIA harness is built with the Windows targets:
bin\unit_tests\eepp-windows-accessibility-tests.exe bin\eepp-ui-accessibility.exe
The harness validates late discovery, fragment navigation, UIA properties and control patterns, UTF-16 text ranges, events, hit testing, provider invalidation, and repeated multi-window creation 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.
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 without enumerating every preceding row. A client explicitly requesting all children still pays for that enumeration; the logical model is not restricted to its visible viewport.
Linux tracks querying clients by their accessibility-bus connection and becomes inactive when those connections disappear. Windows requires window-specific client activity rather than global UIA listeners alone. macOS does not provide equivalent per-client disconnect tracking, so after a native query its observation flag remains enabled for the application's lifetime. Windows can also retain the observation flag for a previously queried window while global UIA listeners remain connected: the global listening API does not identify which window they use.
Use EEPP_DISABLE_ACCESSIBILITY=1 as the disabled baseline when investigating a suspected
regression. Always profile optimized release binaries without AddressSanitizer. Accessibility work
must not depend on rendering, and native queries or notifications must never stall the render loop.
Current scope
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.
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 controls when an optional advanced pattern is absent.
The implementation currently targets Linux, macOS, and Windows desktop applications. Mobile and web accessibility bridges are outside the current scope.