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