Files
cthulhu/docs/testing/qt-control-states.md

8.7 KiB

Qt control state reporting

Cthulhu preserves AT-SPI checkability independently of the native role. An ordinary menu-item role with CHECKABLE, CHECKED or INDETERMINATE is presented as a check menu item for speech, state sounds and Braille. A checked state-change event also establishes checkability when its new value is false. When a provider omits CHECKABLE, Cthulhu remembers that evidence on the native accessible object, so a subsequent unchecked value still has check-menu semantics. This applies to all applications without matching names or labels. An ordinary menu command without that evidence keeps its existing presentation.

Qt menu actions can change state without sending a checked-state event. Cthulhu refreshes their native cache on focus so reopening a menu reads the current state instead of a previously cached value.

Actions shared between a menu bar and a context menu can keep their original AT-SPI parent. Qt can emit focus-gained for the shared accessible while a state query reports it unfocused because its original menu is hidden. Cthulhu uses that explicit focus event for menu items, including check and radio menu items, before applying the existing foreground checks. The event must name the captured accessible itself; redirected descendants and defunct items do not inherit this evidence. No focus evidence is retained beyond the event.

Checkability evidence belongs to the AT-SPI accessible on the GLib owner thread. No extra accessible references are retained. It ends with that object's native lifetime, is cleared on defunct/name-change events or a changed role, and respects withdrawal of an explicitly exported CHECKABLE state. Replacement actions, including ones with the same label, do not inherit the evidence.

Automated checks

Run the generic Qt 5 (PyQt5) and Qt 6 (PySide6) fixture with:

/usr/bin/python3 tests/native_qt_controls.py

This uses private Xvfb, D-Bus and temporary configuration directories. It checks queued speech and Braille frames through Cthulhu's event loop for menu-bar and context-menu actions, checking and unchecking ordinary checkboxes and checkable buttons, an ordinary menu command, same-label action replacement, and explicit capability removal. It also traverses shared and context-only actions in both directions and after reopening the context menu. It also checks Left and Escape from nested menus, including providers which report the return only by hiding the submenu; see Audacious GUI checks. It requires both Python Qt bindings, xdotool and the native build dependencies. It does not replace the active reader or establish audible speech or physical Braille acceptance.

Existing package checks cover mixed states, state sounds, speech-only and sound-only modes, and repeated focus after checking and unchecking. The existing browser menu test checks ordinary and compound menu navigation:

go test ./internal/atspi ./internal/presentation ./internal/app -count=1
CTHULHU_NATIVE_BROWSER_TESTS=menu /usr/bin/python3 tests/native_browser.py

Editable combos with read-only editors

Some Qt applications make a combo editable, then make its internal line edit read-only. Qt can expose that child with both EDITABLE and READ_ONLY states. Closed Up/Down navigation sends child caret events followed by a name/value event from the combo. A caret event can arrive without a new focus event.

On Slint with Qt/PySide6 6.9.3 and AT-SPI 2.58.1, querying the word at that child's caret reproduced an 800 ms timeout, followed by stale list-item queries and loss of further accessibility events. Cthulhu identifies these children using READ_ONLY plus a direct combo parent, copies that parent's identity into each caret snapshot, and omits the unused word query. It keeps native focus on the combo and lets its name/value event present the selection. This does not depend on the last caret/focus event or on a coalescing delay. Writable combo editors and standalone read-only text fields retain word capture.

The app event loop suppresses a child caret only when its copied combo identity matches the owned focus. Normal foreground checks still apply. Popup selection followed by focus is presented once, and deselection of the previous managed item does not replace the current item. Deselection of the current focused item remains audible.

Run the isolated regression with:

python3 tests/native_toby_combo.py

This requires PySide6, Xvfb, xdotool and the native Go build dependencies. It explicitly uses a 24-bit screen; Slint's 8-bit Xvfb default can crash Qt's text cursor painter before accessibility checks complete. Fixture diagnostics are saved in the printed /tmp/cthulhu-combo-qt-*.log path. The test checks consecutive closed arrows, Home/End, popup Return and Escape, Tab/Shift+Tab after each close, and word capture in writable fields, standalone read-only fields and writable combo editors. It waits for each provider event instead of filling the adapter queue with an unobserved key sequence. Snapshot timings are reported for diagnosis, without a machine-speed pass threshold.

For live acceptance, repeat those keys in Toby Doom Launcher for both IWAD and game choices. Confirm each new value is spoken once, navigation continues after several changes, and Tab/Shift+Tab still reaches narration and buttons. In an ordinary text field and writable combo, check typing and Ctrl+Left/Right word reading. Switch away with a popup open and confirm background events do not interrupt the new foreground application. Check a physical Braille display when available; private event tests do not establish audible or physical output.

Providers which omit checkability

The tested Mumble 1.5.939 / Qt 5 installation exports Mute Self and Deafen Self as ordinary menu items, with CHECKED only while checked and no CHECKABLE state. Its toolbar equivalents use the check-box role. Qt 5's upstream AT-SPI state mapping omits state.checkable, although menu accessibility sets that field internally. Qt 6's mapping exports it.

No toolkit patch is required for Cthulhu to retain observed checkability. The Qt 5 native fixture verifies the sequence unchecked, checked, unchecked for both menu-bar and context-menu actions. After checking establishes the capability, unchecking and reopening announces not checked, with the corresponding Braille indicator. Native checked-state events establish the same capability even when the first observed transition is to false.

The Python reference's generator._generateCheckedStateIfCheckable and Orca's generator._generate_state_checked_if_checkable also treat CHECKED as sufficient evidence even without CHECKABLE. Their menu generators do not assign an unchecked state to an ordinary menu action without evidence.

On first encountering an unchecked menu action with neither the correct role nor CHECKABLE nor a checked-state event, its semantics are unknown. Cthulhu keeps ordinary menu presentation until evidence arrives, rather than announcing every menu command as unchecked. Retained evidence cannot detect an unreported capability removal by a provider that never exports CHECKABLE; object destruction, role changes and name changes still retire it.

Live acceptance

After reviewing the diff, rebuild and restart the intended Cthulhu binary:

  1. In Mumble's Self menu, navigate to Mute Self and Deafen Self. Toggle each, reopen the menu, and confirm both checked and not-checked states once the capability is established. Restore the original settings.
  2. On a server authorized for testing, open another user's context menu and exercise Local Mute in both states. Reopen the menu after each change. Restore the original local mute setting; do not change server mute.
  3. Test the toolbar mute/deafen controls and ordinary settings checkboxes in both states. Confirm Tab and Shift+Tab still reach adjacent controls.
  4. Navigate ordinary Mumble and browser menu commands. Confirm no invented checked state, duplicate announcements, or lost keyboard focus.
  5. Listen to actual speech and check a physical Braille display when available.
  6. Open your own user context menu. Arrow through every enabled item in both directions, including ordinary commands and checkable items, then close and reopen it and repeat. Confirm every stop reads once. Also test the menu-bar equivalents, Escape back to the user list, and switching away from Mumble while the popup is open; background menu events must remain silent.

Do not submit before human review and live testing, as required by the contribution policy.