Split accessibility backends, fix CI regressions and document architecture

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.
This commit is contained in:
Martín Lucas Golini
2026-10-09 01:02:11 -03:00
parent d27eae5927
commit 4baa4ae4b1
19 changed files with 4524 additions and 3746 deletions
@@ -107,6 +107,15 @@ jobs:
/usr/bin/python3 projects/scripts/test_atspi.py --hidden --close-primary \
> bin/unit_tests/output/atspi-regressions.log
cat bin/unit_tests/output/atspi-regressions.log
- name: Native AT-SPI Secondary Window Tests
# Closes a secondary window while the client stays connected; the primary must keep answering.
timeout-minutes: 2
run: |
mkdir -p bin/unit_tests/output
env -u AT_SPI_BUS_ADDRESS dbus-run-session -- projects/scripts/xvfb-run-eepp \
/usr/bin/python3 projects/scripts/test_atspi.py --hidden --multi-window \
> bin/unit_tests/output/atspi-multi-window.log
cat bin/unit_tests/output/atspi-multi-window.log
- name: AT-SPI Benchmark
timeout-minutes: 5
run: |
+1
View File
@@ -13,6 +13,7 @@ supported behavior; class headers and current source code remain the reference f
| Inspect or automate a running UI | [Runtime UI inspector](ui_inspector.md) | [Application UI authoring](ui_authoring.md) for a layout debugging workflow |
| Bind application data or commands to widgets | [UI data binding](ui_databinding.md) | [Application UI authoring](ui_authoring.md) for screen structure |
| Make native widgets accessible | [UI accessibility](uiaccessibility.md) | [Application UI authoring](ui_authoring.md) for screen structure |
| Work on the accessibility implementation | [UI accessibility architecture](uiaccessibilityarchitecture.md) | [UI accessibility](uiaccessibility.md) for the application-facing behavior |
| Add an interactive chart | [UI charts](ui_charts.md) | [Application UI authoring](ui_authoring.md) for placement and sizing |
| Work on `eterm` Kitty support | [Keyboard protocol](eterm_kitty_keyboard.md), [graphics protocol](eterm_kitty_graphics.md) | The corresponding `src/modules/eterm/` implementation and tests |
+2
View File
@@ -13,6 +13,8 @@ The native backends are:
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](uiaccessibilityarchitecture.md).
## Enabling accessibility
Native accessibility is enabled by default on supported platforms. Applications using
@@ -0,0 +1,298 @@
# UI Accessibility Architecture
This article is for people working on eepp's accessibility implementation. For the application
facing API (roles, labels, policies and testing an application) read
[UI accessibility](uiaccessibility.md) first.
The implementation has two halves:
- a small, platform independent **core** that projects the live widget tree into an accessible
tree on demand;
- three **native backends** that translate that projection to AT-SPI (Linux/FreeBSD),
NSAccessibility (macOS) and UI Automation (Windows).
Most day to day work (new widgets, new roles, lifecycle fixes) happens in the core. The backends
mostly change when a platform feature is added.
## Map
```text
Screen reader / inspector / automation client
|
AT-SPI (D-Bus) | NSAccessibility | UI Automation (COM)
|
+------------------v-------------------------------------------+
| Native backend (one per platform) accessibilitybackend*|
| - answers native queries by calling the manager |
| - turns manager events into native notifications |
+------------------+-------------------------------------------+
| AccessibilityNodeRef, AccessibilityNodeInfo,
| AccessibilityPendingEvent (accessibility.hpp)
+------------------v-------------------------------------------+
| AccessibilityManager (one per top-level UISceneNode) |
| - node identities, tree navigation, focus, hit testing |
| - event queue, teardown and ownership rules |
+--------+-------------------------+---------------------------+
| |
+--------v-----------+ +---------v------------------------------+
| Widget resolver | | AccessibilitySource (virtual nodes) |
| role/name/state/ | | AccessibilityModelViewSource: rows, |
| actions/text of a | | cells and the cell editor of list, |
| live UIWidget | | table and tree views |
+--------^-----------+ +----------------------------------------+
|
UIWidget hooks: notifyAccessibilityEvent(), ~UIWidget, Node::childRemove,
UISceneNode lifecycle, widget-specific state changes
```
### Files
| Area | Files | What lives there |
| --- | --- | --- |
| Public types | `include/eepp/ui/accessibility/accessibility.hpp` | Roles, states, actions, events, `AccessibilityNodeRef`, `AccessibilityNodeInfo`. |
| Manager | `accessibilitymanager.{hpp,cpp}` | Identities, navigation, focus, hit testing, `notify()`, teardown. |
| Widget semantics | `accessibilitywidgetresolver.{hpp,cpp}` | Role, name, value, range, text, state and actions for each standard widget type; focus projection; event routing. |
| Virtual nodes | `accessibilitysource.hpp`, `accessibilitymodelviewsource.{hpp,cpp}` | The source interface and its model-view implementation. |
| Backend interface | `src/eepp/ui/accessibility/accessibilitybackend.{hpp,cpp}` | `AccessibilityBackend`, the null backend, shared helpers. |
| AT-SPI | `accessibilitybackendatspi.hpp` and `accessibilitybackendatspi{,events,tree,methods}.cpp` | See [AT-SPI](#at-spi-linux-and-freebsd). |
| UI Automation | `accessibilitybackenduia.hpp` and `accessibilitybackenduia{,provider,text}.cpp` | See [UI Automation](#ui-automation-windows). |
| NSAccessibility | `accessibilitybackendmacos.mm` | See [NSAccessibility](#nsaccessibility-macos). |
| Widget hooks | `uiwidget.cpp`, `uiscenenode.cpp`, `scene/node.cpp`, and the individual widgets | Notifications at the point where state changes. |
| Inspector | `src/eepp/ui/tools/uiwidgetinspector.cpp` | The inspector's *Accessibility* tab shows the projected node of the selected widget. |
All backend and source files are private (`src/`), so they can change freely without affecting
the public API.
## Core concepts
### Node references
Every accessible node is an `AccessibilityNodeRef { source, id }`.
- `source == 1` (the manager's widget source) means `id` names a live `UIWidget`. The manager
keeps `mWidgetIds` / `mWidgets` to map between the two.
- Any other `source` is an `AccessibilitySource` that resolves its own ids. Today the only source
is the model-view source created for each `UIAbstractTableView` (list, table and tree views).
Identities are **lazy**: a widget gets an id the first time a client reaches it (`getNodeRef()`).
Ids are never reused, so a stale reference held by a client simply stops resolving
(`isValid()` returns false) instead of pointing at a different widget.
### What is exposed
A widget is an accessible element when its resolved role is not `None`
(`UIWidget::isAccessibilityElement()`). Containers with role `None` are transparent: their
element descendants are reported as children of the nearest element ancestor. A widget marked
hidden (`aria-hidden`) hides its whole subtree.
`AccessibilityWidgetResolver` derives semantics from the widget type (`isType()` chains) unless
the application overrode them through the widget's accessibility properties. It is the single
place that knows how a `UISpinBox`, `UIComboBox`, `UITextEdit`, and so on map to roles, states and
actions.
### Model views
List, table and tree views recycle their cell widgets, so those widgets are implementation
detail. The model-view source exposes rows (and cells for tables) as virtual nodes keyed by
`PersistentModelIndex`, so identities follow rows when the model inserts or removes them.
- Child counts and child access are lazy: a 100k row model creates nodes only for the rows a
client asks for. Enumerating every child is still O(rows), as in other toolkits.
- While a cell is edited, the editor widget is a real widget exposed as the only child of the
virtual row (lists and trees) or cell (tables) it edits.
- `AccessibilityWidgetResolver::getOwningModelView()` is the one test for "is this widget an
implementation child of a view". Use it rather than walking ancestors by hand.
### Focus
Focus is projected, not reported raw: the focused node may be an implementation child (a spin
box's text input) or a cell widget. `AccessibilityWidgetResolver::getFocusOwner()` walks from the
event dispatcher's focus node to the element that represents it. Both
`AccessibilityManager::getKeyboardFocusedNode()` and the `Focused` state use it, so they always
agree. Ancestors that merely contain the focus are not `Focused`.
## The two flows
### Queries (client to eepp)
1. The client asks the backend (D-Bus method, `NSAccessibility` attribute or COM call).
2. The backend translates the native object to an `AccessibilityNodeRef` and calls the manager:
`getNodeInfo()`, `getParent()`, `getChildCount()` / `getChild()`, `getIndexInParent()`,
`hitTest()`, `getKeyboardFocusedNode()`, `performAction()`.
3. The manager resolves the ref to a widget (resolver) or to a source.
Queries always run on the UI thread (see [Threading](#threading)). Use the cheapest query that
answers the question: `getNodeInfo( ref, false, false )` skips copying the value and computing
text offsets, which matters for large text documents; `getChild()` and `getIndexInParent()` avoid
materializing a whole child list.
### Events (eepp to client)
1. A widget changes and calls `notifyAccessibilityEvent( event )`.
2. If no native client is active, this returns immediately (see [the gate](#1-the-dormant-fast-path)).
3. `AccessibilityWidgetResolver::getEventTarget()` routes the event to its semantic owner (a
dropdown's state belongs to its combo box; an implementation child's focus belongs to its
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, either right away (AT-SPI, UIA) or on the next
`update()` (macOS, and AT-SPI text diffs). The manager's queue is cleared at the end of every
`AccessibilityManager::update()`.
## Rules the implementation relies on
These are the invariants that past bugs came from. Keep them when changing the code.
### 1. The dormant fast path
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.
- The bit is set when a backend reports a client (`hasActiveNativeClients()`, refreshed in
`UISceneNode::update()`) or when a native query arrives (`onNativeClientObserved()`).
- 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.
- 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.
`benchmark_accessibility_inactive.py` guards this path in CI.
### 2. Teardown happens once, top-down
Widget destruction is where most of the dangerous bugs were.
- `~UIWidget` calls `AccessibilityManager::onWidgetDelete( this )` **before** `onClose()`, then sets
`NODE_FLAG_DESTROYING`. The manager invalidates the whole subtree at once
(`removeSubtreeIdentities()`), so descendants' destructors find nothing to do and never touch a
partially destroyed ancestor.
- Anything that walks ancestors must stop at a destroying node: `getNodeRef()`, `getParent()`,
`getEventTarget()` and `getFocusOwner()` all do.
- Deleting something no client has seen sends nothing and allocates no identity. Use
`getWidgetParent()`, which does not register the widget, when you only need the parent of a widget
that is going away.
- `~UISceneNode` destroys its manager (and therefore the native backend) **before** deleting its
children, while the root is still alive, and then clears `mRoot`. A closing scene
(`NODE_FLAG_CLOSE`) returns no manager from either `getAccessibilityManager()` overload.
- Nested scenes (`UISceneNode` inside another scene) share the host's manager. Moving or deleting
them goes through `onSubtreeRemoved()`.
### 3. Event order is meaningful
`notify()` hands each event to the backend synchronously, so the backend sees exactly the
sequence that happened. Only adjacent query-again hints (`ModelChanged`, `ChildrenChanged`,
`BoundsChanged`) with identical arguments are coalesced. Never coalesce state or structural
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 (AT-SPI diffs text once per frame, macOS keeps only
the last focus change).
### 4. Threading
The manager, the resolver and every widget are UI-thread only.
- **AT-SPI**: an I/O thread only `poll()`s the D-Bus socket and wakes the event loop. Messages are
read and dispatched on the UI thread in `AtSpiApplication::update()`, at most 256 per frame.
- **UIA**: COM calls arrive on arbitrary threads. Every provider method goes through
`UIAutomationProviderContext::invoke()`, which runs the callback directly on the UI thread or
posts a window message and waits up to `ProviderCallTimeoutMilliseconds`. The pending request
holds a COM reference on the provider so a timed-out call cannot free it mid-callback.
`UiaDisconnectProvider` is deferred to `update()` because outgoing COM calls can re-enter the
STA during widget destruction.
- **macOS**: AppKit is main-thread only; events are queued and delivered on the next `update()`.
### 5. Cost is proportional to what the client asks for
- 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.
## Backends
### AT-SPI (Linux and FreeBSD)
One `AtSpi::AtSpiApplication` is shared by all windows of the process and owns a private
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>`.
| 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`). |
| `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`). |
### UI Automation (Windows)
Each top-level window has a `Uia::UIAutomationProviderContext`, created with the backend, which
subclasses the window to answer `WM_GETOBJECT` and caches one `UIAutomationProvider` per node.
| File | Contents |
| --- | --- |
| `accessibilitybackenduia.hpp` | The context (including `invoke()`), shared structs, helper declarations, provider and text range declarations. |
| `accessibilitybackenduia.cpp` | Provider cache and invalidation, window subclass, manager events to UIA events, `UIAutomationAccessibilityBackend`. |
| `accessibilitybackenduiaprovider.cpp` | The element provider: fragment navigation, properties and every control pattern; role and `VARIANT` helpers. |
| `accessibilitybackenduiatext.cpp` | `ITextRangeProvider`, UTF-16/code point conversion and text unit navigation. |
### NSAccessibility (macOS)
`accessibilitybackendmacos.mm` holds `MacAccessibilityState` (one per window: element cache and
manager access), `EEPPMacAccessibilityElement` (an `NSAccessibilityElement` subclass per node),
`MacAccessibilityRegistry` (process-wide window list and event delivery) and the backend class.
It is still a single file because it can only be built and tested on a macOS host.
## Common tasks
**Expose a new standard widget.** Add its role, name, state, actions and, if applicable, value,
range or text to `accessibilitywidgetresolver.cpp`. Call `notifyAccessibilityEvent()` from the
widget where its semantic state changes (for example `StateChanged` when it is checked). If it
contains implementation children, make sure they resolve to the widget (role `None`, hidden, or
routed in `getEventTarget()`), and add a unit test in `accessibility_tests.cpp`.
**Add a role.** Add it to `AccessibilityRole` (`accessibility.hpp`), map it in the resolver, then in
every backend: `role()` and `roleName()` in `accessibilitybackendatspitree.cpp`, `controlType()` in
`accessibilitybackenduiaprovider.cpp`, and `nativeRole()` / `nativeSubrole()` in
`accessibilitybackendmacos.mm`.
**Add an action or state.** Add the enum value, produce it in the resolver, then map it per backend:
AT-SPI `appendStates()` / `actionName()`, UIA patterns in the provider's `QueryInterface()` and
`GetPatternProvider()`, macOS action names and attributes.
**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`.
**Add a virtual node type.** Implement `AccessibilitySource` and create it from
`AccessibilityManager::sourceFor()`. Only implement the optional hooks (`getIndexInParent()`,
`getSelectedChildren()`, `getEmbeddedWidgetParent()`) when the default fallback would be too slow
or wrong.
## Testing and debugging
| Layer | How it is tested | Where it runs |
| --- | --- | --- |
| Core (manager, resolver, model-view source, lifecycle) | `src/tests/unit_tests/accessibility_tests.cpp`, using the null backend and `onNativeClientObserved()` to open the notification gate. | Every platform's unit test job. |
| 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. |
| 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;
- `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.
## 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.
- Only one cell editor (the view's active editor) is exposed inside a model view.
+5
View File
@@ -2154,6 +2154,11 @@ solution "eepp"
files { "src/tests/windows_accessibility/*.cpp" }
links { "uiautomationcore", "ole32", "oleaut32" }
build_link_configuration( "eepp-windows-accessibility-tests", true )
-- MinGW: the harness uses wmain, and CLSID_CUIAutomation lives in libuuid.
configuration "gmake"
links { "uuid" }
linkoptions { "-municode" }
configuration {}
end
if os.isfile("external_projects.lua") then
+5
View File
@@ -2170,6 +2170,11 @@ workspace "eepp"
files { "src/tests/windows_accessibility/*.cpp" }
links { "uiautomationcore", "ole32", "oleaut32" }
build_link_configuration( "eepp-windows-accessibility-tests", true )
-- MinGW: the harness uses wmain, and CLSID_CUIAutomation lives in libuuid.
filter "action:gmake*"
links { "uuid" }
linkoptions { "-municode" }
filter {}
end
if os.isfile("external_projects.lua") then
@@ -8,6 +8,14 @@ namespace EE { namespace UI {
class AccessibilityManager;
inline bool hasState( AccessibilityState states, AccessibilityState state ) {
return ( static_cast<Uint64>( states ) & static_cast<Uint64>( state ) ) != 0;
}
inline bool hasAction( AccessibilityActions actions, AccessibilityAction action ) {
return ( actions & accessibilityActionMask( action ) ) != 0;
}
class AccessibilityBackend {
public:
virtual ~AccessibilityBackend() = default;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
#ifndef EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDATSPI_HPP
#define EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDATSPI_HPP
#include <eepp/config.hpp>
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD
#include "accessibilitybackend.hpp"
#include <atomic>
#include <cstring>
#include <eepp/core/containers.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/accessibility/accessibilitymanager.hpp>
#include <memory>
#include <string>
#include <thread>
#include <vector>
/**
* AT-SPI (Linux/FreeBSD) accessibility backend.
*
* One AtSpiApplication is shared by every window of the process and owns a private connection
* to the accessibility bus. libdbus is loaded at runtime, so eepp has no build-time D-Bus
* dependency. The implementation is split by concern:
* - accessibilitybackendatspi.cpp: connection, I/O thread, dispatch, client tracking and
* the AccessibilityBackend entry point.
* - accessibilitybackendatspievents.cpp: manager events -> AT-SPI signals, text diffing.
* - accessibilitybackendatspitree.cpp: object paths, roles, states and D-Bus marshalling.
* - accessibilitybackendatspimethods.cpp: incoming method calls, one handler per interface.
*/
namespace EE { namespace UI { namespace AtSpi {
constexpr int DBusMessageTypeError = 3;
struct DBusConnection;
struct DBusMessage;
struct DBusError;
struct DBusMessageIter {
void* dummy1;
void* dummy2;
Uint32 dummy3;
int dummy4;
int dummy5;
int dummy6;
int dummy7;
int dummy8;
int dummy9;
int dummy10;
int dummy11;
int pad1;
void* pad2;
void* pad3;
};
using DBusHandlerResult = int;
using DBusDispatchStatus = int;
using DBusMessageFunction = DBusHandlerResult ( * )( DBusConnection*, DBusMessage*, void* );
constexpr DBusDispatchStatus DBusDispatchDataRemains = 0;
struct DBusObjectPathVTable {
void ( *unregisterFunction )( DBusConnection*, void* );
DBusMessageFunction messageFunction;
void ( *padding1 )( void* );
void ( *padding2 )( void* );
void ( *padding3 )( void* );
void ( *padding4 )( void* );
};
class DBusLibrary {
public:
using BusGet = DBusConnection* ( * )( int, DBusError* );
using BusRegister = int ( * )( DBusConnection*, DBusError* );
using BusGetUniqueName = const char* ( * )( DBusConnection* );
using BusAddMatch = void ( * )( DBusConnection*, const char*, DBusError* );
using ConnectionAddFilter = int ( * )( DBusConnection*, DBusMessageFunction, void*,
void ( * )( void* ) );
using ConnectionOpenPrivate = DBusConnection* ( * )( const char*, DBusError* );
using ConnectionClose = void ( * )( DBusConnection* );
using ConnectionUnref = void ( * )( DBusConnection* );
using ConnectionReadWrite = int ( * )( DBusConnection*, int );
using ConnectionGetDispatchStatus = DBusDispatchStatus ( * )( DBusConnection* );
using ConnectionDispatch = DBusDispatchStatus ( * )( DBusConnection* );
using ConnectionGetUnixFd = int ( * )( DBusConnection*, int* );
using ConnectionGetOutgoingSize = long ( * )( DBusConnection* );
using ConnectionRegisterObjectPath = int ( * )( DBusConnection*, const char*,
const DBusObjectPathVTable*, void* );
using ConnectionRegisterFallback = ConnectionRegisterObjectPath;
using ConnectionSend = int ( * )( DBusConnection*, DBusMessage*, Uint32* );
using ConnectionSendWithReplyAndBlock = DBusMessage* ( * )( DBusConnection*, DBusMessage*, int,
DBusError* );
using MessageNewMethodCall = DBusMessage* ( * )( const char*, const char*, const char*,
const char* );
using MessageGetArgs = int ( * )( DBusMessage*, DBusError*, int, ... );
using MessageGetString = const char* ( * )( DBusMessage* );
using MessageGetType = int ( * )( DBusMessage* );
using MessageNewMethodReturn = DBusMessage* ( * )( DBusMessage* );
using MessageNewError = DBusMessage* ( * )( DBusMessage*, const char*, const char* );
using MessageNewSignal = DBusMessage* ( * )( const char*, const char*, const char* );
using MessageIterInitAppend = void ( * )( DBusMessage*, DBusMessageIter* );
using MessageIterInit = int ( * )( DBusMessage*, DBusMessageIter* );
using MessageIterGetBasic = void ( * )( DBusMessageIter*, void* );
using MessageIterNext = int ( * )( DBusMessageIter* );
using MessageIterGetArgType = int ( * )( DBusMessageIter* );
using MessageIterRecurse = void ( * )( DBusMessageIter*, DBusMessageIter* );
using MessageIterAppendBasic = int ( * )( DBusMessageIter*, int, const void* );
using MessageIterOpenContainer = int ( * )( DBusMessageIter*, int, const char*,
DBusMessageIter* );
using MessageIterCloseContainer = int ( * )( DBusMessageIter*, DBusMessageIter* );
using MessageUnref = void ( * )( DBusMessage* );
~DBusLibrary() {
if ( mHandle )
System::Sys::unloadObject( mHandle );
}
bool load() {
mHandle = System::Sys::loadObject( "libdbus-1.so.3" );
if ( !mHandle )
mHandle = System::Sys::loadObject( "libdbus-1.so" );
return loadSymbol( busGet, "dbus_bus_get" ) &&
loadSymbol( busRegister, "dbus_bus_register" ) &&
loadSymbol( busGetUniqueName, "dbus_bus_get_unique_name" ) &&
loadSymbol( busAddMatch, "dbus_bus_add_match" ) &&
loadSymbol( connectionAddFilter, "dbus_connection_add_filter" ) &&
loadSymbol( connectionOpenPrivate, "dbus_connection_open_private" ) &&
loadSymbol( connectionClose, "dbus_connection_close" ) &&
loadSymbol( connectionUnref, "dbus_connection_unref" ) &&
loadSymbol( connectionReadWrite, "dbus_connection_read_write" ) &&
loadSymbol( connectionGetDispatchStatus, "dbus_connection_get_dispatch_status" ) &&
loadSymbol( connectionDispatch, "dbus_connection_dispatch" ) &&
loadSymbol( connectionGetUnixFd, "dbus_connection_get_unix_fd" ) &&
loadSymbol( connectionGetOutgoingSize, "dbus_connection_get_outgoing_size" ) &&
loadSymbol( connectionRegisterObjectPath, "dbus_connection_register_object_path" ) &&
loadSymbol( connectionRegisterFallback, "dbus_connection_register_fallback" ) &&
loadSymbol( connectionSend, "dbus_connection_send" ) &&
loadSymbol( connectionSendWithReplyAndBlock,
"dbus_connection_send_with_reply_and_block" ) &&
loadSymbol( messageNewMethodCall, "dbus_message_new_method_call" ) &&
loadSymbol( messageNewMethodReturn, "dbus_message_new_method_return" ) &&
loadSymbol( messageNewError, "dbus_message_new_error" ) &&
loadSymbol( messageNewSignal, "dbus_message_new_signal" ) &&
loadSymbol( messageGetPath, "dbus_message_get_path" ) &&
loadSymbol( messageGetInterface, "dbus_message_get_interface" ) &&
loadSymbol( messageGetMember, "dbus_message_get_member" ) &&
loadSymbol( messageGetType, "dbus_message_get_type" ) &&
loadSymbol( messageGetSender, "dbus_message_get_sender" ) &&
loadSymbol( messageGetArgs, "dbus_message_get_args" ) &&
loadSymbol( messageIterInitAppend, "dbus_message_iter_init_append" ) &&
loadSymbol( messageIterInit, "dbus_message_iter_init" ) &&
loadSymbol( messageIterGetBasic, "dbus_message_iter_get_basic" ) &&
loadSymbol( messageIterNext, "dbus_message_iter_next" ) &&
loadSymbol( messageIterGetArgType, "dbus_message_iter_get_arg_type" ) &&
loadSymbol( messageIterRecurse, "dbus_message_iter_recurse" ) &&
loadSymbol( messageIterAppendBasic, "dbus_message_iter_append_basic" ) &&
loadSymbol( messageIterOpenContainer, "dbus_message_iter_open_container" ) &&
loadSymbol( messageIterCloseContainer, "dbus_message_iter_close_container" ) &&
loadSymbol( messageUnref, "dbus_message_unref" );
}
BusGet busGet{};
BusRegister busRegister{};
BusGetUniqueName busGetUniqueName{};
BusAddMatch busAddMatch{};
ConnectionAddFilter connectionAddFilter{};
ConnectionOpenPrivate connectionOpenPrivate{};
ConnectionClose connectionClose{};
ConnectionUnref connectionUnref{};
ConnectionReadWrite connectionReadWrite{};
ConnectionGetDispatchStatus connectionGetDispatchStatus{};
ConnectionDispatch connectionDispatch{};
ConnectionGetUnixFd connectionGetUnixFd{};
ConnectionGetOutgoingSize connectionGetOutgoingSize{};
ConnectionRegisterObjectPath connectionRegisterObjectPath{};
ConnectionRegisterFallback connectionRegisterFallback{};
ConnectionSend connectionSend{};
ConnectionSendWithReplyAndBlock connectionSendWithReplyAndBlock{};
MessageNewMethodCall messageNewMethodCall{};
MessageNewMethodReturn messageNewMethodReturn{};
MessageNewError messageNewError{};
MessageNewSignal messageNewSignal{};
MessageGetString messageGetPath{};
MessageGetString messageGetInterface{};
MessageGetString messageGetMember{};
MessageGetString messageGetSender{};
MessageGetType messageGetType{};
MessageGetArgs messageGetArgs{};
MessageIterInitAppend messageIterInitAppend{};
MessageIterInit messageIterInit{};
MessageIterGetBasic messageIterGetBasic{};
MessageIterNext messageIterNext{};
MessageIterGetArgType messageIterGetArgType{};
MessageIterRecurse messageIterRecurse{};
MessageIterAppendBasic messageIterAppendBasic{};
MessageIterOpenContainer messageIterOpenContainer{};
MessageIterCloseContainer messageIterCloseContainer{};
MessageUnref messageUnref{};
private:
void* mHandle{};
template <typename T> bool loadSymbol( T& symbol, const char* name ) {
if ( !mHandle )
return false;
symbol = reinterpret_cast<T>( System::Sys::loadFunction( mHandle, name ) );
return symbol != nullptr;
}
};
class AtSpiApplication final : public std::enable_shared_from_this<AtSpiApplication> {
public:
explicit AtSpiApplication( AccessibilityManager& manager );
void initialize();
~AtSpiApplication();
Uint64 registerManager( AccessibilityManager& manager );
void unregisterManager( AccessibilityManager& manager );
bool isAvailable() const {
return mInitialized.load( std::memory_order_acquire ) && mConnection != nullptr;
}
bool isInitialized() const { return mInitialized.load( std::memory_order_acquire ); }
bool hasActiveClients() const { return mHasActiveClients.load( std::memory_order_acquire ); }
void update();
void onEvent( AccessibilityManager& manager, const AccessibilityPendingEvent& event );
private:
static constexpr const char* RootPath = "/org/a11y/atspi/accessible/root";
static constexpr const char* CachePath = "/org/a11y/atspi/cache";
static constexpr const char* NodePathPrefix = "/org/eepp/a11y/";
static constexpr AccessibilityNodeRef ApplicationRef{ 0, 1 };
class ScopedManager {
public:
ScopedManager( AtSpiApplication& application, AccessibilityManager* manager ) :
mApplication( application ), mPrevious( application.mManager ) {
application.mManager = manager;
}
~ScopedManager() { mApplication.mManager = mPrevious; }
private:
AtSpiApplication& mApplication;
AccessibilityManager* mPrevious;
};
AccessibilityManager* mManager{};
std::atomic<AccessibilityManager*> mPrimaryManager{};
String mName;
std::vector<std::pair<Uint64, AccessibilityManager*>> mManagers;
std::vector<AccessibilityManager*> mPendingWindowAdds;
Uint64 mNextManagerId{ 1 };
DBusLibrary mDBus;
DBusConnection* mConnection{};
int mConnectionFd{ -1 };
int mWakePipe[2]{ -1, -1 };
std::atomic<bool> mRunning{};
std::atomic<bool> mDispatchRequested{};
std::atomic<bool> mHasActiveClients{};
std::atomic<bool> mInitialized{};
std::thread mIOThread;
std::string mBusName;
std::string mRegistryBusName{ "org.a11y.atspi.Registry" };
std::string mRegistryPath{ RootPath };
Int32 mApplicationId{};
bool mOutgoingPending{};
struct TextSnapshot {
String value;
AccessibilityTextInfo text;
};
UnorderedMap<std::string, TextSnapshot> mTextSnapshots;
UnorderedSet<std::string> mClients;
struct PendingText {
AccessibilityManager* manager;
AccessibilityNodeRef ref;
bool valueChanged;
};
std::vector<PendingText> mPendingTexts;
// Connection, dispatch and client tracking: accessibilitybackendatspi.cpp
void initializationFinished() { mInitialized.store( true, std::memory_order_release ); }
void waitForMessages();
static DBusHandlerResult handleMessage( DBusConnection*, DBusMessage* message, void* userData );
void activate( DBusMessage* message );
static DBusHandlerResult clientDisconnected( DBusConnection*, DBusMessage* message,
void* userData );
void send( DBusMessage* reply );
void sendBasic( DBusMessage* request, int type, const void* value );
void sendError( DBusMessage* request, const char* name, const char* message ) {
send( mDBus.messageNewError( request, name, message ) );
}
void registerApplication();
// Manager events and text change notifications: accessibilitybackendatspievents.cpp
/** Records the client's view of a text. A snapshot is replaced only when the client has just
* read the current contents and no unsent change would otherwise be diffed away. */
void rememberText( AccessibilityNodeRef ref, const AccessibilityNodeInfo& info,
bool replace = false );
bool hasPendingText( AccessibilityNodeRef ref ) const;
void queueText( AccessibilityManager& manager, AccessibilityNodeRef ref, bool valueChanged );
void flushPendingTexts();
void sendObjectEvent( AccessibilityNodeRef ref, const char* signal, const char* detail,
Int32 offset, Int32 length = 0, const String& text = {} );
void sendTextSelection( AccessibilityNodeRef ref, const AccessibilityTextInfo& text );
void sendTextChanged( AccessibilityNodeRef ref );
void sendWindowChanged( AccessibilityManager& manager, bool added, Int32 index );
// Object paths, tree navigation, roles, states and marshalling:
// accessibilitybackendatspitree.cpp
AccessibilityNodeRef refFromPath( const char* path );
Uint64 currentManagerId() const;
bool isSceneRoot( AccessibilityNodeRef ref ) const {
return mManager && ref == mManager->getRoot();
}
bool isValid( AccessibilityNodeRef ref ) const {
return ref == ApplicationRef || ( mManager && mManager->isValid( ref ) );
}
AccessibilityNodeInfo getNodeInfo( AccessibilityNodeRef ref, bool includeValue = true ) const;
AccessibilityNodeRef getParent( AccessibilityNodeRef ref );
size_t getChildCount( AccessibilityNodeRef ref );
AccessibilityNodeRef getChild( AccessibilityNodeRef ref, size_t index );
std::string pathFromRef( AccessibilityNodeRef ref );
Uint32 role( AccessibilityRole role ) const;
const char* roleName( AccessibilityRole role ) const;
AccessibilityAction actionAt( AccessibilityActions actions, Int32 index ) const;
AccessibilityActions nativeActions( AccessibilityActions actions ) const;
const char* actionName( AccessibilityAction action ) const;
void appendBasic( DBusMessageIter& iter, int type, const void* value ) {
mDBus.messageIterAppendBasic( &iter, type, value );
}
bool readPropertySet( DBusMessage* request, const char*& interface, const char*& property,
DBusMessageIter& variant );
void appendRef( DBusMessageIter& iter, AccessibilityNodeRef ref );
void appendApplicationRef( DBusMessageIter& iter ) { appendRef( iter, ApplicationRef ); }
void appendEventProperties( DBusMessageIter& iter );
void appendDesktopRef( DBusMessageIter& iter );
void appendInterfaces( DBusMessageIter& iter, AccessibilityNodeRef ref,
const AccessibilityNodeInfo& info );
void appendStates( DBusMessageIter& iter, const AccessibilityNodeInfo& info );
Int32 indexInParent( AccessibilityNodeRef ref );
Math::Rectf boundsForCoordinateType( AccessibilityNodeRef ref,
const AccessibilityNodeInfo& info, Uint32 coordinateType );
// Incoming method calls, one handler per AT-SPI interface: accessibilitybackendatspimethods.cpp
DBusHandlerResult handleMessage( DBusMessage* request );
/** Serves the AT-SPI Cache object. eepp exposes no cached items; clients query lazily. */
DBusHandlerResult handleCache( DBusMessage* request, const char* interface,
const char* member );
DBusHandlerResult handleAccessible( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handleApplication( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handleComponent( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handleAction( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handleText( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handleEditableText( DBusMessage* request, AccessibilityNodeRef ref,
const char* member );
DBusHandlerResult handlePropertiesGet( DBusMessage* request, AccessibilityNodeRef ref );
DBusHandlerResult handlePropertiesSet( DBusMessage* request, AccessibilityNodeRef ref );
};
}}} // namespace EE::UI::AtSpi
#endif
#endif
@@ -0,0 +1,289 @@
#include "accessibilitybackendatspi.hpp"
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD
#include <algorithm>
#include <eepp/window/input.hpp>
namespace EE { namespace UI { namespace AtSpi {
void AtSpiApplication::onEvent( AccessibilityManager& manager,
const AccessibilityPendingEvent& event ) {
if ( !isAvailable() )
return;
ScopedManager scopedManager( *this, &manager );
if ( event.type == AccessibilityEvent::Destroyed && !event.related.isValid() ) {
if ( !mTextSnapshots.empty() )
mTextSnapshots.erase( pathFromRef( event.ref ) );
mPendingTexts.erase( std::remove_if( mPendingTexts.begin(), mPendingTexts.end(),
[&]( const PendingText& text ) {
return text.manager == &manager &&
text.ref == event.ref;
} ),
mPendingTexts.end() );
return;
}
if ( !hasActiveClients() )
return;
AccessibilityNodeInfo info;
if ( event.type == AccessibilityEvent::FocusChanged ||
event.type == AccessibilityEvent::SelectionChanged ||
event.type == AccessibilityEvent::StateChanged ||
event.type == AccessibilityEvent::EnabledChanged ||
event.type == AccessibilityEvent::VisibilityChanged ||
event.type == AccessibilityEvent::ValueChanged )
info = manager.getNodeInfo( event.ref, false, false );
if ( info.text.valid ) {
// Diff text once per frame: a multi-cursor edit or replace-all emits one change per
// edited range, and each diff copies and scans the whole document.
if ( event.type == AccessibilityEvent::ValueChanged ||
event.type == AccessibilityEvent::SelectionChanged ) {
queueText( manager, event.ref, event.type == AccessibilityEvent::ValueChanged );
return;
}
if ( event.type == AccessibilityEvent::FocusChanged &&
hasState( info.states, AccessibilityState::Focused ) )
rememberText( event.ref, manager.getNodeInfo( event.ref ) );
}
if ( event.type == AccessibilityEvent::EnabledChanged ) {
sendObjectEvent( event.ref, "StateChanged", "sensitive",
hasState( info.states, AccessibilityState::Enabled ) );
} else if ( event.type == AccessibilityEvent::VisibilityChanged ) {
sendObjectEvent( event.ref, "StateChanged", "showing",
hasState( info.states, AccessibilityState::Showing ) );
}
const char* signal = "PropertyChange";
const char* detail = "accessible-state";
if ( event.type == AccessibilityEvent::FocusChanged ) {
signal = "StateChanged";
detail = "focused";
} else if ( event.type == AccessibilityEvent::SelectionChanged ) {
signal = "StateChanged";
detail = "selected";
} else if ( event.type == AccessibilityEvent::NameChanged ) {
detail = "accessible-name";
} else if ( event.type == AccessibilityEvent::DescriptionChanged ) {
detail = "accessible-description";
} else if ( event.type == AccessibilityEvent::ValueChanged ) {
detail = "accessible-value";
} else if ( event.type == AccessibilityEvent::StateChanged ) {
signal = "StateChanged";
if ( info.role == AccessibilityRole::CheckBox ||
info.role == AccessibilityRole::RadioButton ||
info.role == AccessibilityRole::CheckMenuItem ||
info.role == AccessibilityRole::RadioMenuItem )
detail = "checked";
else if ( info.role == AccessibilityRole::ComboBox )
detail = "expanded";
else
return;
} else if ( event.type == AccessibilityEvent::EnabledChanged ) {
signal = "StateChanged";
detail = "enabled";
} else if ( event.type == AccessibilityEvent::VisibilityChanged ) {
signal = "StateChanged";
detail = "visible";
} else if ( event.type == AccessibilityEvent::BoundsChanged ) {
signal = "BoundsChanged";
detail = "";
} else if ( event.type == AccessibilityEvent::ChildrenChanged ||
event.type == AccessibilityEvent::ModelChanged ) {
signal = "ModelChanged";
detail = "";
} else if ( event.type == AccessibilityEvent::Created ||
event.type == AccessibilityEvent::Destroyed ) {
signal = "ChildrenChanged";
detail = event.type == AccessibilityEvent::Destroyed ? "remove" : "add";
}
std::string pathStorage = pathFromRef( event.ref );
DBusMessage* message =
mDBus.messageNewSignal( pathStorage.c_str(), "org.a11y.atspi.Event.Object", signal );
if ( !message )
return;
DBusMessageIter iter;
DBusMessageIter variant;
mDBus.messageIterInitAppend( message, &iter );
appendBasic( iter, 's', &detail );
Int32 detail1 = 0;
if ( event.type == AccessibilityEvent::FocusChanged )
detail1 = hasState( info.states, AccessibilityState::Focused );
else if ( event.type == AccessibilityEvent::SelectionChanged )
detail1 = hasState( info.states, AccessibilityState::Selected );
else if ( event.type == AccessibilityEvent::StateChanged &&
std::strcmp( detail, "checked" ) == 0 )
detail1 = hasState( info.states, AccessibilityState::Checked );
else if ( event.type == AccessibilityEvent::StateChanged &&
std::strcmp( detail, "expanded" ) == 0 )
detail1 = hasState( info.states, AccessibilityState::Expanded );
else if ( event.type == AccessibilityEvent::EnabledChanged )
detail1 = hasState( info.states, AccessibilityState::Enabled );
else if ( event.type == AccessibilityEvent::VisibilityChanged )
detail1 = hasState( info.states, AccessibilityState::Visible );
else if ( event.type == AccessibilityEvent::Created ||
event.type == AccessibilityEvent::Destroyed )
detail1 = event.index;
Int32 detail2 = 0;
appendBasic( iter, 'i', &detail1 );
appendBasic( iter, 'i', &detail2 );
bool childrenChanged =
event.type == AccessibilityEvent::Created || event.type == AccessibilityEvent::Destroyed;
if ( childrenChanged ) {
mDBus.messageIterOpenContainer( &iter, 'v', "(so)", &variant );
appendRef( variant, event.related );
} else {
const char* empty = "";
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &empty );
}
mDBus.messageIterCloseContainer( &iter, &variant );
appendEventProperties( iter );
send( message );
}
void AtSpiApplication::rememberText( AccessibilityNodeRef ref, const AccessibilityNodeInfo& info,
bool replace ) {
if ( !info.text.valid )
return;
auto path = pathFromRef( ref );
auto found = mTextSnapshots.find( path );
if ( found == mTextSnapshots.end() )
mTextSnapshots.emplace( std::move( path ), TextSnapshot{ info.value, info.text } );
else if ( replace && !hasPendingText( ref ) )
found->second = TextSnapshot{ info.value, info.text };
}
bool AtSpiApplication::hasPendingText( AccessibilityNodeRef ref ) const {
for ( const auto& text : mPendingTexts ) {
if ( text.manager == mManager && text.ref == ref )
return true;
}
return false;
}
void AtSpiApplication::queueText( AccessibilityManager& manager, AccessibilityNodeRef ref,
bool valueChanged ) {
for ( auto& text : mPendingTexts ) {
if ( text.manager == &manager && text.ref == ref ) {
text.valueChanged |= valueChanged;
return;
}
}
mPendingTexts.push_back( { &manager, ref, valueChanged } );
Window::Input::wakeUpEventLoop();
}
void AtSpiApplication::flushPendingTexts() {
if ( mPendingTexts.empty() )
return;
auto pending = std::move( mPendingTexts );
mPendingTexts.clear();
if ( !hasActiveClients() )
return;
for ( const auto& text : pending ) {
if ( !text.manager->isValid( text.ref ) )
continue;
ScopedManager scopedManager( *this, text.manager );
if ( text.valueChanged )
sendTextChanged( text.ref );
else
sendTextSelection( text.ref, text.manager->getTextInfo( text.ref ) );
}
}
void AtSpiApplication::sendObjectEvent( AccessibilityNodeRef ref, const char* signal,
const char* detail, Int32 offset, Int32 length,
const String& text ) {
auto path = pathFromRef( ref );
auto* message = mDBus.messageNewSignal( path.c_str(), "org.a11y.atspi.Event.Object", signal );
if ( !message )
return;
DBusMessageIter iter;
DBusMessageIter variant;
mDBus.messageIterInitAppend( message, &iter );
appendBasic( iter, 's', &detail );
appendBasic( iter, 'i', &offset );
appendBasic( iter, 'i', &length );
auto utf8 = text.toUtf8();
const char* value = utf8.c_str();
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
mDBus.messageIterCloseContainer( &iter, &variant );
appendEventProperties( iter );
send( message );
}
void AtSpiApplication::sendTextSelection( AccessibilityNodeRef ref,
const AccessibilityTextInfo& text ) {
auto found = mTextSnapshots.find( pathFromRef( ref ) );
if ( found == mTextSnapshots.end() ) {
rememberText( ref, getNodeInfo( ref ) );
found = mTextSnapshots.find( pathFromRef( ref ) );
}
if ( found == mTextSnapshots.end() )
return;
auto& previous = found->second.text;
if ( previous.caretOffset != text.caretOffset )
sendObjectEvent( ref, "TextCaretMoved", "", text.caretOffset );
if ( previous.selectionStart != text.selectionStart ||
previous.selectionEnd != text.selectionEnd )
sendObjectEvent( ref, "TextSelectionChanged", "", 0 );
previous = text;
}
void AtSpiApplication::sendTextChanged( AccessibilityNodeRef ref ) {
auto info = getNodeInfo( ref );
auto path = pathFromRef( ref );
auto found = mTextSnapshots.find( path );
if ( found == mTextSnapshots.end() ) {
// A client which has not queried or focused this text has no old contents to diff.
rememberText( ref, info );
return;
}
auto& previous = found->second.value;
size_t prefix = 0;
while ( prefix < previous.size() && prefix < info.value.size() &&
previous[prefix] == info.value[prefix] )
++prefix;
size_t suffix = 0;
while ( suffix < previous.size() - prefix && suffix < info.value.size() - prefix &&
previous[previous.size() - suffix - 1] == info.value[info.value.size() - suffix - 1] )
++suffix;
const size_t removed = previous.size() - prefix - suffix;
const size_t inserted = info.value.size() - prefix - suffix;
if ( removed ) {
sendObjectEvent( ref, "TextChanged", "delete", prefix, removed,
previous.substr( prefix, removed ) );
}
if ( inserted ) {
sendObjectEvent( ref, "TextChanged", "insert", prefix, inserted,
info.value.substr( prefix, inserted ) );
}
previous = std::move( info.value );
sendTextSelection( ref, info.text );
}
void AtSpiApplication::sendWindowChanged( AccessibilityManager& manager, bool added, Int32 index ) {
ScopedManager scopedManager( *this, &manager );
auto root = manager.getRoot();
DBusMessage* message =
mDBus.messageNewSignal( RootPath, "org.a11y.atspi.Event.Object", "ChildrenChanged" );
if ( !message )
return;
DBusMessageIter iter;
DBusMessageIter variant;
mDBus.messageIterInitAppend( message, &iter );
const char* detail = added ? "add" : "remove";
appendBasic( iter, 's', &detail );
Int32 detail2 = 0;
appendBasic( iter, 'i', &index );
appendBasic( iter, 'i', &detail2 );
mDBus.messageIterOpenContainer( &iter, 'v', "(so)", &variant );
appendRef( variant, root );
mDBus.messageIterCloseContainer( &iter, &variant );
appendEventProperties( iter );
send( message );
}
}}} // namespace EE::UI::AtSpi
#endif
@@ -0,0 +1,632 @@
#include "accessibilitybackendatspi.hpp"
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD
#include <algorithm>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/window.hpp>
#include <unistd.h>
namespace EE { namespace UI { namespace AtSpi {
DBusHandlerResult AtSpiApplication::handleMessage( DBusMessage* request ) {
const char* path = mDBus.messageGetPath( request );
const char* interface = mDBus.messageGetInterface( request );
const char* member = mDBus.messageGetMember( request );
if ( !interface || !member )
return 1;
if ( path && std::strcmp( path, CachePath ) == 0 )
return handleCache( request, interface, member );
auto ref = refFromPath( path );
if ( !isValid( ref ) )
return 1;
if ( std::strcmp( interface, "org.a11y.atspi.Accessible" ) == 0 )
return handleAccessible( request, ref, member );
if ( std::strcmp( interface, "org.a11y.atspi.Application" ) == 0 )
return handleApplication( request, ref, member );
if ( std::strcmp( interface, "org.a11y.atspi.Component" ) == 0 )
return handleComponent( request, ref, member );
if ( std::strcmp( interface, "org.a11y.atspi.Action" ) == 0 )
return handleAction( request, ref, member );
if ( std::strcmp( interface, "org.a11y.atspi.Text" ) == 0 )
return handleText( request, ref, member );
if ( std::strcmp( interface, "org.a11y.atspi.EditableText" ) == 0 )
return handleEditableText( request, ref, member );
if ( std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 ) {
if ( std::strcmp( member, "Get" ) == 0 )
return handlePropertiesGet( request, ref );
if ( std::strcmp( member, "Set" ) == 0 )
return handlePropertiesSet( request, ref );
}
return 1;
}
DBusHandlerResult AtSpiApplication::handleCache( DBusMessage* request, const char* interface,
const char* member ) {
if ( std::strcmp( interface, "org.a11y.atspi.Cache" ) == 0 &&
std::strcmp( member, "GetItems" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter array;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "((so)(so)a(so)assusau)", &array );
mDBus.messageIterCloseContainer( &iter, &array );
send( reply );
return 0;
}
if ( std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 &&
std::strcmp( member, "Get" ) == 0 ) {
const char* requestedInterface = nullptr;
const char* property = nullptr;
if ( !mDBus.messageGetArgs( request, nullptr, 's', &requestedInterface, 's', &property,
0 ) ||
!requestedInterface || !property ||
std::strcmp( requestedInterface, "org.a11y.atspi.Cache" ) != 0 ||
std::strcmp( property, "version" ) != 0 )
return 1;
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter variant;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'v', "u", &variant );
Uint32 version = 1;
appendBasic( variant, 'u', &version );
mDBus.messageIterCloseContainer( &iter, &variant );
send( reply );
return 0;
}
return 1;
}
DBusHandlerResult AtSpiApplication::handleAccessible( DBusMessage* request,
AccessibilityNodeRef ref,
const char* member ) {
if ( std::strcmp( member, "GetRole" ) == 0 ) {
auto info = getNodeInfo( ref, false );
Uint32 value =
hasState( info.states, AccessibilityState::Protected ) ? 40 : role( info.role );
sendBasic( request, 'u', &value );
} else if ( std::strcmp( member, "GetRoleName" ) == 0 ||
std::strcmp( member, "GetLocalizedRoleName" ) == 0 ) {
auto info = getNodeInfo( ref, false );
const char* value = hasState( info.states, AccessibilityState::Protected )
? "password text"
: roleName( info.role );
sendBasic( request, 's', &value );
} else if ( std::strcmp( member, "GetChildAtIndex" ) == 0 ) {
Int32 index = -1;
mDBus.messageGetArgs( request, nullptr, 'i', &index, 0 );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
if ( ref == ApplicationRef && index >= 0 &&
static_cast<size_t>( index ) < mManagers.size() ) {
auto manager = std::next( mManagers.begin(), index )->second;
ScopedManager scopedManager( *this, manager );
appendRef( iter, manager->getRoot() );
} else {
appendRef( iter, index >= 0 ? getChild( ref, index ) : AccessibilityNodeRef{} );
}
send( reply );
} else if ( std::strcmp( member, "GetChildren" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter array;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "(so)", &array );
if ( ref == ApplicationRef ) {
for ( const auto& entry : mManagers ) {
ScopedManager scopedManager( *this, entry.second );
appendRef( array, entry.second->getRoot() );
}
} else {
for ( const auto& child : mManager->getChildren( ref ) )
appendRef( array, child );
}
mDBus.messageIterCloseContainer( &iter, &array );
send( reply );
} else if ( std::strcmp( member, "GetIndexInParent" ) == 0 ) {
Int32 index = indexInParent( ref );
sendBasic( request, 'i', &index );
} else if ( std::strcmp( member, "GetApplication" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendApplicationRef( iter );
send( reply );
} else if ( std::strcmp( member, "GetInterfaces" ) == 0 ) {
auto info = getNodeInfo( ref, false );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendInterfaces( iter, ref, info );
send( reply );
} else if ( std::strcmp( member, "GetState" ) == 0 ) {
auto info = getNodeInfo( ref, false );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendStates( iter, info );
send( reply );
} else if ( std::strcmp( member, "GetAttributes" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter attributes;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "{ss}", &attributes );
mDBus.messageIterCloseContainer( &iter, &attributes );
send( reply );
} else if ( std::strcmp( member, "GetRelationSet" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter relations;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "(ua(so))", &relations );
mDBus.messageIterCloseContainer( &iter, &relations );
send( reply );
} else if ( std::strcmp( member, "GetAccessibleId" ) == 0 ) {
std::string valueStorage = pathFromRef( ref );
const char* value = valueStorage.c_str();
sendBasic( request, 's', &value );
} else if ( std::strcmp( member, "GetLocale" ) == 0 ) {
const char* locale = "C";
sendBasic( request, 's', &locale );
} else {
return 1;
}
return 0;
}
DBusHandlerResult AtSpiApplication::handleApplication( DBusMessage* request,
AccessibilityNodeRef ref,
const char* member ) {
if ( std::strcmp( member, "GetLocale" ) == 0 ) {
const char* locale = "C";
sendBasic( request, 's', &locale );
} else if ( std::strcmp( member, "GetApplicationBusAddress" ) == 0 ) {
const char* address = "";
sendBasic( request, 's', &address );
} else {
return 1;
}
return 0;
}
DBusHandlerResult AtSpiApplication::handleComponent( DBusMessage* request, AccessibilityNodeRef ref,
const char* member ) {
if ( std::strcmp( member, "GetExtents" ) == 0 ) {
auto info = getNodeInfo( ref, false );
Uint32 coordinateType = 0;
mDBus.messageGetArgs( request, nullptr, 'u', &coordinateType, 0 );
auto bounds = boundsForCoordinateType( ref, info, coordinateType );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter structure;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'r', nullptr, &structure );
Int32 values[] = { static_cast<Int32>( bounds.Left ), static_cast<Int32>( bounds.Top ),
static_cast<Int32>( bounds.getWidth() ),
static_cast<Int32>( bounds.getHeight() ) };
for ( auto value : values )
appendBasic( structure, 'i', &value );
mDBus.messageIterCloseContainer( &iter, &structure );
send( reply );
} else if ( std::strcmp( member, "Contains" ) == 0 ) {
auto info = getNodeInfo( ref, false );
Int32 x = 0;
Int32 y = 0;
Uint32 coordinateType = 0;
mDBus.messageGetArgs( request, nullptr, 'i', &x, 'i', &y, 'u', &coordinateType, 0 );
int contains =
boundsForCoordinateType( ref, info, coordinateType ).contains( Math::Vector2f( x, y ) );
sendBasic( request, 'b', &contains );
} else if ( std::strcmp( member, "GetAccessibleAtPoint" ) == 0 ) {
Int32 x = 0;
Int32 y = 0;
Uint32 coordinateType = 0;
mDBus.messageGetArgs( request, nullptr, 'i', &x, 'i', &y, 'u', &coordinateType, 0 );
if ( coordinateType == 0 ) {
auto scene = mManager ? mManager->getSceneNode() : nullptr;
if ( scene && scene->getWindow() ) {
auto position = scene->getWindow()->getPosition();
x -= position.x;
y -= position.y;
}
} else if ( coordinateType == 2 ) {
auto parent = getNodeInfo( getParent( ref ), false );
x += static_cast<Int32>( parent.bounds.Left );
y += static_cast<Int32>( parent.bounds.Top );
}
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendRef( iter, mManager ? mManager->hitTest( Math::Vector2f( x, y ) )
: AccessibilityNodeRef{} );
send( reply );
} else if ( std::strcmp( member, "GrabFocus" ) == 0 ) {
int success =
mManager && mManager->performAction( ref, { AccessibilityAction::Focus, {} } );
sendBasic( request, 'b', &success );
} else if ( std::strcmp( member, "GetPosition" ) == 0 ) {
auto info = getNodeInfo( ref, false );
Uint32 coordinateType = 0;
mDBus.messageGetArgs( request, nullptr, 'u', &coordinateType, 0 );
auto bounds = boundsForCoordinateType( ref, info, coordinateType );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
Int32 x = static_cast<Int32>( bounds.Left );
Int32 y = static_cast<Int32>( bounds.Top );
appendBasic( iter, 'i', &x );
appendBasic( iter, 'i', &y );
send( reply );
} else if ( std::strcmp( member, "GetSize" ) == 0 ) {
auto info = getNodeInfo( ref, false );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
Int32 width = static_cast<Int32>( info.bounds.getWidth() );
Int32 height = static_cast<Int32>( info.bounds.getHeight() );
appendBasic( iter, 'i', &width );
appendBasic( iter, 'i', &height );
send( reply );
} else if ( std::strcmp( member, "GetLayer" ) == 0 ) {
Uint32 layer = 1;
sendBasic( request, 'u', &layer );
} else if ( std::strcmp( member, "GetMDIZOrder" ) == 0 ) {
Int16 order = 0;
sendBasic( request, 'n', &order );
} else if ( std::strcmp( member, "GetAlpha" ) == 0 ) {
double alpha = 1;
sendBasic( request, 'd', &alpha );
} else {
return 1;
}
return 0;
}
DBusHandlerResult AtSpiApplication::handleAction( DBusMessage* request, AccessibilityNodeRef ref,
const char* member ) {
auto info = getNodeInfo( ref, false );
const auto actions = nativeActions( info.actions );
Int32 count = static_cast<Int32>( __builtin_popcount( actions ) );
if ( std::strcmp( member, "GetNActions" ) == 0 ) {
sendBasic( request, 'i', &count );
} else if ( std::strcmp( member, "GetActions" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter array;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "(sss)", &array );
for ( Int32 index = 0; index < count; ++index ) {
DBusMessageIter structure;
auto action = actionAt( actions, index );
const char* name = actionName( action );
const char* empty = "";
mDBus.messageIterOpenContainer( &array, 'r', nullptr, &structure );
appendBasic( structure, 's', &name );
appendBasic( structure, 's', &empty );
appendBasic( structure, 's', &empty );
mDBus.messageIterCloseContainer( &array, &structure );
}
mDBus.messageIterCloseContainer( &iter, &array );
send( reply );
} else {
Int32 index = -1;
mDBus.messageGetArgs( request, nullptr, 'i', &index, 0 );
if ( index < 0 || index >= count )
return 1;
auto action = actionAt( actions, index );
if ( std::strcmp( member, "DoAction" ) == 0 ) {
int success = mManager && mManager->performAction( ref, { action, {} } );
sendBasic( request, 'b', &success );
} else if ( std::strcmp( member, "GetName" ) == 0 ||
std::strcmp( member, "GetLocalizedName" ) == 0 ) {
const char* name = actionName( action );
sendBasic( request, 's', &name );
} else if ( std::strcmp( member, "GetDescription" ) == 0 ||
std::strcmp( member, "GetKeyBinding" ) == 0 ) {
const char* empty = "";
sendBasic( request, 's', &empty );
} else {
return 1;
}
}
return 0;
}
DBusHandlerResult AtSpiApplication::handleText( DBusMessage* request, AccessibilityNodeRef ref,
const char* member ) {
auto info = getNodeInfo( ref, true );
if ( !info.text.valid )
return 1;
rememberText( ref, info, std::strcmp( member, "GetText" ) == 0 );
const Int32 characterCount = static_cast<Int32>( info.value.size() );
if ( std::strcmp( member, "GetText" ) == 0 ) {
Int32 start = 0;
Int32 end = -1;
mDBus.messageGetArgs( request, nullptr, 'i', &start, 'i', &end, 0 );
start = std::max( 0, std::min( start, characterCount ) );
end = end < 0 ? characterCount : std::max( start, std::min( end, characterCount ) );
std::string textStorage = info.value.substr( start, end - start ).toUtf8();
const char* text = textStorage.c_str();
sendBasic( request, 's', &text );
} else if ( std::strcmp( member, "GetStringAtOffset" ) == 0 ) {
Int32 offset = 0;
Uint32 granularity = 0;
mDBus.messageGetArgs( request, nullptr, 'i', &offset, 'u', &granularity, 0 );
Int32 start = std::max( 0, std::min( offset, characterCount ) );
Int32 end = start;
if ( granularity == 0 && start < characterCount ) {
end = start + 1;
} else if ( granularity == 1 ) {
auto isSpace = [&info]( Int32 index ) {
auto character = info.value[index];
return character == ' ' || character == '\t' || character == '\n' ||
character == '\r';
};
while ( start > 0 && !isSpace( start - 1 ) )
--start;
end = std::max( 0, std::min( offset, characterCount ) );
while ( end < characterCount && !isSpace( end ) )
++end;
} else if ( granularity == 3 || granularity == 4 ) {
while ( start > 0 && info.value[start - 1] != '\n' )
--start;
while ( end < characterCount && info.value[end] != '\n' )
++end;
if ( end < characterCount )
++end;
} else {
start = 0;
end = characterCount;
}
std::string textStorage = info.value.substr( start, end - start ).toUtf8();
const char* text = textStorage.c_str();
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendBasic( iter, 's', &text );
appendBasic( iter, 'i', &start );
appendBasic( iter, 'i', &end );
send( reply );
} else if ( std::strcmp( member, "GetCharacterAtOffset" ) == 0 ) {
Int32 offset = 0;
mDBus.messageGetArgs( request, nullptr, 'i', &offset, 0 );
Int32 character = offset >= 0 && offset < characterCount ? info.value[offset] : 0;
sendBasic( request, 'i', &character );
} else if ( std::strcmp( member, "GetAttributes" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter attributes;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "{ss}", &attributes );
mDBus.messageIterCloseContainer( &iter, &attributes );
Int32 start = 0;
appendBasic( iter, 'i', &start );
appendBasic( iter, 'i', &characterCount );
send( reply );
} else if ( std::strcmp( member, "GetDefaultAttributes" ) == 0 ||
std::strcmp( member, "GetDefaultAttributeSet" ) == 0 ) {
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter attributes;
mDBus.messageIterInitAppend( reply, &iter );
mDBus.messageIterOpenContainer( &iter, 'a', "{ss}", &attributes );
mDBus.messageIterCloseContainer( &iter, &attributes );
send( reply );
} else if ( std::strcmp( member, "GetNSelections" ) == 0 ) {
Int32 count = info.text.selectionStart != info.text.selectionEnd ? 1 : 0;
sendBasic( request, 'i', &count );
} else if ( std::strcmp( member, "GetSelection" ) == 0 ) {
Int32 selection = -1;
mDBus.messageGetArgs( request, nullptr, 'i', &selection, 0 );
if ( selection != 0 || info.text.selectionStart == info.text.selectionEnd )
return 1;
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
mDBus.messageIterInitAppend( reply, &iter );
appendBasic( iter, 'i', &info.text.selectionStart );
appendBasic( iter, 'i', &info.text.selectionEnd );
send( reply );
} else if ( std::strcmp( member, "SetCaretOffset" ) == 0 ) {
Int32 offset = -1;
mDBus.messageGetArgs( request, nullptr, 'i', &offset, 0 );
int success = offset >= 0 && offset <= characterCount && mManager &&
mManager->performAction( ref, { AccessibilityAction::SetTextSelection,
String( String::toString( offset ) + ":" +
String::toString( offset ) ) } );
sendBasic( request, 'b', &success );
} else if ( std::strcmp( member, "SetSelection" ) == 0 ||
std::strcmp( member, "AddSelection" ) == 0 ||
std::strcmp( member, "RemoveSelection" ) == 0 ) {
Int32 selection = 0;
Int32 start = info.text.caretOffset;
Int32 end = start;
bool valid = std::strcmp( member, "AddSelection" ) == 0
? mDBus.messageGetArgs( request, nullptr, 'i', &start, 'i', &end, 0 )
: std::strcmp( member, "SetSelection" ) == 0
? mDBus.messageGetArgs( request, nullptr, 'i', &selection, 'i', &start,
'i', &end, 0 )
: mDBus.messageGetArgs( request, nullptr, 'i', &selection, 0 );
int success = valid && selection == 0 && start >= 0 && end >= start &&
end <= characterCount && mManager &&
mManager->performAction( ref, { AccessibilityAction::SetTextSelection,
String( String::toString( start ) + ":" +
String::toString( end ) ) } );
sendBasic( request, 'b', &success );
} else {
return 1;
}
return 0;
}
DBusHandlerResult AtSpiApplication::handleEditableText( DBusMessage* request,
AccessibilityNodeRef ref,
const char* member ) {
auto info = getNodeInfo( ref, true );
if ( !info.text.valid || !hasState( info.states, AccessibilityState::Editable ) ||
std::strcmp( member, "SetTextContents" ) != 0 )
return 1;
const char* contents = nullptr;
if ( !mDBus.messageGetArgs( request, nullptr, 's', &contents, 0 ) )
return 1;
rememberText( ref, info, true );
int success =
mManager && mManager->performAction(
ref, { AccessibilityAction::SetText,
String::fromUtf8( std::string_view( contents ? contents : "" ) ) } );
sendBasic( request, 'b', &success );
return 0;
}
DBusHandlerResult AtSpiApplication::handlePropertiesGet( DBusMessage* request,
AccessibilityNodeRef ref ) {
const char* requestedInterface = nullptr;
const char* property = nullptr;
if ( !mDBus.messageGetArgs( request, nullptr, 's', &requestedInterface, 's', &property, 0 ) ||
!requestedInterface || !property )
return 1;
const bool includeValue = ( std::strcmp( requestedInterface, "org.a11y.atspi.Text" ) == 0 &&
std::strcmp( property, "CharacterCount" ) == 0 ) ||
( std::strcmp( requestedInterface, "org.a11y.atspi.Value" ) == 0 &&
std::strcmp( property, "CurrentValue" ) == 0 );
const bool needsInfo = std::strcmp( requestedInterface, "org.a11y.atspi.Action" ) == 0 ||
std::strcmp( requestedInterface, "org.a11y.atspi.Text" ) == 0 ||
std::strcmp( requestedInterface, "org.a11y.atspi.Value" ) == 0 ||
std::strcmp( property, "Name" ) == 0 ||
std::strcmp( property, "Description" ) == 0;
AccessibilityNodeInfo info;
if ( needsInfo )
info = getNodeInfo( ref, includeValue );
if ( mManager && std::strcmp( requestedInterface, "org.a11y.atspi.Text" ) == 0 &&
std::strcmp( property, "CaretOffset" ) == 0 )
info.text = mManager->getTextInfo( ref );
DBusMessage* reply = mDBus.messageNewMethodReturn( request );
DBusMessageIter iter;
DBusMessageIter variant;
mDBus.messageIterInitAppend( reply, &iter );
if ( std::strcmp( requestedInterface, "org.a11y.atspi.Action" ) == 0 &&
std::strcmp( property, "NActions" ) == 0 ) {
Int32 value = static_cast<Int32>( __builtin_popcount( nativeActions( info.actions ) ) );
mDBus.messageIterOpenContainer( &iter, 'v', "i", &variant );
appendBasic( variant, 'i', &value );
} else if ( std::strcmp( requestedInterface, "org.a11y.atspi.Text" ) == 0 && info.text.valid &&
( std::strcmp( property, "CharacterCount" ) == 0 ||
std::strcmp( property, "CaretOffset" ) == 0 ) ) {
Int32 value = std::strcmp( property, "CharacterCount" ) == 0
? static_cast<Int32>( info.value.size() )
: info.text.caretOffset;
mDBus.messageIterOpenContainer( &iter, 'v', "i", &variant );
appendBasic( variant, 'i', &value );
} else if ( std::strcmp( requestedInterface, "org.a11y.atspi.Value" ) == 0 &&
info.range.valid ) {
double value = 0;
if ( std::strcmp( property, "CurrentValue" ) == 0 )
String::fromString( value, info.value.toUtf8() );
else if ( std::strcmp( property, "MaximumValue" ) == 0 )
value = info.range.maximum;
else if ( std::strcmp( property, "MinimumValue" ) == 0 )
value = info.range.minimum;
else if ( std::strcmp( property, "MinimumIncrement" ) == 0 )
value = info.range.smallChange;
else {
mDBus.messageUnref( reply );
return 1;
}
mDBus.messageIterOpenContainer( &iter, 'v', "d", &variant );
appendBasic( variant, 'd', &value );
} else if ( std::strcmp( property, "Name" ) == 0 ||
std::strcmp( property, "Description" ) == 0 ) {
std::string text =
( std::strcmp( property, "Name" ) == 0 ? info.name : info.description ).toUtf8();
const char* value = text.c_str();
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
} else if ( std::strcmp( property, "HelpText" ) == 0 ) {
const char* value = "";
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
} else if ( std::strcmp( property, "Locale" ) == 0 ) {
const char* value = "C";
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
} else if ( std::strcmp( property, "AccessibleId" ) == 0 ) {
std::string valueStorage = pathFromRef( ref );
const char* value = valueStorage.c_str();
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
} else if ( std::strcmp( property, "ChildCount" ) == 0 ) {
Int32 value = static_cast<Int32>( getChildCount( ref ) );
mDBus.messageIterOpenContainer( &iter, 'v', "i", &variant );
appendBasic( variant, 'i', &value );
} else if ( std::strcmp( property, "Parent" ) == 0 ) {
mDBus.messageIterOpenContainer( &iter, 'v', "(so)", &variant );
if ( ref == ApplicationRef )
appendDesktopRef( variant );
else
appendRef( variant, getParent( ref ) );
} else if ( std::strcmp( property, "ToolkitName" ) == 0 ||
std::strcmp( property, "Version" ) == 0 ||
std::strcmp( property, "AtspiVersion" ) == 0 ) {
const char* value = std::strcmp( property, "ToolkitName" ) == 0 ? "eepp" : "0.1";
mDBus.messageIterOpenContainer( &iter, 'v', "s", &variant );
appendBasic( variant, 's', &value );
} else if ( std::strcmp( property, "ProcessId" ) == 0 ) {
Int32 value = static_cast<Int32>( getpid() );
mDBus.messageIterOpenContainer( &iter, 'v', "i", &variant );
appendBasic( variant, 'i', &value );
} else if ( std::strcmp( property, "Id" ) == 0 ) {
mDBus.messageIterOpenContainer( &iter, 'v', "i", &variant );
appendBasic( variant, 'i', &mApplicationId );
} else {
mDBus.messageUnref( reply );
return 1;
}
mDBus.messageIterCloseContainer( &iter, &variant );
send( reply );
return 0;
}
DBusHandlerResult AtSpiApplication::handlePropertiesSet( DBusMessage* request,
AccessibilityNodeRef ref ) {
// The registry assigns the application id; every other writable property is Value.
if ( ref == ApplicationRef ) {
DBusMessageIter variant;
const char* requestedInterface = nullptr;
const char* property = nullptr;
if ( !readPropertySet( request, requestedInterface, property, variant ) ||
mDBus.messageIterGetArgType( &variant ) != 'i' ||
std::strcmp( requestedInterface, "org.a11y.atspi.Application" ) != 0 ||
std::strcmp( property, "Id" ) != 0 )
return 1;
mDBus.messageIterGetBasic( &variant, &mApplicationId );
send( mDBus.messageNewMethodReturn( request ) );
return 0;
}
DBusMessageIter variant;
const char* requestedInterface = nullptr;
const char* property = nullptr;
double value = 0;
if ( !readPropertySet( request, requestedInterface, property, variant ) ||
mDBus.messageIterGetArgType( &variant ) != 'd' )
return 1;
mDBus.messageIterGetBasic( &variant, &value );
if ( !requestedInterface || !property ||
std::strcmp( requestedInterface, "org.a11y.atspi.Value" ) != 0 ||
std::strcmp( property, "CurrentValue" ) != 0 )
return 1;
auto info = getNodeInfo( ref, false );
if ( !info.range.valid ||
!( info.actions & accessibilityActionMask( AccessibilityAction::SetValue ) ) )
return 1;
if ( !mManager || !mManager->performAction( ref, { AccessibilityAction::SetValue,
String( String::toString( value ) ) } ) )
return 1;
send( mDBus.messageNewMethodReturn( request ) );
return 0;
}
}}} // namespace EE::UI::AtSpi
#endif
@@ -0,0 +1,422 @@
#include "accessibilitybackendatspi.hpp"
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD
#include <algorithm>
#include <cstdlib>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/window.hpp>
namespace EE { namespace UI { namespace AtSpi {
AccessibilityNodeRef AtSpiApplication::refFromPath( const char* path ) {
if ( !path )
return {};
if ( std::strcmp( path, RootPath ) == 0 ) {
mManager = mPrimaryManager.load( std::memory_order_acquire );
return ApplicationRef;
}
if ( std::strncmp( path, NodePathPrefix, std::strlen( NodePathPrefix ) ) != 0 )
return {};
char* end = nullptr;
const char* encoded = path + std::strlen( NodePathPrefix );
Uint64 managerId = std::strtoull( encoded, &end, 10 );
if ( !end || *end != '/' )
return {};
auto manager =
std::find_if( mManagers.begin(), mManagers.end(),
[managerId]( const auto& entry ) { return entry.first == managerId; } );
if ( manager == mManagers.end() )
return {};
char* sourceEnd = nullptr;
Uint64 source = std::strtoull( end + 1, &sourceEnd, 10 );
if ( !sourceEnd || *sourceEnd != '/' )
return {};
char* idEnd = nullptr;
Uint64 id = std::strtoull( sourceEnd + 1, &idEnd, 10 );
if ( !idEnd || *idEnd != '\0' )
return {};
mManager = manager->second;
return { static_cast<AccessibilitySourceId>( source ), id };
}
Uint64 AtSpiApplication::currentManagerId() const {
for ( const auto& entry : mManagers ) {
if ( entry.second == mManager )
return entry.first;
}
return 0;
}
AccessibilityNodeInfo AtSpiApplication::getNodeInfo( AccessibilityNodeRef ref,
bool includeValue ) const {
if ( ref != ApplicationRef ) {
auto info = mManager ? mManager->getNodeInfo( ref, includeValue, includeValue )
: AccessibilityNodeInfo{};
if ( isSceneRoot( ref ) )
info.role = AccessibilityRole::Window;
return info;
}
AccessibilityNodeInfo info;
info.role = AccessibilityRole::Application;
info.states =
AccessibilityState::Enabled | AccessibilityState::Visible | AccessibilityState::Showing;
info.name = mName;
return info;
}
AccessibilityNodeRef AtSpiApplication::getParent( AccessibilityNodeRef ref ) {
if ( ref == ApplicationRef )
return {};
if ( isSceneRoot( ref ) )
return ApplicationRef;
return mManager ? mManager->getParent( ref ) : AccessibilityNodeRef{};
}
size_t AtSpiApplication::getChildCount( AccessibilityNodeRef ref ) {
if ( ref == ApplicationRef )
return mManagers.size();
return mManager ? mManager->getChildCount( ref ) : 0;
}
AccessibilityNodeRef AtSpiApplication::getChild( AccessibilityNodeRef ref, size_t index ) {
return mManager && ref != ApplicationRef ? mManager->getChild( ref, index )
: AccessibilityNodeRef{};
}
std::string AtSpiApplication::pathFromRef( AccessibilityNodeRef ref ) {
if ( ref == ApplicationRef )
return RootPath;
const Uint64 managerId = currentManagerId();
return std::string( NodePathPrefix ) + String::toString( managerId ) + "/" +
String::toString( ref.source ) + "/" + String::toString( ref.id );
}
Uint32 AtSpiApplication::role( AccessibilityRole role ) const {
switch ( role ) {
case AccessibilityRole::Application:
return 75;
case AccessibilityRole::Window:
return 69;
case AccessibilityRole::Dialog:
return 16;
case AccessibilityRole::Group:
case AccessibilityRole::TabPanel:
return 39;
case AccessibilityRole::Button:
return 43;
case AccessibilityRole::CheckBox:
return 7;
case AccessibilityRole::RadioButton:
return 44;
case AccessibilityRole::Label:
case AccessibilityRole::Text:
return 29;
case AccessibilityRole::TextBox:
return 79;
case AccessibilityRole::Image:
return 27;
case AccessibilityRole::ComboBox:
return 11;
case AccessibilityRole::Slider:
return 51;
case AccessibilityRole::SpinButton:
return 52;
case AccessibilityRole::ProgressBar:
return 42;
case AccessibilityRole::Tab:
return 37;
case AccessibilityRole::TabList:
return 38;
case AccessibilityRole::MenuBar:
return 34;
case AccessibilityRole::Menu:
return 33;
case AccessibilityRole::MenuItem:
return 35;
case AccessibilityRole::CheckMenuItem:
return 8;
case AccessibilityRole::RadioMenuItem:
return 45;
case AccessibilityRole::List:
return 31;
case AccessibilityRole::ListItem:
return 32;
case AccessibilityRole::Table:
return 55;
case AccessibilityRole::Row:
return 90;
case AccessibilityRole::Cell:
return 56;
case AccessibilityRole::Tree:
return 65;
case AccessibilityRole::TreeItem:
return 91;
default:
return 67;
}
}
const char* AtSpiApplication::roleName( AccessibilityRole role ) const {
switch ( role ) {
case AccessibilityRole::Application:
return "application";
case AccessibilityRole::Window:
return "window";
case AccessibilityRole::Dialog:
return "dialog";
case AccessibilityRole::Group:
case AccessibilityRole::TabPanel:
return "panel";
case AccessibilityRole::Button:
return "push button";
case AccessibilityRole::CheckBox:
return "check box";
case AccessibilityRole::RadioButton:
return "radio button";
case AccessibilityRole::Label:
return "label";
case AccessibilityRole::Text:
return "text";
case AccessibilityRole::TextBox:
return "entry";
case AccessibilityRole::Image:
return "image";
case AccessibilityRole::ComboBox:
return "combo box";
case AccessibilityRole::Slider:
return "slider";
case AccessibilityRole::SpinButton:
return "spin button";
case AccessibilityRole::ProgressBar:
return "progress bar";
case AccessibilityRole::Tab:
return "page tab";
case AccessibilityRole::TabList:
return "page tab list";
case AccessibilityRole::MenuBar:
return "menu bar";
case AccessibilityRole::Menu:
return "menu";
case AccessibilityRole::MenuItem:
return "menu item";
case AccessibilityRole::CheckMenuItem:
return "check menu item";
case AccessibilityRole::RadioMenuItem:
return "radio menu item";
case AccessibilityRole::List:
return "list";
case AccessibilityRole::ListItem:
return "list item";
case AccessibilityRole::Table:
return "table";
case AccessibilityRole::Row:
return "table row";
case AccessibilityRole::Cell:
return "table cell";
case AccessibilityRole::Tree:
return "tree";
case AccessibilityRole::TreeItem:
return "tree item";
default:
return "unknown";
}
}
AccessibilityAction AtSpiApplication::actionAt( AccessibilityActions actions, Int32 index ) const {
for ( Uint32 action = 0; action <= static_cast<Uint32>( AccessibilityAction::ScrollTo );
++action ) {
if ( actions & ( 1u << action ) ) {
if ( index-- == 0 )
return static_cast<AccessibilityAction>( action );
}
}
return AccessibilityAction::Focus;
}
AccessibilityActions AtSpiApplication::nativeActions( AccessibilityActions actions ) const {
return actions & ~( accessibilityActionMask( AccessibilityAction::SetValue ) |
accessibilityActionMask( AccessibilityAction::SetText ) |
accessibilityActionMask( AccessibilityAction::SetTextSelection ) );
}
const char* AtSpiApplication::actionName( AccessibilityAction action ) const {
switch ( action ) {
case AccessibilityAction::Focus:
return "focus";
case AccessibilityAction::Press:
return "click";
case AccessibilityAction::Toggle:
return "toggle";
case AccessibilityAction::Select:
return "select";
case AccessibilityAction::Increment:
return "increment";
case AccessibilityAction::Decrement:
return "decrement";
case AccessibilityAction::SetValue:
return "set value";
case AccessibilityAction::SetText:
return "set text";
case AccessibilityAction::Expand:
return "expand";
case AccessibilityAction::Collapse:
return "collapse";
case AccessibilityAction::ScrollTo:
return "scroll to";
case AccessibilityAction::SetTextSelection:
return "set text selection";
}
return "";
}
bool AtSpiApplication::readPropertySet( DBusMessage* request, const char*& interface,
const char*& property, DBusMessageIter& variant ) {
DBusMessageIter iter;
if ( !mDBus.messageIterInit( request, &iter ) || mDBus.messageIterGetArgType( &iter ) != 's' )
return false;
mDBus.messageIterGetBasic( &iter, &interface );
if ( !mDBus.messageIterNext( &iter ) || mDBus.messageIterGetArgType( &iter ) != 's' )
return false;
mDBus.messageIterGetBasic( &iter, &property );
if ( !mDBus.messageIterNext( &iter ) || mDBus.messageIterGetArgType( &iter ) != 'v' )
return false;
mDBus.messageIterRecurse( &iter, &variant );
return interface && property;
}
void AtSpiApplication::appendRef( DBusMessageIter& iter, AccessibilityNodeRef ref ) {
DBusMessageIter structure;
mDBus.messageIterOpenContainer( &iter, 'r', nullptr, &structure );
const bool valid = ref == ApplicationRef || ref.isValid();
const char* bus = valid ? mBusName.c_str() : "";
std::string pathStorage = valid ? pathFromRef( ref ) : "/org/a11y/atspi/null";
const char* path = pathStorage.c_str();
appendBasic( structure, 's', &bus );
appendBasic( structure, 'o', &path );
mDBus.messageIterCloseContainer( &iter, &structure );
}
void AtSpiApplication::appendEventProperties( DBusMessageIter& iter ) {
DBusMessageIter array;
mDBus.messageIterOpenContainer( &iter, 'a', "{sv}", &array );
mDBus.messageIterCloseContainer( &iter, &array );
}
void AtSpiApplication::appendDesktopRef( DBusMessageIter& iter ) {
DBusMessageIter structure;
mDBus.messageIterOpenContainer( &iter, 'r', nullptr, &structure );
const char* bus = mRegistryBusName.c_str();
const char* path = mRegistryPath.c_str();
appendBasic( structure, 's', &bus );
appendBasic( structure, 'o', &path );
mDBus.messageIterCloseContainer( &iter, &structure );
}
void AtSpiApplication::appendInterfaces( DBusMessageIter& iter, AccessibilityNodeRef ref,
const AccessibilityNodeInfo& info ) {
DBusMessageIter array;
mDBus.messageIterOpenContainer( &iter, 'a', "s", &array );
const char* accessible = "org.a11y.atspi.Accessible";
const char* component = "org.a11y.atspi.Component";
appendBasic( array, 's', &accessible );
appendBasic( array, 's', &component );
if ( nativeActions( info.actions ) ) {
const char* action = "org.a11y.atspi.Action";
appendBasic( array, 's', &action );
}
if ( info.range.valid ) {
const char* value = "org.a11y.atspi.Value";
appendBasic( array, 's', &value );
}
if ( info.text.valid ) {
const char* text = "org.a11y.atspi.Text";
appendBasic( array, 's', &text );
if ( hasState( info.states, AccessibilityState::Editable ) ) {
const char* editableText = "org.a11y.atspi.EditableText";
appendBasic( array, 's', &editableText );
}
}
if ( ref == ApplicationRef ) {
const char* application = "org.a11y.atspi.Application";
appendBasic( array, 's', &application );
}
mDBus.messageIterCloseContainer( &iter, &array );
}
void AtSpiApplication::appendStates( DBusMessageIter& iter, const AccessibilityNodeInfo& info ) {
Uint32 words[2]{};
auto addState = [&words]( bool enabled, Uint32 state ) {
if ( enabled )
words[state / 32] |= 1u << ( state % 32 );
};
Uint64 stateBits = static_cast<Uint64>( info.states );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Active ), 1 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Checked ), 4 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Editable ), 7 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Enabled ), 8 );
addState( info.actions & ( accessibilityActionMask( AccessibilityAction::Expand ) |
accessibilityActionMask( AccessibilityAction::Collapse ) ),
9 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Expanded ), 10 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Focusable ), 11 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Focused ), 12 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Selected ), 23 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Enabled ), 24 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Showing ), 25 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::Visible ), 30 );
addState( info.actions & accessibilityActionMask( AccessibilityAction::Select ), 22 );
addState( info.role == AccessibilityRole::CheckBox ||
info.role == AccessibilityRole::RadioButton ||
info.role == AccessibilityRole::CheckMenuItem ||
info.role == AccessibilityRole::RadioMenuItem,
41 );
addState( stateBits & static_cast<Uint64>( AccessibilityState::ReadOnly ), 43 );
addState( info.role == AccessibilityRole::TextBox &&
hasState( info.states, AccessibilityState::MultiLine ),
17 );
addState( info.role == AccessibilityRole::TextBox &&
!hasState( info.states, AccessibilityState::MultiLine ),
26 );
addState( info.text.valid, 38 );
DBusMessageIter array;
mDBus.messageIterOpenContainer( &iter, 'a', "u", &array );
appendBasic( array, 'u', &words[0] );
appendBasic( array, 'u', &words[1] );
mDBus.messageIterCloseContainer( &iter, &array );
}
Int32 AtSpiApplication::indexInParent( AccessibilityNodeRef ref ) {
if ( isSceneRoot( ref ) ) {
for ( size_t i = 0; i < mManagers.size(); ++i ) {
if ( mManagers[i].second == mManager )
return static_cast<Int32>( i );
}
return -1;
}
return mManager ? mManager->getIndexInParent( ref ) : -1;
}
Math::Rectf AtSpiApplication::boundsForCoordinateType( AccessibilityNodeRef ref,
const AccessibilityNodeInfo& info,
Uint32 coordinateType ) {
auto bounds = info.bounds;
if ( coordinateType == 0 ) {
auto scene = mManager ? mManager->getSceneNode() : nullptr;
if ( scene && scene->getWindow() ) {
auto position = scene->getWindow()->getPosition();
bounds.move( Math::Vector2f( position.x, position.y ) );
}
} else if ( coordinateType == 2 ) {
auto parent = getParent( ref );
if ( parent.isValid() ) {
auto parentBounds = getNodeInfo( parent, false ).bounds;
bounds.move( Math::Vector2f( -parentBounds.Left, -parentBounds.Top ) );
}
}
return bounds;
}
}}} // namespace EE::UI::AtSpi
#endif
@@ -42,14 +42,6 @@ static bool isMainThread() {
return [NSThread isMainThread];
}
static bool hasState( AccessibilityState states, AccessibilityState state ) {
return ( static_cast<Uint64>( states ) & static_cast<Uint64>( state ) ) != 0;
}
static bool hasAction( AccessibilityActions actions, AccessibilityAction action ) {
return ( actions & accessibilityActionMask( action ) ) != 0;
}
static double numericValue( const String& value, bool& valid ) {
double number = 0;
valid = String::fromString( number, value.toUtf8() );
@@ -1891,6 +1883,18 @@ class MacAccessibilityBackend final : public AccessibilityBackend {
}
}
}
// AppKit notifications carry no state: clients re-query the element when they arrive. One
// user action can produce several identical events in a frame (setting a text selection
// does), so post each such notification at most once per element and update. Created and
// Destroyed are structural and keep every occurrence, in order.
if ( event.type != AccessibilityEvent::Created &&
event.type != AccessibilityEvent::Destroyed ) {
for ( const auto& pending : mPendingEvents ) {
if ( pending.type == event.type && pending.ref == event.ref &&
pending.related == event.related )
return;
}
}
mPendingEvents.emplace_back( event );
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,612 @@
#ifndef EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDUIA_HPP
#define EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDUIA_HPP
#include <eepp/config.hpp>
#if EE_PLATFORM == EE_PLATFORM_WIN
#include "accessibilitybackend.hpp"
#include <eepp/ui/accessibility/accessibilitymanager.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/window.hpp>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cwctype>
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <vector>
// clang-format off
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <commctrl.h>
// clang-format on
#include <uiautomationclient.h>
#include <uiautomationcore.h>
#include <uiautomationcoreapi.h>
/**
* Windows UI Automation accessibility backend.
*
* Each top-level window owns a UIAutomationProviderContext. UIA calls arrive on arbitrary COM
* threads; every query is marshalled to the UI thread through invoke(), which posts a window
* message and waits (bounded by ProviderCallTimeoutMilliseconds). The implementation is split:
* - accessibilitybackenduia.cpp: provider cache, window subclass, manager events -> UIA
* events, and the AccessibilityBackend entry point.
* - accessibilitybackenduiaprovider.cpp: the element provider (IRawElementProvider* and the
* control patterns), plus role and VARIANT helpers.
* - accessibilitybackenduiatext.cpp: ITextRangeProvider and UTF-16 text unit navigation.
*/
namespace EE { namespace UI { namespace Uia {
using namespace EE::Scene;
constexpr DWORD ProviderCallTimeoutMilliseconds = 2000;
inline UINT accessibilityDispatchMessage() {
static const UINT message = RegisterWindowMessageW( L"eepp.UIAutomation.Dispatch" );
return message;
}
class UIAutomationProvider;
class UIAutomationProviderContext final
: public std::enable_shared_from_this<UIAutomationProviderContext> {
private:
class PendingRequest {
public:
virtual ~PendingRequest() = default;
virtual void execute( AccessibilityManager& manager ) = 0;
void cancel( HRESULT result ) {
std::lock_guard<std::mutex> lock( mMutex );
if ( mComplete )
return;
mResult = result;
mComplete = true;
mCondition.notify_all();
}
HRESULT wait() {
std::unique_lock<std::mutex> lock( mMutex );
if ( !mCondition.wait_for( lock,
std::chrono::milliseconds( ProviderCallTimeoutMilliseconds ),
[&] { return mComplete; } ) ) {
mResult = UIA_E_TIMEOUT;
mComplete = true;
}
return mResult;
}
protected:
bool begin() {
std::lock_guard<std::mutex> lock( mMutex );
if ( mComplete )
return false;
return true;
}
void finish( HRESULT result ) {
std::lock_guard<std::mutex> lock( mMutex );
if ( mComplete )
return;
mResult = result;
mComplete = true;
mCondition.notify_all();
}
std::mutex mMutex;
std::condition_variable mCondition;
HRESULT mResult{ E_FAIL };
bool mComplete{ false };
};
template <typename T> class TypedPendingRequest final : public PendingRequest {
public:
using Callback = std::function<HRESULT( AccessibilityManager&, T& )>;
explicit TypedPendingRequest( Callback callback, IUnknown* owner ) :
mCallback( std::move( callback ) ), mOwner( owner ) {
if ( mOwner )
mOwner->AddRef();
}
~TypedPendingRequest() override {
if ( mOwner )
mOwner->Release();
}
void execute( AccessibilityManager& manager ) override {
if ( !begin() )
return;
T value{};
const HRESULT result = mCallback( manager, value );
{
std::lock_guard<std::mutex> lock( mMutex );
if ( !mComplete && SUCCEEDED( result ) )
mValue = std::move( value );
}
finish( result );
}
HRESULT wait( T& value ) {
const HRESULT result = PendingRequest::wait();
if ( SUCCEEDED( result ) ) {
std::lock_guard<std::mutex> lock( mMutex );
value = std::move( mValue );
}
return result;
}
private:
Callback mCallback;
T mValue{};
IUnknown* mOwner{};
};
public:
UIAutomationProviderContext( AccessibilityManager& manager, HWND window, LONG runtimeScope ) :
mManager( &manager ),
mWindow( window ),
mUIThread( GetCurrentThreadId() ),
mRuntimeScope( runtimeScope ),
mRootRef( manager.getRoot() ) {}
~UIAutomationProviderContext() { detach(); }
UIAutomationProviderContext( const UIAutomationProviderContext& ) = delete;
UIAutomationProviderContext& operator=( const UIAutomationProviderContext& ) = delete;
bool isAlive() const { return mAlive.load( std::memory_order_acquire ); }
HWND window() const { return mWindow.load( std::memory_order_acquire ); }
LONG runtimeScope() const { return mRuntimeScope; }
AccessibilityNodeRef rootRef() const { return mRootRef; }
void updateClientState() {
const bool listening = UiaClientsAreListening();
mClientsListening.store( listening, std::memory_order_release );
if ( !listening && mAdvisedEventCount.load( std::memory_order_acquire ) == 0 )
mClientObserved.store( false, std::memory_order_release );
}
bool hasActiveClients() const {
// A listener elsewhere on the desktop is not evidence that it uses this window.
return isAlive() && ( mAdvisedEventCount.load( std::memory_order_acquire ) != 0 ||
( mClientsListening.load( std::memory_order_acquire ) &&
mClientObserved.load( std::memory_order_acquire ) ) );
}
void clientObserved() {
mClientObserved.store( true, std::memory_order_release );
if ( mManager )
mManager->onNativeClientObserved();
}
void adviseEventAdded() { mAdvisedEventCount.fetch_add( 1, std::memory_order_relaxed ); }
void adviseEventRemoved() {
long count = mAdvisedEventCount.load( std::memory_order_relaxed );
while ( count > 0 && !mAdvisedEventCount.compare_exchange_weak(
count, count - 1, std::memory_order_relaxed ) ) {
}
}
template <typename T, typename Callback>
HRESULT invoke( T& value, Callback&& callback, IUnknown* owner = nullptr ) {
if ( !isAlive() )
return UIA_E_ELEMENTNOTAVAILABLE;
if ( GetCurrentThreadId() == mUIThread ) {
if ( !mManager )
return UIA_E_ELEMENTNOTAVAILABLE;
return callback( *mManager, value );
}
using Request = TypedPendingRequest<T>;
auto request = std::make_shared<Request>(
typename Request::Callback{ std::forward<Callback>( callback ) }, owner );
{
std::lock_guard<std::mutex> lock( mPendingMutex );
if ( !isAlive() )
return UIA_E_ELEMENTNOTAVAILABLE;
mPendingRequests.emplace_back( request );
}
const HWND nativeWindow = window();
if ( !nativeWindow ||
!PostMessageW( nativeWindow, accessibilityDispatchMessage(), 0, 0 ) ) {
request->cancel( UIA_E_ELEMENTNOTAVAILABLE );
removePendingRequest( request.get() );
}
const HRESULT result = request->wait( value );
if ( result == static_cast<HRESULT>( UIA_E_TIMEOUT ) ) {
request->cancel( result );
removePendingRequest( request.get() );
}
return result;
}
UIAutomationProvider* provider( AccessibilityNodeRef ref );
void dispatchPendingRequests() {
std::vector<std::shared_ptr<PendingRequest>> pending;
{
std::lock_guard<std::mutex> lock( mPendingMutex );
pending.swap( mPendingRequests );
}
AccessibilityManager* manager = mManager;
for ( const auto& request : pending ) {
if ( manager && isAlive() )
request->execute( *manager );
else
request->cancel( UIA_E_ELEMENTNOTAVAILABLE );
}
}
void invalidateProvider( AccessibilityNodeRef ref );
void invalidateSource( AccessibilitySourceId sourceId );
void pruneSource( AccessibilitySourceId sourceId );
void disconnectInvalidatedProviders();
void detach();
private:
void removePendingRequest( PendingRequest* request ) {
std::lock_guard<std::mutex> lock( mPendingMutex );
for ( auto iterator = mPendingRequests.begin(); iterator != mPendingRequests.end();
++iterator ) {
if ( iterator->get() == request ) {
mPendingRequests.erase( iterator );
break;
}
}
}
AccessibilityManager* mManager{};
std::atomic<HWND> mWindow{};
DWORD mUIThread{};
LONG mRuntimeScope{};
AccessibilityNodeRef mRootRef;
std::atomic<bool> mAlive{ true };
std::atomic<bool> mClientsListening{ false };
std::atomic<bool> mClientObserved{ false };
std::atomic<long> mAdvisedEventCount{ 0 };
std::mutex mProviderMutex;
std::unordered_map<AccessibilitySourceId, std::unordered_map<Uint64, UIAutomationProvider*>>
mProviders;
std::vector<UIAutomationProvider*> mInvalidatedProviders;
std::mutex mPendingMutex;
std::vector<std::shared_ptr<PendingRequest>> mPendingRequests;
};
struct NavigationResult {
AccessibilityNodeRef target;
};
struct GeometryResult {
Math::Rectf bounds;
POINT clientOrigin{};
bool boundsValid{ false };
bool windowVisible{ false };
};
struct TextSnapshot {
String source;
std::u16string text;
AccessibilityActions actions{};
int selectionStart{};
int selectionEnd{};
Math::Rectf bounds;
bool boundsValid{ false };
};
// Role, VARIANT and runtime id helpers: accessibilitybackenduiaprovider.cpp
CONTROLTYPEID controlType( AccessibilityRole role );
bool isSelectionContainerRole( AccessibilityRole role );
HRESULT setBstr( VARIANT* value, const String& string );
HRESULT setNotSupported( VARIANT* value );
std::vector<int> runtimeIdValues( LONG scope, AccessibilityNodeRef ref );
SAFEARRAY* safeArrayFromInts( const std::vector<int>& values );
// UTF-16 text helpers: accessibilitybackenduiatext.cpp
size_t nextSourceCodePoint( const std::u16string& text, size_t index );
size_t codePointToUTF16( const String& string, Int32 offset );
Int32 utf16ToCodePoint( const String& string, size_t offset );
std::u16string normalizedUTF16( const String& string );
HRESULT textSnapshot( const std::shared_ptr<UIAutomationProviderContext>& context,
AccessibilityNodeRef ref, TextSnapshot& snapshot );
bool isLowSurrogate( char16_t value );
bool isHighSurrogate( char16_t value );
size_t nextCharacterBoundary( const std::u16string& text, size_t offset );
size_t previousCharacterBoundary( const std::u16string& text, size_t offset );
bool isTextSpace( char16_t value );
std::pair<size_t, size_t> enclosingTextUnit( const std::u16string& text, size_t offset,
TextUnit unit );
size_t moveTextEndpoint( const std::u16string& text, size_t offset, TextUnit unit, int count,
int& moved );
class UIAutomationTextRange final : public ITextRangeProvider {
public:
UIAutomationTextRange( std::shared_ptr<UIAutomationProviderContext> context,
AccessibilityNodeRef ref, int start, int end ) :
mContext( std::move( context ) ), mRef( ref ), mStart( start ), mEnd( end ) {}
HRESULT STDMETHODCALLTYPE QueryInterface( REFIID interfaceId, void** object ) override;
ULONG STDMETHODCALLTYPE AddRef() override {
return mReferences.fetch_add( 1, std::memory_order_relaxed ) + 1;
}
ULONG STDMETHODCALLTYPE Release() override;
HRESULT STDMETHODCALLTYPE Clone( ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE Compare( ITextRangeProvider* range, BOOL* same ) override;
HRESULT STDMETHODCALLTYPE CompareEndpoints( TextPatternRangeEndpoint endpoint,
ITextRangeProvider* targetRange,
TextPatternRangeEndpoint targetEndpoint,
int* comparison ) override;
HRESULT STDMETHODCALLTYPE ExpandToEnclosingUnit( TextUnit unit ) override;
HRESULT STDMETHODCALLTYPE FindAttribute( TEXTATTRIBUTEID, VARIANT, BOOL,
ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE FindText( BSTR text, BOOL backward, BOOL ignoreCase,
ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE GetAttributeValue( TEXTATTRIBUTEID, VARIANT* value ) override;
HRESULT STDMETHODCALLTYPE GetBoundingRectangles( SAFEARRAY** rectangles ) override;
HRESULT STDMETHODCALLTYPE GetEnclosingElement( IRawElementProviderSimple** element ) override;
HRESULT STDMETHODCALLTYPE GetText( int maxLength, BSTR* text ) override;
HRESULT STDMETHODCALLTYPE Move( TextUnit unit, int count, int* moved ) override;
HRESULT STDMETHODCALLTYPE MoveEndpointByUnit( TextPatternRangeEndpoint endpoint, TextUnit unit,
int count, int* moved ) override;
HRESULT STDMETHODCALLTYPE
MoveEndpointByRange( TextPatternRangeEndpoint endpoint, ITextRangeProvider* targetRange,
TextPatternRangeEndpoint targetEndpoint ) override;
HRESULT STDMETHODCALLTYPE Select() override;
HRESULT STDMETHODCALLTYPE AddToSelection() override { return UIA_E_INVALIDOPERATION; }
HRESULT STDMETHODCALLTYPE RemoveFromSelection() override { return UIA_E_INVALIDOPERATION; }
HRESULT STDMETHODCALLTYPE ScrollIntoView( BOOL ) override;
HRESULT STDMETHODCALLTYPE GetChildren( SAFEARRAY** children ) override;
private:
~UIAutomationTextRange() = default;
template <typename T, typename Callback> HRESULT invoke( T& value, Callback&& callback ) const {
return mContext->invoke(
value, std::forward<Callback>( callback ),
static_cast<ITextRangeProvider*>( const_cast<UIAutomationTextRange*>( this ) ) );
}
HRESULT currentSnapshot( TextSnapshot& snapshot ) const {
return textSnapshot( mContext, mRef, snapshot );
}
std::pair<int, int> endpoints() const;
std::pair<int, int> normalizedEndpoints( size_t length ) const;
void setEndpoints( int start, int end );
std::atomic<ULONG> mReferences{ 1 };
std::shared_ptr<UIAutomationProviderContext> mContext;
AccessibilityNodeRef mRef;
mutable std::mutex mMutex;
int mStart{};
int mEnd{};
};
class UIAutomationProvider final : public IRawElementProviderSimple,
public IRawElementProviderFragment,
public IRawElementProviderFragmentRoot,
public IRawElementProviderAdviseEvents,
public IInvokeProvider,
public IToggleProvider,
public ISelectionProvider,
public ISelectionItemProvider,
public IValueProvider,
public IRangeValueProvider,
public IExpandCollapseProvider,
public IScrollItemProvider,
public ITextProvider {
public:
UIAutomationProvider( std::shared_ptr<UIAutomationProviderContext> context,
AccessibilityNodeRef ref, bool root ) :
mContext( std::move( context ) ), mRef( ref ), mRoot( root ) {}
void detach() { mDetached.store( true, std::memory_order_release ); }
HRESULT STDMETHODCALLTYPE QueryInterface( REFIID interfaceId, void** object ) override;
ULONG STDMETHODCALLTYPE AddRef() override {
return mReferences.fetch_add( 1, std::memory_order_relaxed ) + 1;
}
ULONG STDMETHODCALLTYPE Release() override;
HRESULT STDMETHODCALLTYPE get_ProviderOptions( ProviderOptions* options ) override;
HRESULT STDMETHODCALLTYPE GetPatternProvider( PATTERNID patternId,
IUnknown** provider ) override;
HRESULT STDMETHODCALLTYPE GetPropertyValue( PROPERTYID propertyId, VARIANT* value ) override;
HRESULT STDMETHODCALLTYPE
get_HostRawElementProvider( IRawElementProviderSimple** provider ) override;
HRESULT STDMETHODCALLTYPE Navigate( NavigateDirection direction,
IRawElementProviderFragment** provider ) override;
HRESULT STDMETHODCALLTYPE GetRuntimeId( SAFEARRAY** runtimeId ) override;
HRESULT STDMETHODCALLTYPE get_BoundingRectangle( UiaRect* rectangle ) override;
HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots( SAFEARRAY** roots ) override;
HRESULT STDMETHODCALLTYPE SetFocus() override { return perform( AccessibilityAction::Focus ); }
HRESULT STDMETHODCALLTYPE get_FragmentRoot( IRawElementProviderFragmentRoot** root ) override;
HRESULT STDMETHODCALLTYPE
ElementProviderFromPoint( double x, double y, IRawElementProviderFragment** provider ) override;
HRESULT STDMETHODCALLTYPE GetFocus( IRawElementProviderFragment** provider ) override;
HRESULT STDMETHODCALLTYPE AdviseEventAdded( EVENTID, SAFEARRAY* ) override;
HRESULT STDMETHODCALLTYPE AdviseEventRemoved( EVENTID, SAFEARRAY* ) override;
HRESULT STDMETHODCALLTYPE Invoke() override { return perform( AccessibilityAction::Press ); }
HRESULT STDMETHODCALLTYPE Toggle() override { return perform( AccessibilityAction::Toggle ); }
HRESULT STDMETHODCALLTYPE get_ToggleState( ToggleState* state ) override;
HRESULT STDMETHODCALLTYPE GetSelection( SAFEARRAY** selection ) override;
HRESULT STDMETHODCALLTYPE get_CanSelectMultiple( BOOL* canSelectMultiple ) override;
HRESULT STDMETHODCALLTYPE get_IsSelectionRequired( BOOL* selectionRequired ) override;
HRESULT STDMETHODCALLTYPE Select() override { return perform( AccessibilityAction::Select ); }
HRESULT STDMETHODCALLTYPE AddToSelection() override { return Select(); }
HRESULT STDMETHODCALLTYPE RemoveFromSelection() override { return UIA_E_INVALIDOPERATION; }
HRESULT STDMETHODCALLTYPE get_IsSelected( BOOL* selected ) override;
HRESULT STDMETHODCALLTYPE
get_SelectionContainer( IRawElementProviderSimple** provider ) override;
HRESULT STDMETHODCALLTYPE SetValue( LPCWSTR value ) override;
HRESULT STDMETHODCALLTYPE get_Value( BSTR* value ) override;
HRESULT STDMETHODCALLTYPE get_IsReadOnly( BOOL* readOnly ) override;
HRESULT STDMETHODCALLTYPE Expand() override { return perform( AccessibilityAction::Expand ); }
HRESULT STDMETHODCALLTYPE Collapse() override {
return perform( AccessibilityAction::Collapse );
}
HRESULT STDMETHODCALLTYPE get_ExpandCollapseState( ExpandCollapseState* state ) override;
HRESULT STDMETHODCALLTYPE ScrollIntoView() override {
return perform( AccessibilityAction::ScrollTo );
}
HRESULT STDMETHODCALLTYPE SetValue( double value ) override;
HRESULT STDMETHODCALLTYPE get_Value( double* value ) override;
HRESULT STDMETHODCALLTYPE get_Maximum( double* value ) override {
return rangeValue( value, &AccessibilityRangeInfo::maximum );
}
HRESULT STDMETHODCALLTYPE get_Minimum( double* value ) override {
return rangeValue( value, &AccessibilityRangeInfo::minimum );
}
HRESULT STDMETHODCALLTYPE get_LargeChange( double* value ) override {
return rangeValue( value, &AccessibilityRangeInfo::largeChange );
}
HRESULT STDMETHODCALLTYPE get_SmallChange( double* value ) override {
return rangeValue( value, &AccessibilityRangeInfo::smallChange );
}
HRESULT STDMETHODCALLTYPE GetVisibleRanges( SAFEARRAY** ranges ) override;
HRESULT STDMETHODCALLTYPE RangeFromChild( IRawElementProviderSimple* child,
ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE RangeFromPoint( UiaPoint point, ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE get_DocumentRange( ITextRangeProvider** range ) override;
HRESULT STDMETHODCALLTYPE
get_SupportedTextSelection( SupportedTextSelection* selection ) override;
private:
~UIAutomationProvider() = default;
template <typename T, typename Callback> HRESULT invoke( T& value, Callback&& callback ) const {
// The request, not the waiting caller, owns this reference. A timeout may return while
// the UI thread is still executing its callback; raw [this] captures must remain alive.
return mContext->invoke(
value, std::forward<Callback>( callback ),
static_cast<IRawElementProviderSimple*>( const_cast<UIAutomationProvider*>( this ) ) );
}
bool unavailable() const {
return mDetached.load( std::memory_order_acquire ) || !mContext || !mContext->isAlive();
}
HRESULT info( AccessibilityNodeInfo& nodeInfo, bool includeValue = true ) const;
HRESULT perform( AccessibilityAction action );
HRESULT rangeValue( double* value, double AccessibilityRangeInfo::* member );
std::atomic<ULONG> mReferences{ 1 };
std::shared_ptr<UIAutomationProviderContext> mContext;
AccessibilityNodeRef mRef;
bool mRoot{ false };
std::atomic<bool> mDetached{ false };
};
}}} // namespace EE::UI::Uia
#endif
#endif
@@ -0,0 +1,885 @@
#include "accessibilitybackenduia.hpp"
#if EE_PLATFORM == EE_PLATFORM_WIN
namespace EE { namespace UI { namespace Uia {
CONTROLTYPEID controlType( AccessibilityRole role ) {
switch ( role ) {
case AccessibilityRole::Application:
case AccessibilityRole::Window:
case AccessibilityRole::Dialog:
return UIA_WindowControlTypeId;
case AccessibilityRole::Button:
return UIA_ButtonControlTypeId;
case AccessibilityRole::CheckBox:
return UIA_CheckBoxControlTypeId;
case AccessibilityRole::RadioButton:
return UIA_RadioButtonControlTypeId;
case AccessibilityRole::Label:
case AccessibilityRole::Text:
return UIA_TextControlTypeId;
case AccessibilityRole::TextBox:
return UIA_EditControlTypeId;
case AccessibilityRole::Image:
return UIA_ImageControlTypeId;
case AccessibilityRole::ComboBox:
return UIA_ComboBoxControlTypeId;
case AccessibilityRole::Slider:
return UIA_SliderControlTypeId;
case AccessibilityRole::SpinButton:
return UIA_SpinnerControlTypeId;
case AccessibilityRole::ProgressBar:
return UIA_ProgressBarControlTypeId;
case AccessibilityRole::Tab:
return UIA_TabItemControlTypeId;
case AccessibilityRole::TabList:
return UIA_TabControlTypeId;
case AccessibilityRole::TabPanel:
return UIA_PaneControlTypeId;
case AccessibilityRole::List:
return UIA_ListControlTypeId;
case AccessibilityRole::ListItem:
return UIA_ListItemControlTypeId;
case AccessibilityRole::Table:
return UIA_DataGridControlTypeId;
case AccessibilityRole::Row:
case AccessibilityRole::Cell:
return UIA_DataItemControlTypeId;
case AccessibilityRole::Tree:
return UIA_TreeControlTypeId;
case AccessibilityRole::TreeItem:
return UIA_TreeItemControlTypeId;
case AccessibilityRole::MenuBar:
return UIA_MenuBarControlTypeId;
case AccessibilityRole::Menu:
return UIA_MenuControlTypeId;
case AccessibilityRole::MenuItem:
case AccessibilityRole::CheckMenuItem:
case AccessibilityRole::RadioMenuItem:
return UIA_MenuItemControlTypeId;
case AccessibilityRole::Group:
case AccessibilityRole::None:
default:
return UIA_GroupControlTypeId;
}
}
bool isSelectionContainerRole( AccessibilityRole role ) {
return role == AccessibilityRole::TabList || role == AccessibilityRole::List ||
role == AccessibilityRole::Table || role == AccessibilityRole::Tree;
}
HRESULT setBstr( VARIANT* value, const String& string ) {
const auto wide = string.toWideString();
value->vt = VT_BSTR;
value->bstrVal = SysAllocStringLen( wide.data(), static_cast<UINT>( wide.size() ) );
return value->bstrVal ? S_OK : E_OUTOFMEMORY;
}
HRESULT setNotSupported( VARIANT* value ) {
IUnknown* reserved{};
const HRESULT result = UiaGetReservedNotSupportedValue( &reserved );
if ( FAILED( result ) )
return result;
value->vt = VT_UNKNOWN;
value->punkVal = reserved;
return S_OK;
}
std::vector<int> runtimeIdValues( LONG scope, AccessibilityNodeRef ref ) {
return { UiaAppendRuntimeId,
scope,
static_cast<int>( ref.source & 0xffffffffu ),
static_cast<int>( ( ref.source >> 32u ) & 0xffffffffu ),
static_cast<int>( ref.id & 0xffffffffu ),
static_cast<int>( ( ref.id >> 32u ) & 0xffffffffu ) };
}
SAFEARRAY* safeArrayFromInts( const std::vector<int>& values ) {
SAFEARRAY* array = SafeArrayCreateVector( VT_I4, 0, static_cast<ULONG>( values.size() ) );
if ( !array )
return nullptr;
for ( LONG index = 0; index < static_cast<LONG>( values.size() ); ++index ) {
int value = values[static_cast<size_t>( index )];
if ( FAILED( SafeArrayPutElement( array, &index, &value ) ) ) {
SafeArrayDestroy( array );
return nullptr;
}
}
return array;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::QueryInterface( REFIID interfaceId,
void** object ) {
if ( !object )
return E_INVALIDARG;
*object = nullptr;
if ( interfaceId == __uuidof( IUnknown ) ||
interfaceId == __uuidof( IRawElementProviderSimple ) ) {
*object = static_cast<IRawElementProviderSimple*>( this );
} else if ( interfaceId == __uuidof( IRawElementProviderFragment ) ) {
*object = static_cast<IRawElementProviderFragment*>( this );
} else if ( interfaceId == __uuidof( IRawElementProviderFragmentRoot ) && mRoot ) {
*object = static_cast<IRawElementProviderFragmentRoot*>( this );
} else if ( interfaceId == __uuidof( IRawElementProviderAdviseEvents ) && mRoot ) {
*object = static_cast<IRawElementProviderAdviseEvents*>( this );
} else {
// COM probes for marshalling/agility must not enqueue a UI-thread query.
if ( interfaceId != __uuidof( IInvokeProvider ) &&
interfaceId != __uuidof( IToggleProvider ) &&
interfaceId != __uuidof( ISelectionProvider ) &&
interfaceId != __uuidof( ISelectionItemProvider ) &&
interfaceId != __uuidof( IValueProvider ) &&
interfaceId != __uuidof( IRangeValueProvider ) &&
interfaceId != __uuidof( IExpandCollapseProvider ) &&
interfaceId != __uuidof( IScrollItemProvider ) &&
interfaceId != __uuidof( ITextProvider ) )
return E_NOINTERFACE;
AccessibilityNodeInfo nodeInfo;
if ( FAILED( info( nodeInfo, false ) ) )
return E_NOINTERFACE;
if ( interfaceId == __uuidof( IInvokeProvider ) &&
hasAction( nodeInfo.actions, AccessibilityAction::Press ) )
*object = static_cast<IInvokeProvider*>( this );
else if ( interfaceId == __uuidof( IToggleProvider ) &&
hasAction( nodeInfo.actions, AccessibilityAction::Toggle ) )
*object = static_cast<IToggleProvider*>( this );
else if ( interfaceId == __uuidof( ISelectionProvider ) &&
isSelectionContainerRole( nodeInfo.role ) )
*object = static_cast<ISelectionProvider*>( this );
else if ( interfaceId == __uuidof( ISelectionItemProvider ) &&
( hasAction( nodeInfo.actions, AccessibilityAction::Select ) ||
hasState( nodeInfo.states, AccessibilityState::Selected ) ) )
*object = static_cast<ISelectionItemProvider*>( this );
else if ( interfaceId == __uuidof( IValueProvider ) &&
( hasAction( nodeInfo.actions, AccessibilityAction::SetText ) ||
nodeInfo.role == AccessibilityRole::ComboBox ) )
*object = static_cast<IValueProvider*>( this );
else if ( interfaceId == __uuidof( IRangeValueProvider ) && nodeInfo.range.valid )
*object = static_cast<IRangeValueProvider*>( this );
else if ( interfaceId == __uuidof( IExpandCollapseProvider ) &&
( hasAction( nodeInfo.actions, AccessibilityAction::Expand ) ||
hasAction( nodeInfo.actions, AccessibilityAction::Collapse ) ) )
*object = static_cast<IExpandCollapseProvider*>( this );
else if ( interfaceId == __uuidof( IScrollItemProvider ) &&
hasAction( nodeInfo.actions, AccessibilityAction::ScrollTo ) )
*object = static_cast<IScrollItemProvider*>( this );
else if ( interfaceId == __uuidof( ITextProvider ) && nodeInfo.text.valid &&
!hasState( nodeInfo.states, AccessibilityState::Protected ) )
*object = static_cast<ITextProvider*>( this );
}
if ( !*object )
return E_NOINTERFACE;
AddRef();
return S_OK;
}
ULONG STDMETHODCALLTYPE UIAutomationProvider::Release() {
const ULONG references = mReferences.fetch_sub( 1, std::memory_order_acq_rel ) - 1;
if ( references == 0 )
delete this;
return references;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_ProviderOptions( ProviderOptions* options ) {
if ( !options )
return E_INVALIDARG;
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
*options = static_cast<ProviderOptions>( ProviderOptions_ServerSideProvider |
ProviderOptions_ProviderOwnsSetFocus |
ProviderOptions_UseComThreading );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetPatternProvider( PATTERNID patternId,
IUnknown** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
HRESULT result = E_NOINTERFACE;
if ( patternId == UIA_InvokePatternId )
result =
QueryInterface( __uuidof( IInvokeProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_TogglePatternId )
result =
QueryInterface( __uuidof( IToggleProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_SelectionPatternId )
result =
QueryInterface( __uuidof( ISelectionProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_SelectionItemPatternId )
result = QueryInterface( __uuidof( ISelectionItemProvider ),
reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_ValuePatternId )
result = QueryInterface( __uuidof( IValueProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_RangeValuePatternId )
result =
QueryInterface( __uuidof( IRangeValueProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_ExpandCollapsePatternId )
result = QueryInterface( __uuidof( IExpandCollapseProvider ),
reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_ScrollItemPatternId )
result =
QueryInterface( __uuidof( IScrollItemProvider ), reinterpret_cast<void**>( provider ) );
else if ( patternId == UIA_TextPatternId )
result = QueryInterface( __uuidof( ITextProvider ), reinterpret_cast<void**>( provider ) );
return result == E_NOINTERFACE ? S_OK : result;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetPropertyValue( PROPERTYID propertyId,
VARIANT* value ) {
if ( !value )
return E_INVALIDARG;
VariantInit( value );
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, propertyId == UIA_ValueValuePropertyId ||
propertyId == UIA_RangeValueValuePropertyId );
if ( FAILED( result ) )
return result;
if ( propertyId == UIA_NamePropertyId )
return setBstr( value, nodeInfo.name );
if ( propertyId == UIA_HelpTextPropertyId )
return setBstr( value, nodeInfo.description );
if ( propertyId == UIA_ControlTypePropertyId ) {
value->vt = VT_I4;
value->lVal = controlType( nodeInfo.role );
} else if ( propertyId == UIA_IsEnabledPropertyId ) {
value->vt = VT_BOOL;
value->boolVal =
hasState( nodeInfo.states, AccessibilityState::Enabled ) ? VARIANT_TRUE : VARIANT_FALSE;
} else if ( propertyId == UIA_HasKeyboardFocusPropertyId ) {
value->vt = VT_BOOL;
value->boolVal =
hasState( nodeInfo.states, AccessibilityState::Focused ) ? VARIANT_TRUE : VARIANT_FALSE;
} else if ( propertyId == UIA_IsKeyboardFocusablePropertyId ) {
value->vt = VT_BOOL;
value->boolVal = hasState( nodeInfo.states, AccessibilityState::Focusable ) ? VARIANT_TRUE
: VARIANT_FALSE;
} else if ( propertyId == UIA_AutomationIdPropertyId ) {
return setBstr( value, String::fromUtf8( String::toString( mRef.source ) + ":" +
String::toString( mRef.id ) ) );
} else if ( propertyId == UIA_FrameworkIdPropertyId ) {
value->vt = VT_BSTR;
value->bstrVal = SysAllocString( L"eepp" );
if ( !value->bstrVal )
return E_OUTOFMEMORY;
} else if ( propertyId == UIA_ClassNamePropertyId ) {
value->vt = VT_BSTR;
value->bstrVal = SysAllocString( mRoot ? L"SDL_app" : L"eepp" );
if ( !value->bstrVal )
return E_OUTOFMEMORY;
} else if ( propertyId == UIA_IsControlElementPropertyId ||
propertyId == UIA_IsContentElementPropertyId ) {
value->vt = VT_BOOL;
value->boolVal = nodeInfo.role != AccessibilityRole::None ? VARIANT_TRUE : VARIANT_FALSE;
} else if ( propertyId == UIA_IsOffscreenPropertyId ) {
value->vt = VT_BOOL;
const HWND nativeWindow = mContext->window();
value->boolVal = hasState( nodeInfo.states, AccessibilityState::Showing ) &&
IsWindowVisible( nativeWindow ) && !IsIconic( nativeWindow )
? VARIANT_FALSE
: VARIANT_TRUE;
} else if ( propertyId == UIA_IsPasswordPropertyId ) {
value->vt = VT_BOOL;
value->boolVal = hasState( nodeInfo.states, AccessibilityState::Protected ) ? VARIANT_TRUE
: VARIANT_FALSE;
} else if ( propertyId == UIA_ProcessIdPropertyId ) {
value->vt = VT_I4;
value->lVal = static_cast<LONG>( GetCurrentProcessId() );
} else if ( propertyId == UIA_NativeWindowHandlePropertyId && mRoot ) {
value->vt = VT_I4;
value->lVal = static_cast<LONG>( reinterpret_cast<LONG_PTR>( mContext->window() ) );
} else if ( propertyId == UIA_ValueValuePropertyId &&
( hasAction( nodeInfo.actions, AccessibilityAction::SetText ) ||
nodeInfo.role == AccessibilityRole::ComboBox ) ) {
return setBstr( value, nodeInfo.value );
} else if ( propertyId == UIA_ValueIsReadOnlyPropertyId ) {
value->vt = VT_BOOL;
value->boolVal = hasAction( nodeInfo.actions, AccessibilityAction::SetText ) ? VARIANT_FALSE
: VARIANT_TRUE;
} else if ( propertyId == UIA_RangeValueValuePropertyId && nodeInfo.range.valid ) {
double number{};
if ( !String::fromString( number, nodeInfo.value.toUtf8() ) || !std::isfinite( number ) )
return UIA_E_INVALIDOPERATION;
value->vt = VT_R8;
value->dblVal = number;
} else if ( propertyId == UIA_RangeValueMinimumPropertyId && nodeInfo.range.valid ) {
value->vt = VT_R8;
value->dblVal = nodeInfo.range.minimum;
} else if ( propertyId == UIA_RangeValueMaximumPropertyId && nodeInfo.range.valid ) {
value->vt = VT_R8;
value->dblVal = nodeInfo.range.maximum;
} else if ( propertyId == UIA_RangeValueSmallChangePropertyId && nodeInfo.range.valid ) {
value->vt = VT_R8;
value->dblVal = nodeInfo.range.smallChange;
} else if ( propertyId == UIA_RangeValueLargeChangePropertyId && nodeInfo.range.valid ) {
value->vt = VT_R8;
value->dblVal = nodeInfo.range.largeChange;
} else if ( propertyId == UIA_RangeValueIsReadOnlyPropertyId && nodeInfo.range.valid ) {
value->vt = VT_BOOL;
value->boolVal = hasAction( nodeInfo.actions, AccessibilityAction::SetValue )
? VARIANT_FALSE
: VARIANT_TRUE;
} else if ( propertyId == UIA_ToggleToggleStatePropertyId &&
hasAction( nodeInfo.actions, AccessibilityAction::Toggle ) ) {
value->vt = VT_I4;
value->lVal = hasState( nodeInfo.states, AccessibilityState::Checked ) ? ToggleState_On
: ToggleState_Off;
} else if ( propertyId == UIA_SelectionItemIsSelectedPropertyId &&
( hasAction( nodeInfo.actions, AccessibilityAction::Select ) ||
hasState( nodeInfo.states, AccessibilityState::Selected ) ) ) {
value->vt = VT_BOOL;
value->boolVal = hasState( nodeInfo.states, AccessibilityState::Selected ) ? VARIANT_TRUE
: VARIANT_FALSE;
} else if ( propertyId == UIA_SelectionCanSelectMultiplePropertyId &&
isSelectionContainerRole( nodeInfo.role ) ) {
value->vt = VT_BOOL;
value->boolVal = VARIANT_FALSE;
} else if ( propertyId == UIA_SelectionIsSelectionRequiredPropertyId &&
isSelectionContainerRole( nodeInfo.role ) ) {
value->vt = VT_BOOL;
value->boolVal = VARIANT_FALSE;
} else if ( propertyId == UIA_ExpandCollapseExpandCollapseStatePropertyId &&
( hasAction( nodeInfo.actions, AccessibilityAction::Expand ) ||
hasAction( nodeInfo.actions, AccessibilityAction::Collapse ) ) ) {
value->vt = VT_I4;
value->lVal = hasState( nodeInfo.states, AccessibilityState::Expanded )
? ExpandCollapseState_Expanded
: ExpandCollapseState_Collapsed;
} else {
return setNotSupported( value );
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE
UIAutomationProvider::get_HostRawElementProvider( IRawElementProviderSimple** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
return mRoot ? UiaHostProviderFromHwnd( mContext->window(), provider ) : S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::Navigate( NavigateDirection direction,
IRawElementProviderFragment** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
NavigationResult navigation;
const HRESULT result = invoke(
navigation,
[this, direction]( AccessibilityManager& manager, NavigationResult& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
if ( direction == NavigateDirection_Parent ) {
output.target = manager.getParent( mRef );
} else if ( direction == NavigateDirection_FirstChild ) {
output.target = manager.getChild( mRef, 0 );
} else if ( direction == NavigateDirection_LastChild ) {
const size_t count = manager.getChildCount( mRef );
if ( count > 0 )
output.target = manager.getChild( mRef, count - 1 );
} else {
const AccessibilityNodeRef parent = manager.getParent( mRef );
if ( !parent.isValid() )
return S_OK;
const size_t count = manager.getChildCount( parent );
const Int32 index = manager.getIndexInParent( mRef );
if ( index >= 0 && direction == NavigateDirection_NextSibling &&
static_cast<size_t>( index + 1 ) < count )
output.target = manager.getChild( parent, index + 1 );
else if ( direction == NavigateDirection_PreviousSibling && index > 0 )
output.target = manager.getChild( parent, index - 1 );
}
return S_OK;
} );
if ( FAILED( result ) )
return result;
if ( navigation.target.isValid() )
*provider =
static_cast<IRawElementProviderFragment*>( mContext->provider( navigation.target ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetRuntimeId( SAFEARRAY** runtimeId ) {
if ( !runtimeId )
return E_INVALIDARG;
*runtimeId = nullptr;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
*runtimeId = safeArrayFromInts( runtimeIdValues( mContext->runtimeScope(), mRef ) );
return *runtimeId ? S_OK : E_OUTOFMEMORY;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_BoundingRectangle( UiaRect* rectangle ) {
if ( !rectangle )
return E_INVALIDARG;
*rectangle = {};
GeometryResult geometry;
const HRESULT result = invoke(
geometry, [this]( AccessibilityManager& manager, GeometryResult& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto nodeInfo = manager.getNodeInfo( mRef, false, false );
output.bounds = nodeInfo.bounds;
output.boundsValid = nodeInfo.boundsValid;
const HWND nativeWindow = mContext->window();
output.windowVisible =
nativeWindow && IsWindowVisible( nativeWindow ) && !IsIconic( nativeWindow );
if ( nativeWindow && !ClientToScreen( nativeWindow, &output.clientOrigin ) )
return HRESULT_FROM_WIN32( GetLastError() );
return S_OK;
} );
if ( FAILED( result ) )
return result;
if ( !geometry.boundsValid || !geometry.windowVisible )
return S_OK;
rectangle->left = geometry.clientOrigin.x + geometry.bounds.Left;
rectangle->top = geometry.clientOrigin.y + geometry.bounds.Top;
rectangle->width = geometry.bounds.getWidth();
rectangle->height = geometry.bounds.getHeight();
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetEmbeddedFragmentRoots( SAFEARRAY** roots ) {
if ( !roots )
return E_INVALIDARG;
*roots = nullptr;
return unavailable() ? UIA_E_ELEMENTNOTAVAILABLE : S_OK;
}
HRESULT STDMETHODCALLTYPE
UIAutomationProvider::get_FragmentRoot( IRawElementProviderFragmentRoot** root ) {
if ( !root )
return E_INVALIDARG;
*root = nullptr;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
UIAutomationProvider* rootProvider = mContext->provider( mContext->rootRef() );
if ( !rootProvider )
return UIA_E_ELEMENTNOTAVAILABLE;
*root = static_cast<IRawElementProviderFragmentRoot*>( rootProvider );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::ElementProviderFromPoint(
double x, double y, IRawElementProviderFragment** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
NavigationResult hit;
const HRESULT result = invoke(
hit, [this, x, y]( AccessibilityManager& manager, NavigationResult& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
POINT clientOrigin{};
const HWND nativeWindow = mContext->window();
if ( !nativeWindow || !ClientToScreen( nativeWindow, &clientOrigin ) )
return UIA_E_ELEMENTNOTAVAILABLE;
output.target =
manager.hitTest( Math::Vector2f( static_cast<Float>( x - clientOrigin.x ),
static_cast<Float>( y - clientOrigin.y ) ) );
return S_OK;
} );
if ( FAILED( result ) )
return result;
if ( hit.target.isValid() )
*provider = static_cast<IRawElementProviderFragment*>( mContext->provider( hit.target ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetFocus( IRawElementProviderFragment** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
AccessibilityNodeRef focused;
const HRESULT result = invoke(
focused, [this]( AccessibilityManager& manager, AccessibilityNodeRef& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
output = manager.getKeyboardFocusedNode();
return S_OK;
} );
if ( FAILED( result ) )
return result;
if ( focused.isValid() )
*provider = static_cast<IRawElementProviderFragment*>( mContext->provider( focused ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::AdviseEventAdded( EVENTID, SAFEARRAY* ) {
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
mContext->adviseEventAdded();
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::AdviseEventRemoved( EVENTID, SAFEARRAY* ) {
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
mContext->adviseEventRemoved();
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_ToggleState( ToggleState* state ) {
if ( !state )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
*state =
hasState( nodeInfo.states, AccessibilityState::Checked ) ? ToggleState_On : ToggleState_Off;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetSelection( SAFEARRAY** selection ) {
if ( !selection )
return E_INVALIDARG;
*selection = nullptr;
AccessibilityNodeInfo nodeInfo;
const HRESULT infoResult = info( nodeInfo, false );
if ( FAILED( infoResult ) )
return infoResult;
if ( nodeInfo.text.valid && !hasState( nodeInfo.states, AccessibilityState::Protected ) ) {
TextSnapshot snapshot;
const HRESULT textResult = textSnapshot( mContext, mRef, snapshot );
if ( FAILED( textResult ) )
return textResult;
SAFEARRAY* array = SafeArrayCreateVector( VT_UNKNOWN, 0, 1 );
if ( !array )
return E_OUTOFMEMORY;
ITextRangeProvider* range = new UIAutomationTextRange(
mContext, mRef, snapshot.selectionStart, snapshot.selectionEnd );
LONG index = 0;
IUnknown* unknown = range;
const HRESULT putResult = SafeArrayPutElement( array, &index, unknown );
range->Release();
if ( FAILED( putResult ) ) {
SafeArrayDestroy( array );
return putResult;
}
*selection = array;
return S_OK;
}
std::vector<AccessibilityNodeRef> selected;
const HRESULT result = invoke( selected,
[this]( AccessibilityManager& manager,
std::vector<AccessibilityNodeRef>& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto containerInfo =
manager.getNodeInfo( mRef, false, false );
if ( !isSelectionContainerRole( containerInfo.role ) )
return UIA_E_INVALIDOPERATION;
output = manager.getSelectedChildren( mRef );
return S_OK;
} );
if ( FAILED( result ) )
return result;
SAFEARRAY* array =
SafeArrayCreateVector( VT_UNKNOWN, 0, static_cast<ULONG>( selected.size() ) );
if ( !array )
return E_OUTOFMEMORY;
for ( LONG index = 0; index < static_cast<LONG>( selected.size() ); ++index ) {
UIAutomationProvider* item = mContext->provider( selected[static_cast<size_t>( index )] );
IUnknown* unknown = static_cast<IRawElementProviderSimple*>( item );
const HRESULT putResult = SafeArrayPutElement( array, &index, unknown );
item->Release();
if ( FAILED( putResult ) ) {
SafeArrayDestroy( array );
return putResult;
}
}
*selection = array;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_CanSelectMultiple( BOOL* canSelectMultiple ) {
if ( !canSelectMultiple )
return E_INVALIDARG;
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
*canSelectMultiple = FALSE;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_IsSelectionRequired( BOOL* selectionRequired ) {
if ( !selectionRequired )
return E_INVALIDARG;
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
*selectionRequired = FALSE;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_IsSelected( BOOL* selected ) {
if ( !selected )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
*selected = hasState( nodeInfo.states, AccessibilityState::Selected ) ? TRUE : FALSE;
return S_OK;
}
HRESULT STDMETHODCALLTYPE
UIAutomationProvider::get_SelectionContainer( IRawElementProviderSimple** provider ) {
if ( !provider )
return E_INVALIDARG;
*provider = nullptr;
AccessibilityNodeRef parent;
const HRESULT result = invoke(
parent, [this]( AccessibilityManager& manager, AccessibilityNodeRef& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
output = manager.getParent( mRef );
return S_OK;
} );
if ( FAILED( result ) )
return result;
if ( parent.isValid() )
*provider = static_cast<IRawElementProviderSimple*>( mContext->provider( parent ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::SetValue( LPCWSTR value ) {
String text( value ? value : L"" );
bool ignored{};
return invoke( ignored,
[this, text = std::move( text )]( AccessibilityManager& manager,
bool& ) mutable -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto nodeInfo = manager.getNodeInfo( mRef, false, false );
if ( !hasState( nodeInfo.states, AccessibilityState::Enabled ) )
return UIA_E_ELEMENTNOTENABLED;
if ( !hasAction( nodeInfo.actions, AccessibilityAction::SetText ) )
return UIA_E_INVALIDOPERATION;
return manager.performAction(
mRef, { AccessibilityAction::SetText, std::move( text ) } )
? S_OK
: E_FAIL;
} );
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_Value( BSTR* value ) {
if ( !value )
return E_INVALIDARG;
*value = nullptr;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo );
if ( FAILED( result ) )
return result;
const auto wide = nodeInfo.value.toWideString();
*value = SysAllocStringLen( wide.data(), static_cast<UINT>( wide.size() ) );
return *value ? S_OK : E_OUTOFMEMORY;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_IsReadOnly( BOOL* readOnly ) {
if ( !readOnly )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
const AccessibilityAction writableAction =
nodeInfo.range.valid ? AccessibilityAction::SetValue : AccessibilityAction::SetText;
*readOnly = hasAction( nodeInfo.actions, writableAction ) ? FALSE : TRUE;
return S_OK;
}
HRESULT STDMETHODCALLTYPE
UIAutomationProvider::get_ExpandCollapseState( ExpandCollapseState* state ) {
if ( !state )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
*state = hasState( nodeInfo.states, AccessibilityState::Expanded )
? ExpandCollapseState_Expanded
: ExpandCollapseState_Collapsed;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::SetValue( double value ) {
if ( !std::isfinite( value ) )
return E_INVALIDARG;
bool ignored{};
return invoke( ignored, [this, value]( AccessibilityManager& manager, bool& ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto nodeInfo = manager.getNodeInfo( mRef, false, false );
if ( !hasState( nodeInfo.states, AccessibilityState::Enabled ) )
return UIA_E_ELEMENTNOTENABLED;
if ( !nodeInfo.range.valid ||
!hasAction( nodeInfo.actions, AccessibilityAction::SetValue ) )
return UIA_E_INVALIDOPERATION;
if ( value < nodeInfo.range.minimum || value > nodeInfo.range.maximum )
return E_INVALIDARG;
return manager.performAction(
mRef, { AccessibilityAction::SetValue, String( String::toString( value ) ) } )
? S_OK
: E_FAIL;
} );
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_Value( double* value ) {
if ( !value )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo );
if ( FAILED( result ) )
return result;
if ( !String::fromString( *value, nodeInfo.value.toUtf8() ) || !std::isfinite( *value ) )
return UIA_E_INVALIDOPERATION;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::GetVisibleRanges( SAFEARRAY** ranges ) {
if ( !ranges )
return E_INVALIDARG;
*ranges = nullptr;
TextSnapshot snapshot;
const HRESULT result = textSnapshot( mContext, mRef, snapshot );
if ( FAILED( result ) )
return result;
SAFEARRAY* array = SafeArrayCreateVector( VT_UNKNOWN, 0, 1 );
if ( !array )
return E_OUTOFMEMORY;
ITextRangeProvider* range =
new UIAutomationTextRange( mContext, mRef, 0, static_cast<int>( snapshot.text.size() ) );
LONG index = 0;
IUnknown* unknown = range;
const HRESULT putResult = SafeArrayPutElement( array, &index, unknown );
range->Release();
if ( FAILED( putResult ) ) {
SafeArrayDestroy( array );
return putResult;
}
*ranges = array;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::RangeFromChild( IRawElementProviderSimple* child,
ITextRangeProvider** range ) {
if ( !child || !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
const HRESULT result = textSnapshot( mContext, mRef, snapshot );
return FAILED( result ) ? result : E_INVALIDARG;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::RangeFromPoint( UiaPoint point,
ITextRangeProvider** range ) {
if ( !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
const HRESULT result = textSnapshot( mContext, mRef, snapshot );
if ( FAILED( result ) )
return result;
POINT origin{};
if ( !snapshot.boundsValid || !ClientToScreen( mContext->window(), &origin ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const double left = origin.x + snapshot.bounds.Left;
const double width = snapshot.bounds.getWidth();
double ratio = width > 0 ? ( point.x - left ) / width : 0;
ratio = std::max( 0.0, std::min( ratio, 1.0 ) );
size_t offset = static_cast<size_t>( ratio * snapshot.text.size() );
if ( offset < snapshot.text.size() && isLowSurrogate( snapshot.text[offset] ) )
--offset;
*range = new UIAutomationTextRange( mContext, mRef, static_cast<int>( offset ),
static_cast<int>( offset ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationProvider::get_DocumentRange( ITextRangeProvider** range ) {
if ( !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
const HRESULT result = textSnapshot( mContext, mRef, snapshot );
if ( FAILED( result ) )
return result;
*range =
new UIAutomationTextRange( mContext, mRef, 0, static_cast<int>( snapshot.text.size() ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE
UIAutomationProvider::get_SupportedTextSelection( SupportedTextSelection* selection ) {
if ( !selection )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
if ( !nodeInfo.text.valid || hasState( nodeInfo.states, AccessibilityState::Protected ) )
return UIA_E_NOTSUPPORTED;
*selection = SupportedTextSelection_Single;
return S_OK;
}
HRESULT UIAutomationProvider::info( AccessibilityNodeInfo& nodeInfo, bool includeValue ) const {
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
return invoke( nodeInfo,
[this, includeValue]( AccessibilityManager& manager,
AccessibilityNodeInfo& output ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
output = manager.getNodeInfo( mRef, includeValue, includeValue );
return S_OK;
} );
}
HRESULT UIAutomationProvider::perform( AccessibilityAction action ) {
if ( unavailable() )
return UIA_E_ELEMENTNOTAVAILABLE;
bool ignored{};
return invoke( ignored, [this, action]( AccessibilityManager& manager, bool& ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto nodeInfo = manager.getNodeInfo( mRef, false, false );
if ( !hasState( nodeInfo.states, AccessibilityState::Enabled ) )
return UIA_E_ELEMENTNOTENABLED;
if ( !hasAction( nodeInfo.actions, action ) )
return UIA_E_INVALIDOPERATION;
if ( action == AccessibilityAction::Focus ) {
const HWND nativeWindow = mContext->window();
if ( !nativeWindow || !IsWindow( nativeWindow ) )
return UIA_E_ELEMENTNOTAVAILABLE;
SetForegroundWindow( nativeWindow );
::SetFocus( nativeWindow );
}
return manager.performAction( mRef, { action, {} } ) ? S_OK : E_FAIL;
} );
}
HRESULT UIAutomationProvider::rangeValue( double* value, double AccessibilityRangeInfo::* member ) {
if ( !value )
return E_INVALIDARG;
AccessibilityNodeInfo nodeInfo;
const HRESULT result = info( nodeInfo, false );
if ( FAILED( result ) )
return result;
if ( !nodeInfo.range.valid )
return UIA_E_INVALIDOPERATION;
*value = nodeInfo.range.*member;
return S_OK;
}
}}} // namespace EE::UI::Uia
#endif
@@ -0,0 +1,558 @@
#include "accessibilitybackenduia.hpp"
#if EE_PLATFORM == EE_PLATFORM_WIN
namespace EE { namespace UI { namespace Uia {
size_t nextSourceCodePoint( const std::u16string& text, size_t index ) {
if ( index >= text.size() )
return text.size();
const char16_t value = text[index++];
if ( value >= 0xd800 && value <= 0xdbff && index < text.size() && text[index] >= 0xdc00 &&
text[index] <= 0xdfff )
++index;
return index;
}
size_t codePointToUTF16( const String& string, Int32 offset ) {
const auto source = string.toUtf16();
const size_t wanted = offset > 0 ? static_cast<size_t>( offset ) : 0;
size_t sourceIndex = 0;
size_t displayIndex = 0;
for ( size_t codePoint = 0; sourceIndex < source.size() && codePoint < wanted; ++codePoint ) {
const size_t next = nextSourceCodePoint( source, sourceIndex );
if ( source[sourceIndex] == u'\n' &&
( sourceIndex == 0 || source[sourceIndex - 1] != u'\r' ) )
++displayIndex;
displayIndex += next - sourceIndex;
sourceIndex = next;
}
return displayIndex;
}
Int32 utf16ToCodePoint( const String& string, size_t offset ) {
const auto source = string.toUtf16();
size_t sourceIndex = 0;
size_t displayIndex = 0;
Int32 codePoints = 0;
while ( sourceIndex < source.size() && displayIndex < offset ) {
const size_t next = nextSourceCodePoint( source, sourceIndex );
size_t displayLength = next - sourceIndex;
if ( source[sourceIndex] == u'\n' &&
( sourceIndex == 0 || source[sourceIndex - 1] != u'\r' ) )
++displayLength;
if ( displayIndex + displayLength > offset )
break;
displayIndex += displayLength;
sourceIndex = next;
++codePoints;
}
return codePoints;
}
std::u16string normalizedUTF16( const String& string ) {
const auto source = string.toUtf16();
std::u16string result;
result.reserve( source.size() );
for ( size_t index = 0; index < source.size(); ++index ) {
if ( source[index] == u'\n' && ( index == 0 || source[index - 1] != u'\r' ) )
result.push_back( u'\r' );
result.push_back( source[index] );
}
return result;
}
HRESULT textSnapshot( const std::shared_ptr<UIAutomationProviderContext>& context,
AccessibilityNodeRef ref, TextSnapshot& snapshot ) {
if ( !context || !context->isAlive() )
return UIA_E_ELEMENTNOTAVAILABLE;
return context->invoke(
snapshot, [=]( AccessibilityManager& manager, TextSnapshot& output ) -> HRESULT {
if ( !manager.isValid( ref ) )
return UIA_E_ELEMENTNOTAVAILABLE;
const auto info = manager.getNodeInfo( ref );
if ( !info.text.valid || hasState( info.states, AccessibilityState::Protected ) )
return UIA_E_INVALIDOPERATION;
output.source = info.value;
output.text = normalizedUTF16( info.value );
output.actions = info.actions;
output.selectionStart = static_cast<int>( codePointToUTF16(
info.value, std::min( info.text.selectionStart, info.text.selectionEnd ) ) );
output.selectionEnd = static_cast<int>( codePointToUTF16(
info.value, std::max( info.text.selectionStart, info.text.selectionEnd ) ) );
output.bounds = info.bounds;
output.boundsValid = info.boundsValid;
return S_OK;
} );
}
bool isLowSurrogate( char16_t value ) {
return value >= 0xdc00 && value <= 0xdfff;
}
bool isHighSurrogate( char16_t value ) {
return value >= 0xd800 && value <= 0xdbff;
}
size_t nextCharacterBoundary( const std::u16string& text, size_t offset ) {
if ( offset >= text.size() )
return text.size();
size_t next = offset + 1;
if ( text[offset] == u'\r' && next < text.size() && text[next] == u'\n' )
++next;
else if ( isHighSurrogate( text[offset] ) && next < text.size() &&
isLowSurrogate( text[next] ) )
++next;
while ( next < text.size() ) {
WORD type{};
const wchar_t value = static_cast<wchar_t>( text[next] );
if ( !GetStringTypeW( CT_CTYPE3, &value, 1, &type ) ||
!( type & ( C3_NONSPACING | C3_DIACRITIC | C3_VOWELMARK ) ) )
break;
++next;
}
return next;
}
size_t previousCharacterBoundary( const std::u16string& text, size_t offset ) {
if ( offset == 0 )
return 0;
size_t previous = std::min( offset, text.size() ) - 1;
if ( isLowSurrogate( text[previous] ) && previous > 0 && isHighSurrogate( text[previous - 1] ) )
--previous;
if ( text[previous] == u'\n' && previous > 0 && text[previous - 1] == u'\r' )
--previous;
while ( previous > 0 ) {
WORD type{};
const wchar_t value = static_cast<wchar_t>( text[previous] );
if ( !GetStringTypeW( CT_CTYPE3, &value, 1, &type ) ||
!( type & ( C3_NONSPACING | C3_DIACRITIC | C3_VOWELMARK ) ) )
break;
previous = previousCharacterBoundary( text, previous );
}
return previous;
}
bool isTextSpace( char16_t value ) {
return value == u'\r' || value == u'\n' || value == u'\t' || value == u' ';
}
std::pair<size_t, size_t> enclosingTextUnit( const std::u16string& text, size_t offset,
TextUnit unit ) {
offset = std::min( offset, text.size() );
if ( unit == TextUnit_Document || unit == TextUnit_Page || unit == TextUnit_Format )
return { 0, text.size() };
if ( unit == TextUnit_Character ) {
if ( text.empty() )
return { 0, 0 };
if ( offset == text.size() )
offset = previousCharacterBoundary( text, offset );
return { offset, nextCharacterBoundary( text, offset ) };
}
if ( unit == TextUnit_Line || unit == TextUnit_Paragraph ) {
size_t start = offset;
while ( start > 0 && text[start - 1] != u'\n' )
--start;
size_t end = offset;
while ( end < text.size() && text[end] != u'\n' )
++end;
if ( end < text.size() )
++end;
return { start, end };
}
if ( text.empty() )
return { 0, 0 };
if ( offset == text.size() )
--offset;
const bool space = isTextSpace( text[offset] );
size_t start = offset;
while ( start > 0 && isTextSpace( text[start - 1] ) == space )
--start;
size_t end = offset;
while ( end < text.size() && isTextSpace( text[end] ) == space )
++end;
return { start, end };
}
size_t moveTextEndpoint( const std::u16string& text, size_t offset, TextUnit unit, int count,
int& moved ) {
moved = 0;
offset = std::min( offset, text.size() );
if ( count == 0 )
return offset;
if ( unit == TextUnit_Document || unit == TextUnit_Page || unit == TextUnit_Format ) {
const size_t target = count > 0 ? text.size() : 0;
if ( target != offset )
moved = count > 0 ? 1 : -1;
return target;
}
const int direction = count > 0 ? 1 : -1;
for ( int step = 0; step < std::abs( count ); ++step ) {
size_t target = offset;
if ( unit == TextUnit_Character )
target = direction > 0 ? nextCharacterBoundary( text, offset )
: previousCharacterBoundary( text, offset );
else if ( direction > 0 ) {
const auto bounds = enclosingTextUnit( text, offset, unit );
target = bounds.second;
} else if ( offset > 0 ) {
const auto bounds = enclosingTextUnit( text, offset - 1, unit );
target = bounds.first;
}
if ( target == offset )
break;
offset = target;
moved += direction;
}
return offset;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::QueryInterface( REFIID interfaceId,
void** object ) {
if ( !object )
return E_INVALIDARG;
*object = nullptr;
if ( interfaceId != __uuidof( IUnknown ) && interfaceId != __uuidof( ITextRangeProvider ) )
return E_NOINTERFACE;
*object = static_cast<ITextRangeProvider*>( this );
AddRef();
return S_OK;
}
ULONG STDMETHODCALLTYPE UIAutomationTextRange::Release() {
const ULONG references = mReferences.fetch_sub( 1, std::memory_order_acq_rel ) - 1;
if ( references == 0 )
delete this;
return references;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::Clone( ITextRangeProvider** range ) {
if ( !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
auto endpoints = normalizedEndpoints( snapshot.text.size() );
*range = new UIAutomationTextRange( mContext, mRef, endpoints.first, endpoints.second );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::Compare( ITextRangeProvider* range, BOOL* same ) {
if ( !range || !same )
return E_INVALIDARG;
*same = FALSE;
auto other = dynamic_cast<UIAutomationTextRange*>( range );
if ( !other || other->mRef != mRef )
return S_OK;
const auto ours = endpoints();
const auto theirs = other->endpoints();
*same = ours == theirs ? TRUE : FALSE;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::CompareEndpoints(
TextPatternRangeEndpoint endpoint, ITextRangeProvider* targetRange,
TextPatternRangeEndpoint targetEndpoint, int* comparison ) {
if ( !targetRange || !comparison )
return E_INVALIDARG;
auto other = dynamic_cast<UIAutomationTextRange*>( targetRange );
if ( !other || other->mRef != mRef )
return E_INVALIDARG;
const auto ours = endpoints();
const auto theirs = other->endpoints();
const int left = endpoint == TextPatternRangeEndpoint_Start ? ours.first : ours.second;
const int right =
targetEndpoint == TextPatternRangeEndpoint_Start ? theirs.first : theirs.second;
*comparison = left < right ? -1 : left > right ? 1 : 0;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::ExpandToEnclosingUnit( TextUnit unit ) {
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
const auto current = normalizedEndpoints( snapshot.text.size() );
const auto expanded = enclosingTextUnit( snapshot.text, current.first, unit );
setEndpoints( static_cast<int>( expanded.first ), static_cast<int>( expanded.second ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::FindAttribute( TEXTATTRIBUTEID, VARIANT, BOOL,
ITextRangeProvider** range ) {
if ( !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
return currentSnapshot( snapshot );
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::FindText( BSTR text, BOOL backward,
BOOL ignoreCase,
ITextRangeProvider** range ) {
if ( !text || !range )
return E_INVALIDARG;
*range = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
const auto current = normalizedEndpoints( snapshot.text.size() );
const size_t rangeStart = static_cast<size_t>( current.first );
const size_t rangeEnd = static_cast<size_t>( current.second );
std::u16string needle( reinterpret_cast<const char16_t*>( text ), SysStringLen( text ) );
if ( needle.empty() )
return S_OK;
auto equal = [=]( char16_t left, char16_t right ) {
return ignoreCase ? std::towlower( static_cast<wchar_t>( left ) ) ==
std::towlower( static_cast<wchar_t>( right ) )
: left == right;
};
size_t found = std::u16string::npos;
if ( backward ) {
for ( size_t candidate = rangeEnd; candidate-- > rangeStart; ) {
if ( candidate + needle.size() <= rangeEnd &&
std::equal( needle.begin(), needle.end(), snapshot.text.begin() + candidate,
equal ) ) {
found = candidate;
break;
}
}
} else {
for ( size_t candidate = rangeStart; candidate + needle.size() <= rangeEnd; ++candidate ) {
if ( std::equal( needle.begin(), needle.end(), snapshot.text.begin() + candidate,
equal ) ) {
found = candidate;
break;
}
}
}
if ( found != std::u16string::npos )
*range = new UIAutomationTextRange( mContext, mRef, static_cast<int>( found ),
static_cast<int>( found + needle.size() ) );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::GetAttributeValue( TEXTATTRIBUTEID,
VARIANT* value ) {
if ( !value )
return E_INVALIDARG;
VariantInit( value );
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
return FAILED( result ) ? result : setNotSupported( value );
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::GetBoundingRectangles( SAFEARRAY** rectangles ) {
if ( !rectangles )
return E_INVALIDARG;
*rectangles = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
const auto current = normalizedEndpoints( snapshot.text.size() );
const bool hasRectangle = current.first != current.second && snapshot.boundsValid &&
IsWindowVisible( mContext->window() ) &&
!IsIconic( mContext->window() );
POINT origin{};
if ( hasRectangle && !ClientToScreen( mContext->window(), &origin ) )
return UIA_E_ELEMENTNOTAVAILABLE;
*rectangles = SafeArrayCreateVector( VT_R8, 0, hasRectangle ? 4 : 0 );
if ( !*rectangles )
return E_OUTOFMEMORY;
if ( !hasRectangle )
return S_OK;
double values[] = { origin.x + snapshot.bounds.Left, origin.y + snapshot.bounds.Top,
snapshot.bounds.getWidth(), snapshot.bounds.getHeight() };
for ( LONG index = 0; index < 4; ++index ) {
const HRESULT result = SafeArrayPutElement( *rectangles, &index, &values[index] );
if ( FAILED( result ) ) {
SafeArrayDestroy( *rectangles );
*rectangles = nullptr;
return result;
}
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::GetText( int maxLength, BSTR* text ) {
if ( !text || maxLength < -1 )
return E_INVALIDARG;
*text = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
const auto current = normalizedEndpoints( snapshot.text.size() );
size_t length = current.second - current.first;
if ( maxLength >= 0 )
length = std::min( length, static_cast<size_t>( maxLength ) );
*text =
SysAllocStringLen( reinterpret_cast<const wchar_t*>( snapshot.text.data() + current.first ),
static_cast<UINT>( length ) );
return *text ? S_OK : E_OUTOFMEMORY;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::Move( TextUnit unit, int count, int* moved ) {
if ( !moved )
return E_INVALIDARG;
*moved = 0;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
const auto current = normalizedEndpoints( snapshot.text.size() );
int actual{};
const size_t start = moveTextEndpoint( snapshot.text, current.first, unit, count, actual );
if ( current.first == current.second )
setEndpoints( static_cast<int>( start ), static_cast<int>( start ) );
else {
const auto expanded = enclosingTextUnit( snapshot.text, start, unit );
setEndpoints( static_cast<int>( expanded.first ), static_cast<int>( expanded.second ) );
}
*moved = actual;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::MoveEndpointByUnit(
TextPatternRangeEndpoint endpoint, TextUnit unit, int count, int* moved ) {
if ( !moved )
return E_INVALIDARG;
*moved = 0;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
auto current = normalizedEndpoints( snapshot.text.size() );
int actual{};
const size_t target = moveTextEndpoint(
snapshot.text, endpoint == TextPatternRangeEndpoint_Start ? current.first : current.second,
unit, count, actual );
if ( endpoint == TextPatternRangeEndpoint_Start ) {
current.first = static_cast<int>( target );
if ( current.first > current.second )
current.second = current.first;
} else {
current.second = static_cast<int>( target );
if ( current.second < current.first )
current.first = current.second;
}
setEndpoints( current.first, current.second );
*moved = actual;
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::MoveEndpointByRange(
TextPatternRangeEndpoint endpoint, ITextRangeProvider* targetRange,
TextPatternRangeEndpoint targetEndpoint ) {
if ( !targetRange )
return E_INVALIDARG;
auto other = dynamic_cast<UIAutomationTextRange*>( targetRange );
if ( !other || other->mRef != mRef )
return E_INVALIDARG;
auto current = endpoints();
const auto target = other->endpoints();
const int position =
targetEndpoint == TextPatternRangeEndpoint_Start ? target.first : target.second;
if ( endpoint == TextPatternRangeEndpoint_Start ) {
current.first = position;
if ( current.first > current.second )
current.second = current.first;
} else {
current.second = position;
if ( current.second < current.first )
current.first = current.second;
}
setEndpoints( current.first, current.second );
return S_OK;
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::Select() {
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
if ( !hasAction( snapshot.actions, AccessibilityAction::SetTextSelection ) )
return UIA_E_INVALIDOPERATION;
const auto current = normalizedEndpoints( snapshot.text.size() );
const Int32 start = utf16ToCodePoint( snapshot.source, current.first );
const Int32 end = utf16ToCodePoint( snapshot.source, current.second );
bool ignored{};
return invoke( ignored, [this, start, end]( AccessibilityManager& manager, bool& ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
return manager.performAction(
mRef, { AccessibilityAction::SetTextSelection,
String( String::toString( start ) + ":" + String::toString( end ) ) } )
? S_OK
: E_FAIL;
} );
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::ScrollIntoView( BOOL ) {
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
if ( !hasAction( snapshot.actions, AccessibilityAction::ScrollTo ) )
return UIA_E_NOTSUPPORTED;
bool ignored{};
return invoke( ignored, [this]( AccessibilityManager& manager, bool& ) -> HRESULT {
if ( !manager.isValid( mRef ) )
return UIA_E_ELEMENTNOTAVAILABLE;
return manager.performAction( mRef, { AccessibilityAction::ScrollTo, {} } ) ? S_OK : E_FAIL;
} );
}
HRESULT STDMETHODCALLTYPE UIAutomationTextRange::GetChildren( SAFEARRAY** children ) {
if ( !children )
return E_INVALIDARG;
*children = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
*children = SafeArrayCreateVector( VT_UNKNOWN, 0, 0 );
return *children ? S_OK : E_OUTOFMEMORY;
}
std::pair<int, int> UIAutomationTextRange::endpoints() const {
std::lock_guard<std::mutex> lock( mMutex );
return { mStart, mEnd };
}
std::pair<int, int> UIAutomationTextRange::normalizedEndpoints( size_t length ) const {
auto result = endpoints();
result.first = std::max( 0, std::min( result.first, static_cast<int>( length ) ) );
result.second = std::max( result.first, std::min( result.second, static_cast<int>( length ) ) );
return result;
}
void UIAutomationTextRange::setEndpoints( int start, int end ) {
std::lock_guard<std::mutex> lock( mMutex );
mStart = start;
mEnd = end;
}
HRESULT STDMETHODCALLTYPE
UIAutomationTextRange::GetEnclosingElement( IRawElementProviderSimple** element ) {
if ( !element )
return E_INVALIDARG;
*element = nullptr;
TextSnapshot snapshot;
const HRESULT result = currentSnapshot( snapshot );
if ( FAILED( result ) )
return result;
UIAutomationProvider* provider = mContext->provider( mRef );
if ( !provider )
return UIA_E_ELEMENTNOTAVAILABLE;
*element = static_cast<IRawElementProviderSimple*>( provider );
return S_OK;
}
}}} // namespace EE::UI::Uia
#endif
+7
View File
@@ -1722,6 +1722,13 @@ void UISceneNode::onSizeChange() {
SceneNode::onSizeChange();
mRoot->setPixelsSize( getRootPixelsSize() );
// Widgets do not report their own bounds changes (clients query bounds on demand). A window
// resize is the one layout change worth announcing, once, on the window's root.
if ( !mHostUISceneNode && hasActiveAccessibilityClients() ) {
if ( auto* manager = getAccessibilityManager() )
manager->notify( manager->getRoot(), AccessibilityEvent::BoundsChanged );
}
}
const Sizef& UISceneNode::getRootPixelsSize() const {
@@ -260,6 +260,27 @@ UTEST( Accessibility, CellEditorIsExposedBelowItsVirtualRow ) {
EXPECT_TRUE( rowChanged );
}
UTEST( Accessibility, SceneResizeAnnouncesOneLayoutChange ) {
UIApplication app( accessibilityTestWindow(), accessibilityTestSettings() );
ASSERT_NE( app.getUI(), nullptr );
auto* scene = app.getUI();
auto* manager = scene->getAccessibilityManager();
auto* button = UIPushButton::New();
button->setParent( scene->getRoot() );
manager->onNativeClientObserved();
manager->clearPendingEvents();
scene->setPixelsSize( scene->getPixelsSize() + Sizef( 16, 16 ) );
// Widgets moved by the relayout stay silent; the window root reports the change once.
size_t bounds = 0;
for ( const auto& event : manager->getPendingEvents() ) {
if ( event.type == AccessibilityEvent::BoundsChanged ) {
EXPECT_TRUE( event.ref == manager->getRoot() );
++bounds;
}
}
EXPECT_EQ( bounds, 1u );
}
UTEST( Accessibility, RepeatedStateAndStructuralEventsKeepTheirOrder ) {
UIApplication app( accessibilityTestWindow(), accessibilityTestSettings() );
ASSERT_NE( app.getUI(), nullptr );