Skip to content

UI Accessibility: expose painted (non-QWidget) items as accessible children via Accessible::Item#278

Open
rezabakhshilaktasaraei wants to merge 27 commits intodesktop-app:masterfrom
rezabakhshilaktasaraei:accessibility/lib_ui
Open

UI Accessibility: expose painted (non-QWidget) items as accessible children via Accessible::Item#278
rezabakhshilaktasaraei wants to merge 27 commits intodesktop-app:masterfrom
rezabakhshilaktasaraei:accessibility/lib_ui

Conversation

@rezabakhshilaktasaraei
Copy link
Contributor

This PR adds accessibility support for painted UI elements that are not real QWidgets (i.e., items drawn inside a widget). The goal is to make these “virtual” elements appear in the accessibility tree so screen readers can discover, navigate, and interact with them.
Summary

Introduces Ui::Accessible::Item (QAccessibleInterface) plus AccessibleItemWrap to represent non-QWidget / non-QObject painted items as accessibility nodes.

Extends RpWidget with a simple API to expose painted items as accessibility children via accessibilityChildInterface(index), along with optional text hooks:

accessibilityChildName(int)

accessibilityChildDescription(int)

accessibilityChildValue(int)

Updates Ui::Accessible::Widget to use the new interface-based child API and support:

correct child lookup (child(index))

hit-testing for painted items (childAt(x, y))

improved focus reporting (focusChild() returns the focused/selected painted item when the widget has focus)

Removes the old widget-only child accessibility methods on RpWidget (accessibilityChildAt, accessibilityIndexOfChild, accessibilityFocusChild) since they don’t fit painted-item scenarios.
Why
A lot of our UI is rendered manually (custom painting) rather than composed from actual Qt widgets. Without explicit QAccessibleInterface objects, those elements are invisible to assistive technologies. This change provides a clean path to expose painted items with proper role, state, geometry, and text, without requiring them to be real widgets.
Notes

Item geometry is mapped to global coordinates for accurate screen reader hit-testing.

Validation ensures the parent widget exists and indices are within bounds when a child count is provided.
Testing

Manual verification with a screen reader:

painted items are discoverable in the accessibility tree

hit-testing works (childAt returns the correct item)

focus/selection is reflected correctly for painted items


const auto index = _item->accessibilityIndex();
if (index < 0) return false;
if (const auto rp = qobject_cast<Ui::RpWidget*>(widget)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (const auto rp = qobject_cast<Ui::RpWidget*>(widget)) {
if (const auto rp = dynamic_cast<Ui::RpWidget*>(widget)) {

if (!_item) return QString();

const auto widget = _item->accessibilityParentWidget();
const auto rp = widget ? qobject_cast<Ui::RpWidget*>(widget) : nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const auto rp = widget ? qobject_cast<Ui::RpWidget*>(widget) : nullptr;
const auto rp = dynamic_cast<Ui::RpWidget*>(widget);

ui/rp_widget.h Outdated
Comment on lines 399 to 404
virtual QAccessibleInterface* accessibilityChildInterface(int index) const {
return nullptr;
}
virtual QString accessibilityChildName(int index) const { return QString(); }
virtual QString accessibilityChildDescription(int index) const { return QString(); }
virtual QString accessibilityChildValue(int index) const { return QString(); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why those are declared in .h unlike others?

rezabakhshilaktasaraei and others added 26 commits February 7, 2026 09:46
- Introduce Ui::Accessible::Item and AccessibleItemWrap to expose non-QWidget / non-QObject items via QAccessibleInterface
- Switch Ui::Accessible::Widget custom child handling to RpWidget::accessibilityChildInterface(index)
- Implement hit-testing via Widget::childAt(x, y) for custom accessibility children
- Improve focusChild() by returning the custom child whose state is focused or selected
- Add RpWidget hooks for child metadata (accessibilityChildName/Description/Value)
- Remove legacy RpWidget child APIs (accessibilityChildAt / accessibilityIndexOfChild / accessibilityFocusChild)
- Update CMakeLists.txt to compile the new accessible item sources
Remove A11y debug LOG statements and add re-entrancy guard for focusChild(),
HasEffectiveFocus helper for focus proxy detection, and accessibilityFocusedChildIndex()
support for faster focused child lookup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive accessibility support for virtual lists with screen readers:

- Implement VirtualListCallbacks struct for list data callbacks
- Add VirtualListFocusProxy widget for keyboard focus management
- Implement VirtualListAccessible for Qt accessibility interface
- Add VirtualListItemAccessible for individual list items
- Support Home/End/Up/Down arrow key navigation
- Add debounced announcements to prevent screen reader flooding
- Use single focus proxy per list with proper event firing
- Support dynamic row positions and heights
- Binary search for childAt() hit testing
- Direct index-to-item mapping for stable accessibility references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert 4-space indentation to tabs
- Add standard license header
- Add braces to single-line if statements
- Fix extra indentation bug on return statement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add optional itemRole callback to VirtualListCallbacks
- Support QAccessible::RadioButton role for radio button lists
- Add checkable/checked state for radio button items
- Announce focus when focus proxy receives focus

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove all state computation from accessible_virtual_list.cpp
- Make VirtualListItemAccessible::state() a simple passthrough
- Make VirtualListFocusProxyAccessible::state() a simple passthrough
- Remove itemRole callback, use widget's accessibilityChildRole() instead
- Add accessibilityChildRole(), accessibilityChildState() to RpWidget
- Move accessibilityChild* methods from inline to declaration + impl

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Keep state handling inside accessible_virtual_list.cpp rather than
delegating to widget classes. This fixes Tab focus not working for
the language list which is deeply nested in the widget hierarchy.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move state logic from accessible_virtual_list.cpp to widget classes.
Now VirtualListItemAccessible and VirtualListFocusProxyAccessible
delegate to widget's accessibilityChildState() for base state and only
compute focused state locally (requires access to internal focus proxy).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…classes

- Remove role(), state(), text() overrides from VirtualListAccessible
- Pass role to QAccessibleWidget constructor from widget->accessibilityRole()
- Remove listName callback from VirtualListCallbacks
- Add readOnly field to AccessibilityState struct

Widget classes now define their own role via accessibilityRole(),
name via setAccessibleName(), and state via accessibilityState().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove rowName callback from VirtualListCallbacks
- Use widget->accessibilityChildName(index) as passthrough instead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ments

Announce screen reader updates immediately without the 50ms delay.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…proxy

Move keyboard navigation responsibility to widget classes.
VirtualListCallbacks now only contains geometry/selection callbacks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify virtual list accessibility by removing the per-item accessible
objects. The focus proxy alone provides sufficient accessibility as it
represents the currently selected item to screen readers.

- Remove VirtualListItemAccessible class (~85 lines)
- Update VirtualListAccessible to only return focus proxy for selected index
- Remove getOrCreateItem() method and _items cache
- Keep VirtualListAccessible for container semantics (childCount, role)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add VirtualListItemProxy class for NVDA Insert+4/6 object navigation.
Created on-demand without caching, ~50 lines vs ~85 in original.

- Focus proxy used for selected index (has focus state)
- Item proxy used for other indices (object navigation)
- Binary search restored in childAt() for hit testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Make ui_accessible_item use RpWidget methods consistently:
- role() now uses rp->accessibilityChildRole()
- state() now uses rp->accessibilityChildState(index) only
- rect() now uses rp->accessibilityChildRect(index)

Remove unused methods from AccessibleItemWrap:
- accessibilityRole()
- accessibilityState()
- accessibilityRect()

Add accessibilityChildRect(int index) to RpWidget for child geometry.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove accessibilityFocusedChildIndex() optimization in favor of
Qt's standard approach - iterate through children checking state().

- Remove accessibilityFocusedChildIndex() from RpWidget
- Remove fast path from Widget::focusChild()
- Use iteration to find focused child (Qt standard pattern)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Screen readers like NVDA need list context (name, item count) when
focusing list items. Previously, focus events were sent on the proxy
widget directly, which didn't provide list container information.

Now send focus events on the parent list with setChild(index), matching
the approach used in dialogs_inner_widget. This allows screen readers to
announce "List name, list, X items" when entering a list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move implementation from inline in header to cpp file, matching
the pattern used by other accessibility methods in RpWidget.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Focus event on parent with setChild() provides list context when
entering a list, but arrow key navigation within the list needs the
NameChanged event on the focus proxy to announce item names.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add parentRp() helper to reduce repeated qobject_cast calls
- Simplify isValid() with early returns
- Use switch statement in text() like ui_accessible_widget
- Consolidate null checks using helper method
- Add comments for clarity (object() returns nullptr for virtual items)
- Remove unused QWidget include
- Reorder methods to match QAccessibleInterface declaration order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Ensure ui_accessible_item and ui_accessible_widget follow consistent
patterns:

- Add section comments (Identity, Content, Children, Navigation, Actions)
- Reorder methods to match logical grouping
- Use braces in switch cases consistently
- Align header declarations with implementation order

Both files now follow the same organizational structure making them
easier to maintain and compare.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ctly

Remove AccessibleItemWrap interface entirely. Item now takes
(RpWidget* parent, int index) directly, like VirtualListItemProxy.

- Remove AccessibleItemWrap class
- Item stores _parent and _index directly
- Add setIndex() for updating index when items reorder
- Rename indexInParent() to index()

This is simpler and matches how VirtualListItemProxy works.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove redundant comments, unnecessary braces from switch cases,
and align section comments with Widget for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add accessibilityChildNameChanged, accessibilityChildDescriptionChanged,
accessibilityChildValueChanged, accessibilityChildStateChanged, and
accessibilityChildFocused to RpWidget for virtual/painted items.

Make selected, focused, and focusable fields in AccessibilityState use
tri-state int (-1 = don't override) so writeTo doesn't clobber Qt's
tracked state when only other fields change.

Fix typo in AccessibilityState comment, add [[nodiscard]] to child
accessor methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All consumers migrated to Ui::Accessible::Item. Remove the VirtualList
infrastructure (focus proxy, registries, factories) and clean up the
accessibility factory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…State

Change selected from tri-state int back to bool so it can be easily
overridden with designated initializers. Keep focused and focusable
as tri-state since they need "don't override" semantics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants