From 4baa4ae4b1f23cd6ee4d007b7ab71a005bb2b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 9 Oct 2026 01:02:11 -0300 Subject: [PATCH] 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. --- .github/workflows/eepp-linux-build-check.yml | 9 + docs/articles/README.md | 1 + docs/articles/uiaccessibility.md | 2 + docs/articles/uiaccessibilityarchitecture.md | 298 +++ premake4.lua | 5 + premake5.lua | 5 + .../ui/accessibility/accessibilitybackend.hpp | 8 + .../accessibilitybackendatspi.cpp | 2187 +++-------------- .../accessibilitybackendatspi.hpp | 431 ++++ .../accessibilitybackendatspievents.cpp | 289 +++ .../accessibilitybackendatspimethods.cpp | 632 +++++ .../accessibilitybackendatspitree.cpp | 422 ++++ .../accessibilitybackendmacos.mm | 20 +- .../accessibility/accessibilitybackenduia.cpp | 1878 +------------- .../accessibility/accessibilitybackenduia.hpp | 612 +++++ .../accessibilitybackenduiaprovider.cpp | 885 +++++++ .../accessibilitybackenduiatext.cpp | 558 +++++ src/eepp/ui/uiscenenode.cpp | 7 + src/tests/unit_tests/accessibility_tests.cpp | 21 + 19 files changed, 4524 insertions(+), 3746 deletions(-) create mode 100644 docs/articles/uiaccessibilityarchitecture.md create mode 100644 src/eepp/ui/accessibility/accessibilitybackendatspi.hpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackendatspievents.cpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackendatspimethods.cpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackendatspitree.cpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackenduia.hpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackenduiaprovider.cpp create mode 100644 src/eepp/ui/accessibility/accessibilitybackenduiatext.cpp diff --git a/.github/workflows/eepp-linux-build-check.yml b/.github/workflows/eepp-linux-build-check.yml index 8345266a3..7abbf3b26 100644 --- a/.github/workflows/eepp-linux-build-check.yml +++ b/.github/workflows/eepp-linux-build-check.yml @@ -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: | diff --git a/docs/articles/README.md b/docs/articles/README.md index 5c8116fa5..38a5a29fa 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -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 | diff --git a/docs/articles/uiaccessibility.md b/docs/articles/uiaccessibility.md index e3e5b2084..4eaf8ba86 100644 --- a/docs/articles/uiaccessibility.md +++ b/docs/articles/uiaccessibility.md @@ -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 diff --git a/docs/articles/uiaccessibilityarchitecture.md b/docs/articles/uiaccessibilityarchitecture.md new file mode 100644 index 000000000..6b3a91895 --- /dev/null +++ b/docs/articles/uiaccessibilityarchitecture.md @@ -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///`. + +| 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()` 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. diff --git a/premake4.lua b/premake4.lua index b5e0aaf68..844a35b01 100644 --- a/premake4.lua +++ b/premake4.lua @@ -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 diff --git a/premake5.lua b/premake5.lua index 9faed99ae..91024b010 100644 --- a/premake5.lua +++ b/premake5.lua @@ -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 diff --git a/src/eepp/ui/accessibility/accessibilitybackend.hpp b/src/eepp/ui/accessibility/accessibilitybackend.hpp index 1ce1d6d48..88b8f5064 100644 --- a/src/eepp/ui/accessibility/accessibilitybackend.hpp +++ b/src/eepp/ui/accessibility/accessibilitybackend.hpp @@ -8,6 +8,14 @@ namespace EE { namespace UI { class AccessibilityManager; +inline bool hasState( AccessibilityState states, AccessibilityState state ) { + return ( static_cast( states ) & static_cast( state ) ) != 0; +} + +inline bool hasAction( AccessibilityActions actions, AccessibilityAction action ) { + return ( actions & accessibilityActionMask( action ) ) != 0; +} + class AccessibilityBackend { public: virtual ~AccessibilityBackend() = default; diff --git a/src/eepp/ui/accessibility/accessibilitybackendatspi.cpp b/src/eepp/ui/accessibility/accessibilitybackendatspi.cpp index 2dedd9727..30f158576 100644 --- a/src/eepp/ui/accessibility/accessibilitybackendatspi.cpp +++ b/src/eepp/ui/accessibility/accessibilitybackendatspi.cpp @@ -1,1896 +1,343 @@ -#include "accessibilitybackend.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "accessibilitybackendatspi.hpp" #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD +#include #include +#include +#include +#include +#include #include #include #include -namespace EE { namespace UI { -namespace { +namespace EE { namespace UI { namespace AtSpi { -constexpr int DBusMessageTypeError = 3; - -bool hasState( AccessibilityState states, AccessibilityState state ) { - return static_cast( states ) & static_cast( state ); +AtSpiApplication::AtSpiApplication( AccessibilityManager& manager ) : + mManager( &manager ), mPrimaryManager( &manager ) { + registerManager( manager ); + if ( manager.getSceneNode() && manager.getSceneNode()->getWindow() ) + mName = String::fromUtf8( manager.getSceneNode()->getWindow()->getTitle() ); } -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 bool loadSymbol( T& symbol, const char* name ) { - if ( !mHandle ) - return false; - symbol = reinterpret_cast( System::Sys::loadFunction( mHandle, name ) ); - return symbol != nullptr; - } -}; - -class AtSpiApplication final : public std::enable_shared_from_this { - public: - explicit AtSpiApplication( AccessibilityManager& manager ) : - mManager( &manager ), mPrimaryManager( &manager ) { - registerManager( manager ); - if ( manager.getSceneNode() && manager.getSceneNode()->getWindow() ) - mName = String::fromUtf8( manager.getSceneNode()->getWindow()->getTitle() ); - } - - void initialize() { - if ( !mDBus.load() ) - return initializationFinished(); - DBusConnection* sessionBus = mDBus.busGet( 0, nullptr ); - if ( !sessionBus ) - return initializationFinished(); - DBusMessage* request = mDBus.messageNewMethodCall( "org.a11y.Bus", "/org/a11y/bus", - "org.a11y.Bus", "GetAddress" ); - if ( !request ) - return initializationFinished(); - DBusMessage* reply = - mDBus.connectionSendWithReplyAndBlock( sessionBus, request, 1000, nullptr ); - mDBus.messageUnref( request ); - if ( !reply ) - return initializationFinished(); - const char* address = nullptr; - const bool gotAddress = mDBus.messageGetArgs( reply, nullptr, 's', &address, 0 ) != 0; - if ( gotAddress && address ) { - mConnection = mDBus.connectionOpenPrivate( address, nullptr ); - if ( mConnection && !mDBus.busRegister( mConnection, nullptr ) ) { - mDBus.connectionClose( mConnection ); - mDBus.connectionUnref( mConnection ); - mConnection = nullptr; - } - } - mDBus.messageUnref( reply ); - if ( mConnection ) { - mDBus.connectionAddFilter( mConnection, &AtSpiApplication::clientDisconnected, this, - nullptr ); - mDBus.busAddMatch( - mConnection, - "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged'", - nullptr ); - registerApplication(); - if ( mConnection && mDBus.connectionGetUnixFd( mConnection, &mConnectionFd ) && - pipe( mWakePipe ) == 0 ) { - for ( int fd : mWakePipe ) - fcntl( fd, F_SETFL, fcntl( fd, F_GETFL ) | O_NONBLOCK ); - mRunning.store( true, std::memory_order_release ); - mIOThread = std::thread( &AtSpiApplication::waitForMessages, this ); - } - // Socket.Embed emits the registry's application-add event before replying. A client can - // query us immediately, and the blocking Embed call may already have moved that query - // into libdbus's internal dispatch queue before the I/O thread starts polling the - // drained socket. - if ( mConnection ) - mDispatchRequested.store( true, std::memory_order_release ); - } - initializationFinished(); - } - - ~AtSpiApplication() { - if ( mConnection ) { - mRunning.store( false, std::memory_order_release ); - if ( mIOThread.joinable() ) { - const char stop = 1; - const ssize_t written = write( mWakePipe[1], &stop, sizeof( stop ) ); - (void)written; - mIOThread.join(); - close( mWakePipe[0] ); - close( mWakePipe[1] ); - } - mDBus.connectionClose( mConnection ); - mDBus.connectionUnref( mConnection ); - } - } - - Uint64 registerManager( AccessibilityManager& manager ) { - for ( const auto& entry : mManagers ) { - if ( entry.second == &manager ) - return entry.first; - } - const Uint64 id = mNextManagerId++; - mManagers.emplace_back( id, &manager ); - // initialize() may still be constructing the connection on the scene's ThreadPool. - // Acquire its completion flag before reading any of the published D-Bus state. - if ( isAvailable() && hasActiveClients() ) - mPendingWindowAdds.push_back( &manager ); - return id; - } - - void unregisterManager( AccessibilityManager& manager ) { - mPendingWindowAdds.erase( - std::remove( mPendingWindowAdds.begin(), mPendingWindowAdds.end(), &manager ), - mPendingWindowAdds.end() ); - mPendingTexts.erase( std::remove_if( mPendingTexts.begin(), mPendingTexts.end(), - [&manager]( const PendingText& text ) { - return text.manager == &manager; - } ), - mPendingTexts.end() ); - for ( auto it = mManagers.begin(); it != mManagers.end(); ++it ) { - if ( it->second != &manager ) - continue; - const Int32 index = static_cast( it - mManagers.begin() ); - const auto prefix = std::string( NodePathPrefix ) + String::toString( it->first ) + "/"; - for ( auto text = mTextSnapshots.begin(); text != mTextSnapshots.end(); ) { - if ( text->first.compare( 0, prefix.size(), prefix ) == 0 ) - text = mTextSnapshots.erase( text ); - else - ++text; - } - if ( isAvailable() && hasActiveClients() ) - sendWindowChanged( manager, false, index ); - mManagers.erase( it ); - break; - } - if ( mManager == &manager ) - mManager = mManagers.empty() ? nullptr : mManagers.begin()->second; - if ( mPrimaryManager.load( std::memory_order_acquire ) == &manager ) { - auto* primaryManager = mManagers.empty() ? nullptr : mManagers.begin()->second; - mPrimaryManager.store( primaryManager, std::memory_order_release ); - } - } - - 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() { - if ( !mInitialized.load( std::memory_order_acquire ) || !mConnection ) - return; - flushPendingTexts(); - // Signals queued by widget updates must leave the connection even when no client sends - // another query. Otherwise typing notifications wait indefinitely in libdbus's queue. - if ( mOutgoingPending ) { - mDBus.connectionReadWrite( mConnection, 0 ); - mOutgoingPending = mDBus.connectionGetOutgoingSize( mConnection ) > 0; - if ( mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains ) - mDispatchRequested.store( true, std::memory_order_release ); - } - if ( !mPendingWindowAdds.empty() ) { - for ( auto* manager : mPendingWindowAdds ) { - for ( size_t i = 0; i < mManagers.size(); ++i ) { - if ( mManagers[i].second == manager ) { - sendWindowChanged( *manager, true, static_cast( i ) ); - break; - } - } - } - mPendingWindowAdds.clear(); - } - if ( !mDispatchRequested.exchange( false, std::memory_order_acq_rel ) ) - return; - mDBus.connectionReadWrite( mConnection, 0 ); - constexpr size_t MaxMessagesPerUpdate = 256; - for ( size_t i = 0; - i < MaxMessagesPerUpdate && - mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains; - ++i ) - mDBus.connectionDispatch( mConnection ); - mDBus.connectionReadWrite( mConnection, 0 ); - // connectionReadWrite() can move multiple requests from the socket into libdbus. Once the - // socket is drained, poll() will not wake us for requests that remain in libdbus's internal - // dispatch queue. Preserve the bounded per-update work without stranding that backlog. - if ( mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains ) { - mDispatchRequested.store( true, std::memory_order_release ); - Window::Input::wakeUpEventLoop(); - } else { - const char drained = 1; - const ssize_t written = write( mWakePipe[1], &drained, sizeof( drained ) ); - (void)written; - } - } - - void 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 ); - } - - 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 mPrimaryManager{}; - String mName; - std::vector> mManagers; - std::vector mPendingWindowAdds; - Uint64 mNextManagerId{ 1 }; - DBusLibrary mDBus; - DBusConnection* mConnection{}; - int mConnectionFd{ -1 }; - int mWakePipe[2]{ -1, -1 }; - std::atomic mRunning{}; - std::atomic mDispatchRequested{}; - std::atomic mHasActiveClients{}; - std::atomic 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 mTextSnapshots; - UnorderedSet mClients; - - struct PendingText { - AccessibilityManager* manager; - AccessibilityNodeRef ref; - bool valueChanged; - }; - std::vector mPendingTexts; - - /** 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 ) { - 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 hasPendingText( AccessibilityNodeRef ref ) const { - for ( const auto& text : mPendingTexts ) { - if ( text.manager == mManager && text.ref == ref ) - return true; - } - return false; - } - - void 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 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 sendObjectEvent( AccessibilityNodeRef ref, const char* signal, const char* detail, - Int32 offset, Int32 length = 0, 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 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 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 initializationFinished() { mInitialized.store( true, std::memory_order_release ); } - - void waitForMessages() { - bool dispatchPending = false; - while ( mRunning.load( std::memory_order_acquire ) ) { - // A readable socket stays readable until the UI thread drains it. Do not poll it - // again while dispatch is pending: only the drain/stop pipe can re-arm this watcher. - pollfd descriptors[2]{ { dispatchPending ? -1 : mConnectionFd, POLLIN, 0 }, - { mWakePipe[0], POLLIN, 0 } }; - const int ready = poll( descriptors, 2, -1 ); - if ( ready < 0 && errno == EINTR ) - continue; - if ( ready <= 0 || descriptors[0].revents & ( POLLERR | POLLHUP | POLLNVAL ) ) - break; - if ( descriptors[1].revents & POLLIN ) { - char wake[64]; - const ssize_t consumed = read( mWakePipe[0], wake, sizeof( wake ) ); - (void)consumed; - dispatchPending = mDispatchRequested.load( std::memory_order_acquire ); - continue; - } - if ( !( descriptors[0].revents & POLLIN ) ) - continue; - dispatchPending = true; - if ( mDispatchRequested.exchange( true, std::memory_order_acq_rel ) ) - continue; - // UIApplication waits on its first live window, not necessarily the queried scene. - // SDL windows share the event loop, so wake it without retaining an Input pointer that - // the main thread could delete when that window closes. - Window::Input::wakeUpEventLoop(); - } - } - - static DBusHandlerResult handleMessage( DBusConnection*, DBusMessage* message, - void* userData ) { - auto backend = static_cast( userData ); - const char* interface = backend->mDBus.messageGetInterface( message ); - const char* member = backend->mDBus.messageGetMember( message ); - const bool registryPropertySet = - interface && member && - std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 && - std::strcmp( member, "Set" ) == 0 && - std::strcmp( backend->mDBus.messageGetPath( message ), RootPath ) == 0; - if ( !registryPropertySet ) - backend->activate( message ); - if ( backend->handleMessage( message ) != 0 ) { - backend->sendError( message, "org.freedesktop.DBus.Error.UnknownMethod", - "Unsupported accessibility method or property" ); - } - return 0; - } - - void activate( DBusMessage* message ) { - const char* sender = mDBus.messageGetSender( message ); - // Registry bookkeeping is not evidence of an assistive client. Its bus connection - // normally outlives every screen reader and would otherwise pin the active flag forever. - if ( !sender || mRegistryBusName == sender ) - return; - mClients.emplace( sender ); - mHasActiveClients.store( !mClients.empty(), std::memory_order_release ); - } - - static DBusHandlerResult clientDisconnected( DBusConnection*, DBusMessage* message, - void* userData ) { - auto* application = static_cast( userData ); - const char* interface = application->mDBus.messageGetInterface( message ); - const char* member = application->mDBus.messageGetMember( message ); - const char* sender = application->mDBus.messageGetSender( message ); - if ( !interface || !member || std::strcmp( interface, "org.freedesktop.DBus" ) != 0 || - std::strcmp( member, "NameOwnerChanged" ) != 0 || !sender || - std::strcmp( sender, "org.freedesktop.DBus" ) != 0 ) - return 1; - const char* name = nullptr; - const char* oldOwner = nullptr; - const char* newOwner = nullptr; - if ( application->mDBus.messageGetArgs( message, nullptr, 's', &name, 's', &oldOwner, 's', - &newOwner, 0 ) && - name && newOwner && !*newOwner ) { - application->mClients.erase( name ); - const bool active = !application->mClients.empty(); - if ( !active ) { - application->mTextSnapshots.clear(); - application->mPendingTexts.clear(); - } - application->mHasActiveClients.store( active, std::memory_order_release ); - } - return 1; - } - - AccessibilityNodeRef 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( source ), id }; - } - - Uint64 currentManagerId() const { - for ( const auto& entry : mManagers ) { - if ( entry.second == mManager ) - return entry.first; - } - return 0; - } - - 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 { - 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 getParent( AccessibilityNodeRef ref ) { - if ( ref == ApplicationRef ) - return {}; - if ( isSceneRoot( ref ) ) - return ApplicationRef; - return mManager ? mManager->getParent( ref ) : AccessibilityNodeRef{}; - } - - size_t getChildCount( AccessibilityNodeRef ref ) { - if ( ref == ApplicationRef ) - return mManagers.size(); - return mManager ? mManager->getChildCount( ref ) : 0; - } - - AccessibilityNodeRef getChild( AccessibilityNodeRef ref, size_t index ) { - return mManager && ref != ApplicationRef ? mManager->getChild( ref, index ) - : AccessibilityNodeRef{}; - } - - std::string 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 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* 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 actionAt( AccessibilityActions actions, Int32 index ) const { - for ( Uint32 action = 0; action <= static_cast( AccessibilityAction::ScrollTo ); - ++action ) { - if ( actions & ( 1u << action ) ) { - if ( index-- == 0 ) - return static_cast( action ); - } - } - return AccessibilityAction::Focus; - } - - AccessibilityActions nativeActions( AccessibilityActions actions ) const { - return actions & ~( accessibilityActionMask( AccessibilityAction::SetValue ) | - accessibilityActionMask( AccessibilityAction::SetText ) | - accessibilityActionMask( AccessibilityAction::SetTextSelection ) ); - } - - const char* 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 ""; - } - - 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 ) { - 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 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 appendApplicationRef( DBusMessageIter& iter ) { appendRef( iter, ApplicationRef ); } - - void appendEventProperties( DBusMessageIter& iter ) { - DBusMessageIter array; - mDBus.messageIterOpenContainer( &iter, 'a', "{sv}", &array ); - mDBus.messageIterCloseContainer( &iter, &array ); - } - - void 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 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 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( info.states ); - addState( stateBits & static_cast( AccessibilityState::Active ), 1 ); - addState( stateBits & static_cast( AccessibilityState::Checked ), 4 ); - addState( stateBits & static_cast( AccessibilityState::Editable ), 7 ); - addState( stateBits & static_cast( AccessibilityState::Enabled ), 8 ); - addState( info.actions & ( accessibilityActionMask( AccessibilityAction::Expand ) | - accessibilityActionMask( AccessibilityAction::Collapse ) ), - 9 ); - addState( stateBits & static_cast( AccessibilityState::Expanded ), 10 ); - addState( stateBits & static_cast( AccessibilityState::Focusable ), 11 ); - addState( stateBits & static_cast( AccessibilityState::Focused ), 12 ); - addState( stateBits & static_cast( AccessibilityState::Selected ), 23 ); - addState( stateBits & static_cast( AccessibilityState::Enabled ), 24 ); - addState( stateBits & static_cast( AccessibilityState::Showing ), 25 ); - addState( stateBits & static_cast( 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( 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 indexInParent( AccessibilityNodeRef ref ) { - if ( isSceneRoot( ref ) ) { - for ( size_t i = 0; i < mManagers.size(); ++i ) { - if ( mManagers[i].second == mManager ) - return static_cast( i ); - } - return -1; - } - return mManager ? mManager->getIndexInParent( ref ) : -1; - } - - void 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 ); - } - - void send( DBusMessage* reply ) { - if ( !reply ) - return; - mDBus.connectionSend( mConnection, reply, nullptr ); - mDBus.messageUnref( reply ); - if ( !mOutgoingPending ) { - mOutgoingPending = true; - Window::Input::wakeUpEventLoop(); - } - } - - void sendBasic( DBusMessage* request, int type, const void* value ) { - DBusMessage* reply = mDBus.messageNewMethodReturn( request ); - DBusMessageIter iter; - mDBus.messageIterInitAppend( reply, &iter ); - appendBasic( iter, type, value ); - send( reply ); - } - - void sendError( DBusMessage* request, const char* name, const char* message ) { - send( mDBus.messageNewError( request, name, message ) ); - } - - Math::Rectf 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; - } - - void registerApplication() { - mBusName = mDBus.busGetUniqueName( mConnection ); - static const DBusObjectPathVTable vtable{ - nullptr, &AtSpiApplication::handleMessage, nullptr, nullptr, nullptr, nullptr }; - if ( mBusName.empty() || - !mDBus.connectionRegisterObjectPath( mConnection, RootPath, &vtable, this ) || - !mDBus.connectionRegisterObjectPath( mConnection, CachePath, &vtable, this ) || - !mDBus.connectionRegisterFallback( mConnection, "/org/eepp/a11y", &vtable, this ) ) { +void AtSpiApplication::initialize() { + if ( !mDBus.load() ) + return initializationFinished(); + DBusConnection* sessionBus = mDBus.busGet( 0, nullptr ); + if ( !sessionBus ) + return initializationFinished(); + DBusMessage* request = + mDBus.messageNewMethodCall( "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress" ); + if ( !request ) + return initializationFinished(); + DBusMessage* reply = + mDBus.connectionSendWithReplyAndBlock( sessionBus, request, 1000, nullptr ); + mDBus.messageUnref( request ); + if ( !reply ) + return initializationFinished(); + const char* address = nullptr; + const bool gotAddress = mDBus.messageGetArgs( reply, nullptr, 's', &address, 0 ) != 0; + if ( gotAddress && address ) { + mConnection = mDBus.connectionOpenPrivate( address, nullptr ); + if ( mConnection && !mDBus.busRegister( mConnection, nullptr ) ) { mDBus.connectionClose( mConnection ); mDBus.connectionUnref( mConnection ); mConnection = nullptr; - return; } + } + mDBus.messageUnref( reply ); + if ( mConnection ) { + mDBus.connectionAddFilter( mConnection, &AtSpiApplication::clientDisconnected, this, + nullptr ); + mDBus.busAddMatch( + mConnection, "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged'", + nullptr ); + registerApplication(); + if ( mConnection && mDBus.connectionGetUnixFd( mConnection, &mConnectionFd ) && + pipe( mWakePipe ) == 0 ) { + for ( int fd : mWakePipe ) + fcntl( fd, F_SETFL, fcntl( fd, F_GETFL ) | O_NONBLOCK ); + mRunning.store( true, std::memory_order_release ); + mIOThread = std::thread( &AtSpiApplication::waitForMessages, this ); + } + // Socket.Embed emits the registry's application-add event before replying. A client can + // query us immediately, and the blocking Embed call may already have moved that query + // into libdbus's internal dispatch queue before the I/O thread starts polling the + // drained socket. + if ( mConnection ) + mDispatchRequested.store( true, std::memory_order_release ); + } + initializationFinished(); +} - DBusMessage* embed = mDBus.messageNewMethodCall( "org.a11y.atspi.Registry", - "/org/a11y/atspi/accessible/root", - "org.a11y.atspi.Socket", "Embed" ); - if ( !embed ) - return; - DBusMessageIter iter; - mDBus.messageIterInitAppend( embed, &iter ); - appendApplicationRef( iter ); - DBusMessage* reply = - mDBus.connectionSendWithReplyAndBlock( mConnection, embed, 1000, nullptr ); - mDBus.messageUnref( embed ); - if ( reply && mDBus.messageGetType( reply ) != DBusMessageTypeError ) { - DBusMessageIter iter; - DBusMessageIter structure; - if ( mDBus.messageIterInit( reply, &iter ) ) { - mDBus.messageIterRecurse( &iter, &structure ); - const char* bus = nullptr; - const char* path = nullptr; - mDBus.messageIterGetBasic( &structure, &bus ); - if ( mDBus.messageIterNext( &structure ) ) - mDBus.messageIterGetBasic( &structure, &path ); - if ( bus && path ) { - mRegistryBusName = bus; - mRegistryPath = path; +AtSpiApplication::~AtSpiApplication() { + if ( mConnection ) { + mRunning.store( false, std::memory_order_release ); + if ( mIOThread.joinable() ) { + const char stop = 1; + const ssize_t written = write( mWakePipe[1], &stop, sizeof( stop ) ); + (void)written; + mIOThread.join(); + close( mWakePipe[0] ); + close( mWakePipe[1] ); + } + mDBus.connectionClose( mConnection ); + mDBus.connectionUnref( mConnection ); + } +} + +Uint64 AtSpiApplication::registerManager( AccessibilityManager& manager ) { + for ( const auto& entry : mManagers ) { + if ( entry.second == &manager ) + return entry.first; + } + const Uint64 id = mNextManagerId++; + mManagers.emplace_back( id, &manager ); + // initialize() may still be constructing the connection on the scene's ThreadPool. + // Acquire its completion flag before reading any of the published D-Bus state. + if ( isAvailable() && hasActiveClients() ) + mPendingWindowAdds.push_back( &manager ); + return id; +} + +void AtSpiApplication::unregisterManager( AccessibilityManager& manager ) { + mPendingWindowAdds.erase( + std::remove( mPendingWindowAdds.begin(), mPendingWindowAdds.end(), &manager ), + mPendingWindowAdds.end() ); + mPendingTexts.erase( std::remove_if( mPendingTexts.begin(), mPendingTexts.end(), + [&manager]( const PendingText& text ) { + return text.manager == &manager; + } ), + mPendingTexts.end() ); + for ( auto it = mManagers.begin(); it != mManagers.end(); ++it ) { + if ( it->second != &manager ) + continue; + const Int32 index = static_cast( it - mManagers.begin() ); + const auto prefix = std::string( NodePathPrefix ) + String::toString( it->first ) + "/"; + for ( auto text = mTextSnapshots.begin(); text != mTextSnapshots.end(); ) { + if ( text->first.compare( 0, prefix.size(), prefix ) == 0 ) + text = mTextSnapshots.erase( text ); + else + ++text; + } + if ( isAvailable() && hasActiveClients() ) + sendWindowChanged( manager, false, index ); + mManagers.erase( it ); + break; + } + if ( mManager == &manager ) + mManager = mManagers.empty() ? nullptr : mManagers.begin()->second; + if ( mPrimaryManager.load( std::memory_order_acquire ) == &manager ) { + auto* primaryManager = mManagers.empty() ? nullptr : mManagers.begin()->second; + mPrimaryManager.store( primaryManager, std::memory_order_release ); + } +} + +void AtSpiApplication::update() { + if ( !mInitialized.load( std::memory_order_acquire ) || !mConnection ) + return; + flushPendingTexts(); + // Signals queued by widget updates must leave the connection even when no client sends + // another query. Otherwise typing notifications wait indefinitely in libdbus's queue. + if ( mOutgoingPending ) { + mDBus.connectionReadWrite( mConnection, 0 ); + mOutgoingPending = mDBus.connectionGetOutgoingSize( mConnection ) > 0; + if ( mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains ) + mDispatchRequested.store( true, std::memory_order_release ); + } + if ( !mPendingWindowAdds.empty() ) { + for ( auto* manager : mPendingWindowAdds ) { + for ( size_t i = 0; i < mManagers.size(); ++i ) { + if ( mManagers[i].second == manager ) { + sendWindowChanged( *manager, true, static_cast( i ) ); + break; } } - mDBus.messageUnref( reply ); - return; } - if ( reply ) - mDBus.messageUnref( reply ); + mPendingWindowAdds.clear(); + } + if ( !mDispatchRequested.exchange( false, std::memory_order_acq_rel ) ) + return; + mDBus.connectionReadWrite( mConnection, 0 ); + constexpr size_t MaxMessagesPerUpdate = 256; + for ( size_t i = 0; i < MaxMessagesPerUpdate && + mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains; + ++i ) + mDBus.connectionDispatch( mConnection ); + mDBus.connectionReadWrite( mConnection, 0 ); + // connectionReadWrite() can move multiple requests from the socket into libdbus. Once the + // socket is drained, poll() will not wake us for requests that remain in libdbus's internal + // dispatch queue. Preserve the bounded per-update work without stranding that backlog. + if ( mDBus.connectionGetDispatchStatus( mConnection ) == DBusDispatchDataRemains ) { + mDispatchRequested.store( true, std::memory_order_release ); + Window::Input::wakeUpEventLoop(); + } else { + const char drained = 1; + const ssize_t written = write( mWakePipe[1], &drained, sizeof( drained ) ); + (void)written; + } +} + +void AtSpiApplication::waitForMessages() { + bool dispatchPending = false; + while ( mRunning.load( std::memory_order_acquire ) ) { + // A readable socket stays readable until the UI thread drains it. Do not poll it + // again while dispatch is pending: only the drain/stop pipe can re-arm this watcher. + pollfd descriptors[2]{ { dispatchPending ? -1 : mConnectionFd, POLLIN, 0 }, + { mWakePipe[0], POLLIN, 0 } }; + const int ready = poll( descriptors, 2, -1 ); + if ( ready < 0 && errno == EINTR ) + continue; + if ( ready <= 0 || descriptors[0].revents & ( POLLERR | POLLHUP | POLLNVAL ) ) + break; + if ( descriptors[1].revents & POLLIN ) { + char wake[64]; + const ssize_t consumed = read( mWakePipe[0], wake, sizeof( wake ) ); + (void)consumed; + dispatchPending = mDispatchRequested.load( std::memory_order_acquire ); + continue; + } + if ( !( descriptors[0].revents & POLLIN ) ) + continue; + dispatchPending = true; + if ( mDispatchRequested.exchange( true, std::memory_order_acq_rel ) ) + continue; + // UIApplication waits on its first live window, not necessarily the queried scene. + // SDL windows share the event loop, so wake it without retaining an Input pointer that + // the main thread could delete when that window closes. + Window::Input::wakeUpEventLoop(); + } +} + +DBusHandlerResult AtSpiApplication::handleMessage( DBusConnection*, DBusMessage* message, + void* userData ) { + auto backend = static_cast( userData ); + const char* interface = backend->mDBus.messageGetInterface( message ); + const char* member = backend->mDBus.messageGetMember( message ); + const bool registryPropertySet = + interface && member && std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 && + std::strcmp( member, "Set" ) == 0 && + std::strcmp( backend->mDBus.messageGetPath( message ), RootPath ) == 0; + if ( !registryPropertySet ) + backend->activate( message ); + if ( backend->handleMessage( message ) != 0 ) { + backend->sendError( message, "org.freedesktop.DBus.Error.UnknownMethod", + "Unsupported accessibility method or property" ); + } + return 0; +} + +void AtSpiApplication::activate( DBusMessage* message ) { + const char* sender = mDBus.messageGetSender( message ); + // Registry bookkeeping is not evidence of an assistive client. Its bus connection + // normally outlives every screen reader and would otherwise pin the active flag forever. + if ( !sender || mRegistryBusName == sender ) + return; + mClients.emplace( sender ); + mHasActiveClients.store( !mClients.empty(), std::memory_order_release ); +} + +DBusHandlerResult AtSpiApplication::clientDisconnected( DBusConnection*, DBusMessage* message, + void* userData ) { + auto* application = static_cast( userData ); + const char* interface = application->mDBus.messageGetInterface( message ); + const char* member = application->mDBus.messageGetMember( message ); + const char* sender = application->mDBus.messageGetSender( message ); + if ( !interface || !member || std::strcmp( interface, "org.freedesktop.DBus" ) != 0 || + std::strcmp( member, "NameOwnerChanged" ) != 0 || !sender || + std::strcmp( sender, "org.freedesktop.DBus" ) != 0 ) + return 1; + const char* name = nullptr; + const char* oldOwner = nullptr; + const char* newOwner = nullptr; + if ( application->mDBus.messageGetArgs( message, nullptr, 's', &name, 's', &oldOwner, 's', + &newOwner, 0 ) && + name && newOwner && !*newOwner ) { + application->mClients.erase( name ); + const bool active = !application->mClients.empty(); + if ( !active ) { + application->mTextSnapshots.clear(); + application->mPendingTexts.clear(); + } + application->mHasActiveClients.store( active, std::memory_order_release ); + } + return 1; +} + +void AtSpiApplication::send( DBusMessage* reply ) { + if ( !reply ) + return; + mDBus.connectionSend( mConnection, reply, nullptr ); + mDBus.messageUnref( reply ); + if ( !mOutgoingPending ) { + mOutgoingPending = true; + Window::Input::wakeUpEventLoop(); + } +} + +void AtSpiApplication::sendBasic( DBusMessage* request, int type, const void* value ) { + DBusMessage* reply = mDBus.messageNewMethodReturn( request ); + DBusMessageIter iter; + mDBus.messageIterInitAppend( reply, &iter ); + appendBasic( iter, type, value ); + send( reply ); +} + +void AtSpiApplication::registerApplication() { + mBusName = mDBus.busGetUniqueName( mConnection ); + static const DBusObjectPathVTable vtable{ + nullptr, &AtSpiApplication::handleMessage, nullptr, nullptr, nullptr, nullptr }; + if ( mBusName.empty() || + !mDBus.connectionRegisterObjectPath( mConnection, RootPath, &vtable, this ) || + !mDBus.connectionRegisterObjectPath( mConnection, CachePath, &vtable, this ) || + !mDBus.connectionRegisterFallback( mConnection, "/org/eepp/a11y", &vtable, this ) ) { mDBus.connectionClose( mConnection ); mDBus.connectionUnref( mConnection ); mConnection = nullptr; + return; } - DBusHandlerResult handleMessage( DBusMessage* request ) { - const char* path = mDBus.messageGetPath( request ); - const char* interface = mDBus.messageGetInterface( request ); - const char* member = mDBus.messageGetMember( request ); - if ( path && interface && member && std::strcmp( path, CachePath ) == 0 && - 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 ( path && interface && member && std::strcmp( path, CachePath ) == 0 && - 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; - } - auto ref = refFromPath( path ); - if ( !interface || !member || !isValid( ref ) ) - return 1; - - if ( std::strcmp( interface, "org.a11y.atspi.Accessible" ) == 0 ) { - 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( 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; + DBusMessage* embed = + mDBus.messageNewMethodCall( "org.a11y.atspi.Registry", "/org/a11y/atspi/accessible/root", + "org.a11y.atspi.Socket", "Embed" ); + if ( !embed ) + return; + DBusMessageIter iter; + mDBus.messageIterInitAppend( embed, &iter ); + appendApplicationRef( iter ); + DBusMessage* reply = mDBus.connectionSendWithReplyAndBlock( mConnection, embed, 1000, nullptr ); + mDBus.messageUnref( embed ); + if ( reply && mDBus.messageGetType( reply ) != DBusMessageTypeError ) { + DBusMessageIter iter; + DBusMessageIter structure; + if ( mDBus.messageIterInit( reply, &iter ) ) { + mDBus.messageIterRecurse( &iter, &structure ); + const char* bus = nullptr; + const char* path = nullptr; + mDBus.messageIterGetBasic( &structure, &bus ); + if ( mDBus.messageIterNext( &structure ) ) + mDBus.messageIterGetBasic( &structure, &path ); + if ( bus && path ) { + mRegistryBusName = bus; + mRegistryPath = path; } - return 0; } - - if ( std::strcmp( interface, "org.a11y.atspi.Application" ) == 0 ) { - 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; - } - - if ( std::strcmp( interface, "org.a11y.atspi.Component" ) == 0 ) { - 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( bounds.Left ), - static_cast( bounds.Top ), - static_cast( bounds.getWidth() ), - static_cast( 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( parent.bounds.Left ); - y += static_cast( 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( bounds.Left ); - Int32 y = static_cast( 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( info.bounds.getWidth() ); - Int32 height = static_cast( 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; - } - - if ( std::strcmp( interface, "org.a11y.atspi.Action" ) == 0 ) { - auto info = getNodeInfo( ref, false ); - const auto actions = nativeActions( info.actions ); - Int32 count = static_cast( __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; - } - - if ( std::strcmp( interface, "org.a11y.atspi.Text" ) == 0 ) { - auto info = getNodeInfo( ref, true ); - if ( !info.text.valid ) - return 1; - rememberText( ref, info, std::strcmp( member, "GetText" ) == 0 ); - const Int32 characterCount = static_cast( 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; - } - - if ( std::strcmp( interface, "org.a11y.atspi.EditableText" ) == 0 ) { - 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; - } - - 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 ) - 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( __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( 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( 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( 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; - } - if ( std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 && - std::strcmp( member, "Set" ) == 0 && 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; - } - if ( std::strcmp( interface, "org.freedesktop.DBus.Properties" ) == 0 && - std::strcmp( member, "Set" ) == 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; - } - return 1; + mDBus.messageUnref( reply ); + return; } -}; + if ( reply ) + mDBus.messageUnref( reply ); + mDBus.connectionClose( mConnection ); + mDBus.connectionUnref( mConnection ); + mConnection = nullptr; +} + +}}} // namespace EE::UI::AtSpi + +namespace EE { namespace UI { + +namespace { class AtSpiAccessibilityBackend final : public AccessibilityBackend { public: @@ -1900,7 +347,7 @@ class AtSpiAccessibilityBackend final : public AccessibilityBackend { return; mApplication = application().lock(); if ( !mApplication ) { - mApplication = std::make_shared( manager ); + mApplication = std::make_shared( manager ); application() = mApplication; if ( scene->hasThreadPool() ) { scene->getThreadPool()->run( @@ -1937,13 +384,13 @@ class AtSpiAccessibilityBackend final : public AccessibilityBackend { } private: - static std::weak_ptr& application() { - static std::weak_ptr instance; + static std::weak_ptr& application() { + static std::weak_ptr instance; return instance; } AccessibilityManager& mManager; - std::shared_ptr mApplication; + std::shared_ptr mApplication; }; } // namespace diff --git a/src/eepp/ui/accessibility/accessibilitybackendatspi.hpp b/src/eepp/ui/accessibility/accessibilitybackendatspi.hpp new file mode 100644 index 000000000..cd9de16dd --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackendatspi.hpp @@ -0,0 +1,431 @@ +#ifndef EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDATSPI_HPP +#define EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDATSPI_HPP + +#include + +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD + +#include "accessibilitybackend.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * 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 bool loadSymbol( T& symbol, const char* name ) { + if ( !mHandle ) + return false; + symbol = reinterpret_cast( System::Sys::loadFunction( mHandle, name ) ); + return symbol != nullptr; + } +}; + +class AtSpiApplication final : public std::enable_shared_from_this { + 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 mPrimaryManager{}; + String mName; + std::vector> mManagers; + std::vector mPendingWindowAdds; + Uint64 mNextManagerId{ 1 }; + DBusLibrary mDBus; + DBusConnection* mConnection{}; + int mConnectionFd{ -1 }; + int mWakePipe[2]{ -1, -1 }; + std::atomic mRunning{}; + std::atomic mDispatchRequested{}; + std::atomic mHasActiveClients{}; + std::atomic 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 mTextSnapshots; + UnorderedSet mClients; + + struct PendingText { + AccessibilityManager* manager; + AccessibilityNodeRef ref; + bool valueChanged; + }; + std::vector 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 diff --git a/src/eepp/ui/accessibility/accessibilitybackendatspievents.cpp b/src/eepp/ui/accessibility/accessibilitybackendatspievents.cpp new file mode 100644 index 000000000..0379cdf9b --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackendatspievents.cpp @@ -0,0 +1,289 @@ +#include "accessibilitybackendatspi.hpp" + +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD + +#include +#include + +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 diff --git a/src/eepp/ui/accessibility/accessibilitybackendatspimethods.cpp b/src/eepp/ui/accessibility/accessibilitybackendatspimethods.cpp new file mode 100644 index 000000000..e76e149cc --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackendatspimethods.cpp @@ -0,0 +1,632 @@ +#include "accessibilitybackendatspi.hpp" + +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD + +#include +#include +#include +#include + +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( 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( bounds.Left ), static_cast( bounds.Top ), + static_cast( bounds.getWidth() ), + static_cast( 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( parent.bounds.Left ); + y += static_cast( 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( bounds.Left ); + Int32 y = static_cast( 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( info.bounds.getWidth() ); + Int32 height = static_cast( 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( __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( 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( __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( 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( 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( 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 diff --git a/src/eepp/ui/accessibility/accessibilitybackendatspitree.cpp b/src/eepp/ui/accessibility/accessibilitybackendatspitree.cpp new file mode 100644 index 000000000..da774f674 --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackendatspitree.cpp @@ -0,0 +1,422 @@ +#include "accessibilitybackendatspi.hpp" + +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_FREEBSD + +#include +#include +#include +#include + +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( 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( AccessibilityAction::ScrollTo ); + ++action ) { + if ( actions & ( 1u << action ) ) { + if ( index-- == 0 ) + return static_cast( 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( info.states ); + addState( stateBits & static_cast( AccessibilityState::Active ), 1 ); + addState( stateBits & static_cast( AccessibilityState::Checked ), 4 ); + addState( stateBits & static_cast( AccessibilityState::Editable ), 7 ); + addState( stateBits & static_cast( AccessibilityState::Enabled ), 8 ); + addState( info.actions & ( accessibilityActionMask( AccessibilityAction::Expand ) | + accessibilityActionMask( AccessibilityAction::Collapse ) ), + 9 ); + addState( stateBits & static_cast( AccessibilityState::Expanded ), 10 ); + addState( stateBits & static_cast( AccessibilityState::Focusable ), 11 ); + addState( stateBits & static_cast( AccessibilityState::Focused ), 12 ); + addState( stateBits & static_cast( AccessibilityState::Selected ), 23 ); + addState( stateBits & static_cast( AccessibilityState::Enabled ), 24 ); + addState( stateBits & static_cast( AccessibilityState::Showing ), 25 ); + addState( stateBits & static_cast( 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( 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( 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 diff --git a/src/eepp/ui/accessibility/accessibilitybackendmacos.mm b/src/eepp/ui/accessibility/accessibilitybackendmacos.mm index 32a88653d..90ab215b4 100644 --- a/src/eepp/ui/accessibility/accessibilitybackendmacos.mm +++ b/src/eepp/ui/accessibility/accessibilitybackendmacos.mm @@ -42,14 +42,6 @@ static bool isMainThread() { return [NSThread isMainThread]; } -static bool hasState( AccessibilityState states, AccessibilityState state ) { - return ( static_cast( states ) & static_cast( 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 ); } diff --git a/src/eepp/ui/accessibility/accessibilitybackenduia.cpp b/src/eepp/ui/accessibility/accessibilitybackenduia.cpp index bc34879dd..41a842a98 100644 --- a/src/eepp/ui/accessibility/accessibilitybackenduia.cpp +++ b/src/eepp/ui/accessibility/accessibilitybackenduia.cpp @@ -1,1874 +1,8 @@ -#include "accessibilitybackend.hpp" +#include "accessibilitybackenduia.hpp" #if EE_PLATFORM == EE_PLATFORM_WIN -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// clang-format off -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -// clang-format on -#include -#include -#include - -namespace EE { namespace UI { - -namespace { - -using namespace EE::Scene; - -constexpr DWORD ProviderCallTimeoutMilliseconds = 2000; - -UINT accessibilityDispatchMessage() { - static const UINT message = RegisterWindowMessageW( L"eepp.UIAutomation.Dispatch" ); - return message; -} - -class UIAutomationProvider; - -class UIAutomationProviderContext final - : public std::enable_shared_from_this { - private: - class PendingRequest { - public: - virtual ~PendingRequest() = default; - - virtual void execute( AccessibilityManager& manager ) = 0; - - void cancel( HRESULT result ) { - std::lock_guard lock( mMutex ); - if ( mComplete ) - return; - mResult = result; - mComplete = true; - mCondition.notify_all(); - } - - HRESULT wait() { - std::unique_lock 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 lock( mMutex ); - if ( mComplete ) - return false; - return true; - } - - void finish( HRESULT result ) { - std::lock_guard 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 class TypedPendingRequest final : public PendingRequest { - public: - using Callback = std::function; - - 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 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 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 - 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; - auto request = std::make_shared( - typename Request::Callback{ std::forward( callback ) }, owner ); - { - std::lock_guard 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( UIA_E_TIMEOUT ) ) { - request->cancel( result ); - removePendingRequest( request.get() ); - } - return result; - } - - UIAutomationProvider* provider( AccessibilityNodeRef ref ); - - void dispatchPendingRequests() { - std::vector> pending; - { - std::lock_guard 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 lock( mPendingMutex ); - for ( auto iterator = mPendingRequests.begin(); iterator != mPendingRequests.end(); - ++iterator ) { - if ( iterator->get() == request ) { - mPendingRequests.erase( iterator ); - break; - } - } - } - - AccessibilityManager* mManager{}; - std::atomic mWindow{}; - DWORD mUIThread{}; - LONG mRuntimeScope{}; - AccessibilityNodeRef mRootRef; - std::atomic mAlive{ true }; - std::atomic mClientsListening{ false }; - std::atomic mClientObserved{ false }; - std::atomic mAdvisedEventCount{ 0 }; - std::mutex mProviderMutex; - std::unordered_map> - mProviders; - std::vector mInvalidatedProviders; - std::mutex mPendingMutex; - std::vector> mPendingRequests; -}; - -bool hasState( AccessibilityState states, AccessibilityState state ) { - return static_cast( states ) & static_cast( state ); -} - -bool hasAction( AccessibilityActions actions, AccessibilityAction action ) { - return actions & accessibilityActionMask( action ); -} - -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( 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 runtimeIdValues( LONG scope, AccessibilityNodeRef ref ) { - return { UiaAppendRuntimeId, - scope, - static_cast( ref.source & 0xffffffffu ), - static_cast( ( ref.source >> 32u ) & 0xffffffffu ), - static_cast( ref.id & 0xffffffffu ), - static_cast( ( ref.id >> 32u ) & 0xffffffffu ) }; -} - -SAFEARRAY* safeArrayFromInts( const std::vector& values ) { - SAFEARRAY* array = SafeArrayCreateVector( VT_I4, 0, static_cast( values.size() ) ); - if ( !array ) - return nullptr; - for ( LONG index = 0; index < static_cast( values.size() ); ++index ) { - int value = values[static_cast( index )]; - if ( FAILED( SafeArrayPutElement( array, &index, &value ) ) ) { - SafeArrayDestroy( array ); - return nullptr; - } - } - return array; -} - -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 }; -}; - -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( 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& 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( codePointToUTF16( - info.value, std::min( info.text.selectionStart, info.text.selectionEnd ) ) ); - output.selectionEnd = static_cast( 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( 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( 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 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; -} - -class UIAutomationTextRange final : public ITextRangeProvider { - public: - UIAutomationTextRange( std::shared_ptr 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 { - if ( !object ) - return E_INVALIDARG; - *object = nullptr; - if ( interfaceId != __uuidof( IUnknown ) && interfaceId != __uuidof( ITextRangeProvider ) ) - return E_NOINTERFACE; - *object = static_cast( this ); - AddRef(); - return S_OK; - } - - ULONG STDMETHODCALLTYPE AddRef() override { - return mReferences.fetch_add( 1, std::memory_order_relaxed ) + 1; - } - - ULONG STDMETHODCALLTYPE Release() override { - const ULONG references = mReferences.fetch_sub( 1, std::memory_order_acq_rel ) - 1; - if ( references == 0 ) - delete this; - return references; - } - - HRESULT STDMETHODCALLTYPE Clone( ITextRangeProvider** range ) override { - 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 Compare( ITextRangeProvider* range, BOOL* same ) override { - if ( !range || !same ) - return E_INVALIDARG; - *same = FALSE; - auto other = dynamic_cast( 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 CompareEndpoints( TextPatternRangeEndpoint endpoint, - ITextRangeProvider* targetRange, - TextPatternRangeEndpoint targetEndpoint, - int* comparison ) override { - if ( !targetRange || !comparison ) - return E_INVALIDARG; - auto other = dynamic_cast( 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 ExpandToEnclosingUnit( TextUnit unit ) override { - 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( expanded.first ), static_cast( expanded.second ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE FindAttribute( TEXTATTRIBUTEID, VARIANT, BOOL, - ITextRangeProvider** range ) override { - if ( !range ) - return E_INVALIDARG; - *range = nullptr; - TextSnapshot snapshot; - return currentSnapshot( snapshot ); - } - - HRESULT STDMETHODCALLTYPE FindText( BSTR text, BOOL backward, BOOL ignoreCase, - ITextRangeProvider** range ) override { - 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( current.first ); - const size_t rangeEnd = static_cast( current.second ); - std::u16string needle( reinterpret_cast( text ), SysStringLen( text ) ); - if ( needle.empty() ) - return S_OK; - auto equal = [=]( char16_t left, char16_t right ) { - return ignoreCase ? std::towlower( static_cast( left ) ) == - std::towlower( static_cast( 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( found ), - static_cast( found + needle.size() ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetAttributeValue( TEXTATTRIBUTEID, VARIANT* value ) override { - if ( !value ) - return E_INVALIDARG; - VariantInit( value ); - TextSnapshot snapshot; - const HRESULT result = currentSnapshot( snapshot ); - return FAILED( result ) ? result : setNotSupported( value ); - } - - HRESULT STDMETHODCALLTYPE GetBoundingRectangles( SAFEARRAY** rectangles ) override { - 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 GetEnclosingElement( IRawElementProviderSimple** element ) override; - - HRESULT STDMETHODCALLTYPE GetText( int maxLength, BSTR* text ) override { - 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( maxLength ) ); - *text = SysAllocStringLen( - reinterpret_cast( snapshot.text.data() + current.first ), - static_cast( length ) ); - return *text ? S_OK : E_OUTOFMEMORY; - } - - HRESULT STDMETHODCALLTYPE Move( TextUnit unit, int count, int* moved ) override { - 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( start ), static_cast( start ) ); - else { - const auto expanded = enclosingTextUnit( snapshot.text, start, unit ); - setEndpoints( static_cast( expanded.first ), static_cast( expanded.second ) ); - } - *moved = actual; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE MoveEndpointByUnit( TextPatternRangeEndpoint endpoint, TextUnit unit, - int count, int* moved ) override { - 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( target ); - if ( current.first > current.second ) - current.second = current.first; - } else { - current.second = static_cast( target ); - if ( current.second < current.first ) - current.first = current.second; - } - setEndpoints( current.first, current.second ); - *moved = actual; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE - MoveEndpointByRange( TextPatternRangeEndpoint endpoint, ITextRangeProvider* targetRange, - TextPatternRangeEndpoint targetEndpoint ) override { - if ( !targetRange ) - return E_INVALIDARG; - auto other = dynamic_cast( 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 Select() override { - 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 AddToSelection() override { return UIA_E_INVALIDOPERATION; } - - HRESULT STDMETHODCALLTYPE RemoveFromSelection() override { return UIA_E_INVALIDOPERATION; } - - HRESULT STDMETHODCALLTYPE ScrollIntoView( BOOL ) override { - 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 GetChildren( SAFEARRAY** children ) override { - 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; - } - - private: - ~UIAutomationTextRange() = default; - - template HRESULT invoke( T& value, Callback&& callback ) const { - return mContext->invoke( - value, std::forward( callback ), - static_cast( const_cast( this ) ) ); - } - - HRESULT currentSnapshot( TextSnapshot& snapshot ) const { - return textSnapshot( mContext, mRef, snapshot ); - } - - std::pair endpoints() const { - std::lock_guard lock( mMutex ); - return { mStart, mEnd }; - } - - std::pair normalizedEndpoints( size_t length ) const { - auto result = endpoints(); - result.first = std::max( 0, std::min( result.first, static_cast( length ) ) ); - result.second = - std::max( result.first, std::min( result.second, static_cast( length ) ) ); - return result; - } - - void setEndpoints( int start, int end ) { - std::lock_guard lock( mMutex ); - mStart = start; - mEnd = end; - } - - std::atomic mReferences{ 1 }; - std::shared_ptr 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 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 { - if ( !object ) - return E_INVALIDARG; - *object = nullptr; - if ( interfaceId == __uuidof( IUnknown ) || - interfaceId == __uuidof( IRawElementProviderSimple ) ) { - *object = static_cast( this ); - } else if ( interfaceId == __uuidof( IRawElementProviderFragment ) ) { - *object = static_cast( this ); - } else if ( interfaceId == __uuidof( IRawElementProviderFragmentRoot ) && mRoot ) { - *object = static_cast( this ); - } else if ( interfaceId == __uuidof( IRawElementProviderAdviseEvents ) && mRoot ) { - *object = static_cast( 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( this ); - else if ( interfaceId == __uuidof( IToggleProvider ) && - hasAction( nodeInfo.actions, AccessibilityAction::Toggle ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( ISelectionProvider ) && - isSelectionContainerRole( nodeInfo.role ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( ISelectionItemProvider ) && - ( hasAction( nodeInfo.actions, AccessibilityAction::Select ) || - hasState( nodeInfo.states, AccessibilityState::Selected ) ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( IValueProvider ) && - ( hasAction( nodeInfo.actions, AccessibilityAction::SetText ) || - nodeInfo.role == AccessibilityRole::ComboBox ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( IRangeValueProvider ) && nodeInfo.range.valid ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( IExpandCollapseProvider ) && - ( hasAction( nodeInfo.actions, AccessibilityAction::Expand ) || - hasAction( nodeInfo.actions, AccessibilityAction::Collapse ) ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( IScrollItemProvider ) && - hasAction( nodeInfo.actions, AccessibilityAction::ScrollTo ) ) - *object = static_cast( this ); - else if ( interfaceId == __uuidof( ITextProvider ) && nodeInfo.text.valid && - !hasState( nodeInfo.states, AccessibilityState::Protected ) ) - *object = static_cast( this ); - } - if ( !*object ) - return E_NOINTERFACE; - AddRef(); - return S_OK; - } - - ULONG STDMETHODCALLTYPE AddRef() override { - return mReferences.fetch_add( 1, std::memory_order_relaxed ) + 1; - } - - ULONG STDMETHODCALLTYPE Release() override { - const ULONG references = mReferences.fetch_sub( 1, std::memory_order_acq_rel ) - 1; - if ( references == 0 ) - delete this; - return references; - } - - HRESULT STDMETHODCALLTYPE get_ProviderOptions( ProviderOptions* options ) override { - if ( !options ) - return E_INVALIDARG; - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - *options = static_cast( ProviderOptions_ServerSideProvider | - ProviderOptions_ProviderOwnsSetFocus | - ProviderOptions_UseComThreading ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetPatternProvider( PATTERNID patternId, - IUnknown** provider ) override { - if ( !provider ) - return E_INVALIDARG; - *provider = nullptr; - HRESULT result = E_NOINTERFACE; - if ( patternId == UIA_InvokePatternId ) - result = - QueryInterface( __uuidof( IInvokeProvider ), reinterpret_cast( provider ) ); - else if ( patternId == UIA_TogglePatternId ) - result = - QueryInterface( __uuidof( IToggleProvider ), reinterpret_cast( provider ) ); - else if ( patternId == UIA_SelectionPatternId ) - result = QueryInterface( __uuidof( ISelectionProvider ), - reinterpret_cast( provider ) ); - else if ( patternId == UIA_SelectionItemPatternId ) - result = QueryInterface( __uuidof( ISelectionItemProvider ), - reinterpret_cast( provider ) ); - else if ( patternId == UIA_ValuePatternId ) - result = - QueryInterface( __uuidof( IValueProvider ), reinterpret_cast( provider ) ); - else if ( patternId == UIA_RangeValuePatternId ) - result = QueryInterface( __uuidof( IRangeValueProvider ), - reinterpret_cast( provider ) ); - else if ( patternId == UIA_ExpandCollapsePatternId ) - result = QueryInterface( __uuidof( IExpandCollapseProvider ), - reinterpret_cast( provider ) ); - else if ( patternId == UIA_ScrollItemPatternId ) - result = QueryInterface( __uuidof( IScrollItemProvider ), - reinterpret_cast( provider ) ); - else if ( patternId == UIA_TextPatternId ) - result = - QueryInterface( __uuidof( ITextProvider ), reinterpret_cast( provider ) ); - return result == E_NOINTERFACE ? S_OK : result; - } - - HRESULT STDMETHODCALLTYPE GetPropertyValue( PROPERTYID propertyId, VARIANT* value ) override { - 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( GetCurrentProcessId() ); - } else if ( propertyId == UIA_NativeWindowHandlePropertyId && mRoot ) { - value->vt = VT_I4; - value->lVal = static_cast( reinterpret_cast( 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 - get_HostRawElementProvider( IRawElementProviderSimple** provider ) override { - if ( !provider ) - return E_INVALIDARG; - *provider = nullptr; - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - return mRoot ? UiaHostProviderFromHwnd( mContext->window(), provider ) : S_OK; - } - - HRESULT STDMETHODCALLTYPE Navigate( NavigateDirection direction, - IRawElementProviderFragment** provider ) override { - 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( 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( - mContext->provider( navigation.target ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetRuntimeId( SAFEARRAY** runtimeId ) override { - 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 get_BoundingRectangle( UiaRect* rectangle ) override { - 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 GetEmbeddedFragmentRoots( SAFEARRAY** roots ) override { - if ( !roots ) - return E_INVALIDARG; - *roots = nullptr; - return unavailable() ? UIA_E_ELEMENTNOTAVAILABLE : S_OK; - } - - HRESULT STDMETHODCALLTYPE SetFocus() override { return perform( AccessibilityAction::Focus ); } - - HRESULT STDMETHODCALLTYPE get_FragmentRoot( IRawElementProviderFragmentRoot** root ) override { - 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( rootProvider ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE ElementProviderFromPoint( - double x, double y, IRawElementProviderFragment** provider ) override { - 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( x - clientOrigin.x ), - static_cast( y - clientOrigin.y ) ) ); - return S_OK; - } ); - if ( FAILED( result ) ) - return result; - if ( hit.target.isValid() ) - *provider = - static_cast( mContext->provider( hit.target ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetFocus( IRawElementProviderFragment** provider ) override { - 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( mContext->provider( focused ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE AdviseEventAdded( EVENTID, SAFEARRAY* ) override { - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - mContext->adviseEventAdded(); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE AdviseEventRemoved( EVENTID, SAFEARRAY* ) override { - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - mContext->adviseEventRemoved(); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE Invoke() override { return perform( AccessibilityAction::Press ); } - - HRESULT STDMETHODCALLTYPE Toggle() override { return perform( AccessibilityAction::Toggle ); } - - HRESULT STDMETHODCALLTYPE get_ToggleState( ToggleState* state ) override { - 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 GetSelection( SAFEARRAY** selection ) override { - 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 selected; - const HRESULT result = - invoke( selected, - [this]( AccessibilityManager& manager, - std::vector& 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( selected.size() ) ); - if ( !array ) - return E_OUTOFMEMORY; - for ( LONG index = 0; index < static_cast( selected.size() ); ++index ) { - UIAutomationProvider* item = - mContext->provider( selected[static_cast( index )] ); - IUnknown* unknown = static_cast( item ); - const HRESULT putResult = SafeArrayPutElement( array, &index, unknown ); - item->Release(); - if ( FAILED( putResult ) ) { - SafeArrayDestroy( array ); - return putResult; - } - } - *selection = array; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE get_CanSelectMultiple( BOOL* canSelectMultiple ) override { - if ( !canSelectMultiple ) - return E_INVALIDARG; - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - *canSelectMultiple = FALSE; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE get_IsSelectionRequired( BOOL* selectionRequired ) override { - if ( !selectionRequired ) - return E_INVALIDARG; - if ( unavailable() ) - return UIA_E_ELEMENTNOTAVAILABLE; - *selectionRequired = FALSE; - return S_OK; - } - - 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 { - 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 - get_SelectionContainer( IRawElementProviderSimple** provider ) override { - 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( mContext->provider( parent ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE SetValue( LPCWSTR value ) override { - 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 get_Value( BSTR* value ) override { - 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( wide.size() ) ); - return *value ? S_OK : E_OUTOFMEMORY; - } - - HRESULT STDMETHODCALLTYPE get_IsReadOnly( BOOL* readOnly ) override { - 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 Expand() override { return perform( AccessibilityAction::Expand ); } - - HRESULT STDMETHODCALLTYPE Collapse() override { - return perform( AccessibilityAction::Collapse ); - } - - HRESULT STDMETHODCALLTYPE get_ExpandCollapseState( ExpandCollapseState* state ) override { - 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 ScrollIntoView() override { - return perform( AccessibilityAction::ScrollTo ); - } - - HRESULT STDMETHODCALLTYPE SetValue( double value ) override { - 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 get_Value( double* value ) override { - 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 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 { - 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( 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 RangeFromChild( IRawElementProviderSimple* child, - ITextRangeProvider** range ) override { - 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 RangeFromPoint( UiaPoint point, - ITextRangeProvider** range ) override { - 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( ratio * snapshot.text.size() ); - if ( offset < snapshot.text.size() && isLowSurrogate( snapshot.text[offset] ) ) - --offset; - *range = new UIAutomationTextRange( mContext, mRef, static_cast( offset ), - static_cast( offset ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE get_DocumentRange( ITextRangeProvider** range ) override { - 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( snapshot.text.size() ) ); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE - get_SupportedTextSelection( SupportedTextSelection* selection ) override { - 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; - } - - private: - ~UIAutomationProvider() = default; - - template 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 ), - static_cast( const_cast( this ) ) ); - } - - bool unavailable() const { - return mDetached.load( std::memory_order_acquire ) || !mContext || !mContext->isAlive(); - } - - HRESULT info( AccessibilityNodeInfo& nodeInfo, bool includeValue = true ) 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 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 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; - } - - std::atomic mReferences{ 1 }; - std::shared_ptr mContext; - AccessibilityNodeRef mRef; - bool mRoot{ false }; - std::atomic mDetached{ false }; -}; - -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( provider ); - return S_OK; -} +namespace EE { namespace UI { namespace Uia { UIAutomationProvider* UIAutomationProviderContext::provider( AccessibilityNodeRef ref ) { if ( !ref.isValid() || !isAlive() ) @@ -1996,6 +130,14 @@ void UIAutomationProviderContext::detach() { } } +}}} // namespace EE::UI::Uia + +namespace EE { namespace UI { + +namespace { + +using namespace Uia; + class UIAutomationAccessibilityBackend final : public AccessibilityBackend { public: explicit UIAutomationAccessibilityBackend( AccessibilityManager& manager ); diff --git a/src/eepp/ui/accessibility/accessibilitybackenduia.hpp b/src/eepp/ui/accessibility/accessibilitybackenduia.hpp new file mode 100644 index 000000000..793a4caf6 --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackenduia.hpp @@ -0,0 +1,612 @@ +#ifndef EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDUIA_HPP +#define EE_UI_ACCESSIBILITY_ACCESSIBILITYBACKENDUIA_HPP + +#include + +#if EE_PLATFORM == EE_PLATFORM_WIN + +#include "accessibilitybackend.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +// clang-format on +#include +#include +#include + +/** + * 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 { + private: + class PendingRequest { + public: + virtual ~PendingRequest() = default; + + virtual void execute( AccessibilityManager& manager ) = 0; + + void cancel( HRESULT result ) { + std::lock_guard lock( mMutex ); + if ( mComplete ) + return; + mResult = result; + mComplete = true; + mCondition.notify_all(); + } + + HRESULT wait() { + std::unique_lock 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 lock( mMutex ); + if ( mComplete ) + return false; + return true; + } + + void finish( HRESULT result ) { + std::lock_guard 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 class TypedPendingRequest final : public PendingRequest { + public: + using Callback = std::function; + + 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 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 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 + 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; + auto request = std::make_shared( + typename Request::Callback{ std::forward( callback ) }, owner ); + { + std::lock_guard 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( UIA_E_TIMEOUT ) ) { + request->cancel( result ); + removePendingRequest( request.get() ); + } + return result; + } + + UIAutomationProvider* provider( AccessibilityNodeRef ref ); + + void dispatchPendingRequests() { + std::vector> pending; + { + std::lock_guard 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 lock( mPendingMutex ); + for ( auto iterator = mPendingRequests.begin(); iterator != mPendingRequests.end(); + ++iterator ) { + if ( iterator->get() == request ) { + mPendingRequests.erase( iterator ); + break; + } + } + } + + AccessibilityManager* mManager{}; + std::atomic mWindow{}; + DWORD mUIThread{}; + LONG mRuntimeScope{}; + AccessibilityNodeRef mRootRef; + std::atomic mAlive{ true }; + std::atomic mClientsListening{ false }; + std::atomic mClientObserved{ false }; + std::atomic mAdvisedEventCount{ 0 }; + std::mutex mProviderMutex; + std::unordered_map> + mProviders; + std::vector mInvalidatedProviders; + std::mutex mPendingMutex; + std::vector> 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 runtimeIdValues( LONG scope, AccessibilityNodeRef ref ); + +SAFEARRAY* safeArrayFromInts( const std::vector& 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& 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 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 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 HRESULT invoke( T& value, Callback&& callback ) const { + return mContext->invoke( + value, std::forward( callback ), + static_cast( const_cast( this ) ) ); + } + + HRESULT currentSnapshot( TextSnapshot& snapshot ) const { + return textSnapshot( mContext, mRef, snapshot ); + } + + std::pair endpoints() const; + + std::pair normalizedEndpoints( size_t length ) const; + + void setEndpoints( int start, int end ); + + std::atomic mReferences{ 1 }; + std::shared_ptr 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 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 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 ), + static_cast( const_cast( 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 mReferences{ 1 }; + std::shared_ptr mContext; + AccessibilityNodeRef mRef; + bool mRoot{ false }; + std::atomic mDetached{ false }; +}; + +}}} // namespace EE::UI::Uia + +#endif + +#endif diff --git a/src/eepp/ui/accessibility/accessibilitybackenduiaprovider.cpp b/src/eepp/ui/accessibility/accessibilitybackenduiaprovider.cpp new file mode 100644 index 000000000..eec2102bf --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackenduiaprovider.cpp @@ -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( 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 runtimeIdValues( LONG scope, AccessibilityNodeRef ref ) { + return { UiaAppendRuntimeId, + scope, + static_cast( ref.source & 0xffffffffu ), + static_cast( ( ref.source >> 32u ) & 0xffffffffu ), + static_cast( ref.id & 0xffffffffu ), + static_cast( ( ref.id >> 32u ) & 0xffffffffu ) }; +} + +SAFEARRAY* safeArrayFromInts( const std::vector& values ) { + SAFEARRAY* array = SafeArrayCreateVector( VT_I4, 0, static_cast( values.size() ) ); + if ( !array ) + return nullptr; + for ( LONG index = 0; index < static_cast( values.size() ); ++index ) { + int value = values[static_cast( 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( this ); + } else if ( interfaceId == __uuidof( IRawElementProviderFragment ) ) { + *object = static_cast( this ); + } else if ( interfaceId == __uuidof( IRawElementProviderFragmentRoot ) && mRoot ) { + *object = static_cast( this ); + } else if ( interfaceId == __uuidof( IRawElementProviderAdviseEvents ) && mRoot ) { + *object = static_cast( 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( this ); + else if ( interfaceId == __uuidof( IToggleProvider ) && + hasAction( nodeInfo.actions, AccessibilityAction::Toggle ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( ISelectionProvider ) && + isSelectionContainerRole( nodeInfo.role ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( ISelectionItemProvider ) && + ( hasAction( nodeInfo.actions, AccessibilityAction::Select ) || + hasState( nodeInfo.states, AccessibilityState::Selected ) ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( IValueProvider ) && + ( hasAction( nodeInfo.actions, AccessibilityAction::SetText ) || + nodeInfo.role == AccessibilityRole::ComboBox ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( IRangeValueProvider ) && nodeInfo.range.valid ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( IExpandCollapseProvider ) && + ( hasAction( nodeInfo.actions, AccessibilityAction::Expand ) || + hasAction( nodeInfo.actions, AccessibilityAction::Collapse ) ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( IScrollItemProvider ) && + hasAction( nodeInfo.actions, AccessibilityAction::ScrollTo ) ) + *object = static_cast( this ); + else if ( interfaceId == __uuidof( ITextProvider ) && nodeInfo.text.valid && + !hasState( nodeInfo.states, AccessibilityState::Protected ) ) + *object = static_cast( 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_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( provider ) ); + else if ( patternId == UIA_TogglePatternId ) + result = + QueryInterface( __uuidof( IToggleProvider ), reinterpret_cast( provider ) ); + else if ( patternId == UIA_SelectionPatternId ) + result = + QueryInterface( __uuidof( ISelectionProvider ), reinterpret_cast( provider ) ); + else if ( patternId == UIA_SelectionItemPatternId ) + result = QueryInterface( __uuidof( ISelectionItemProvider ), + reinterpret_cast( provider ) ); + else if ( patternId == UIA_ValuePatternId ) + result = QueryInterface( __uuidof( IValueProvider ), reinterpret_cast( provider ) ); + else if ( patternId == UIA_RangeValuePatternId ) + result = + QueryInterface( __uuidof( IRangeValueProvider ), reinterpret_cast( provider ) ); + else if ( patternId == UIA_ExpandCollapsePatternId ) + result = QueryInterface( __uuidof( IExpandCollapseProvider ), + reinterpret_cast( provider ) ); + else if ( patternId == UIA_ScrollItemPatternId ) + result = + QueryInterface( __uuidof( IScrollItemProvider ), reinterpret_cast( provider ) ); + else if ( patternId == UIA_TextPatternId ) + result = QueryInterface( __uuidof( ITextProvider ), reinterpret_cast( 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( GetCurrentProcessId() ); + } else if ( propertyId == UIA_NativeWindowHandlePropertyId && mRoot ) { + value->vt = VT_I4; + value->lVal = static_cast( reinterpret_cast( 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( 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( 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( 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( x - clientOrigin.x ), + static_cast( y - clientOrigin.y ) ) ); + return S_OK; + } ); + if ( FAILED( result ) ) + return result; + if ( hit.target.isValid() ) + *provider = static_cast( 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( 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 selected; + const HRESULT result = invoke( selected, + [this]( AccessibilityManager& manager, + std::vector& 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( selected.size() ) ); + if ( !array ) + return E_OUTOFMEMORY; + for ( LONG index = 0; index < static_cast( selected.size() ); ++index ) { + UIAutomationProvider* item = mContext->provider( selected[static_cast( index )] ); + IUnknown* unknown = static_cast( 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( 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( 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( 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( ratio * snapshot.text.size() ); + if ( offset < snapshot.text.size() && isLowSurrogate( snapshot.text[offset] ) ) + --offset; + *range = new UIAutomationTextRange( mContext, mRef, static_cast( offset ), + static_cast( 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( 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 diff --git a/src/eepp/ui/accessibility/accessibilitybackenduiatext.cpp b/src/eepp/ui/accessibility/accessibilitybackenduiatext.cpp new file mode 100644 index 000000000..a97833a09 --- /dev/null +++ b/src/eepp/ui/accessibility/accessibilitybackenduiatext.cpp @@ -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( 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& 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( codePointToUTF16( + info.value, std::min( info.text.selectionStart, info.text.selectionEnd ) ) ); + output.selectionEnd = static_cast( 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( 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( 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 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( 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( 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( 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( expanded.first ), static_cast( 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( current.first ); + const size_t rangeEnd = static_cast( current.second ); + std::u16string needle( reinterpret_cast( text ), SysStringLen( text ) ); + if ( needle.empty() ) + return S_OK; + auto equal = [=]( char16_t left, char16_t right ) { + return ignoreCase ? std::towlower( static_cast( left ) ) == + std::towlower( static_cast( 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( found ), + static_cast( 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( maxLength ) ); + *text = + SysAllocStringLen( reinterpret_cast( snapshot.text.data() + current.first ), + static_cast( 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( start ), static_cast( start ) ); + else { + const auto expanded = enclosingTextUnit( snapshot.text, start, unit ); + setEndpoints( static_cast( expanded.first ), static_cast( 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( target ); + if ( current.first > current.second ) + current.second = current.first; + } else { + current.second = static_cast( 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( 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 UIAutomationTextRange::endpoints() const { + std::lock_guard lock( mMutex ); + return { mStart, mEnd }; +} + +std::pair UIAutomationTextRange::normalizedEndpoints( size_t length ) const { + auto result = endpoints(); + result.first = std::max( 0, std::min( result.first, static_cast( length ) ) ); + result.second = std::max( result.first, std::min( result.second, static_cast( length ) ) ); + return result; +} + +void UIAutomationTextRange::setEndpoints( int start, int end ) { + std::lock_guard 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( provider ); + return S_OK; +} + +}}} // namespace EE::UI::Uia + +#endif diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index f3e10df69..b82df93f9 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -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 { diff --git a/src/tests/unit_tests/accessibility_tests.cpp b/src/tests/unit_tests/accessibility_tests.cpp index 906eaa133..87382c744 100644 --- a/src/tests/unit_tests/accessibility_tests.cpp +++ b/src/tests/unit_tests/accessibility_tests.cpp @@ -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 );