Bug#1111485: Acknowledgement (orca: Incorrectly determines the active keyboard layout)
Илья Пащук
ilusha.paschuk at gmail.com
Tue Aug 19 13:47:37 BST 2025
New Info:
As I found yesterday, bug is in the at-spi2-core package, not in the
Orca package.
The following was investigated with help of the AI assistent:
Title at-spi2-core Xorg backend uses wrong XKB group for keysym
resolution; Orca speaks characters from first layout instead of active
layout; specials broken in non-zero group
Description (original symptom)
When multiple keyboard layouts are enabled (e.g., English first, Russian
second), and the second layout (Russian) is active:
- The application receives and inserts the correct character (e.g.,
pressing the physical “W” key inserts “ц”).
- However, Orca speaks the character from the first layout: it says “W”
instead of “ц”.
When English (first) layout is active, everything is correct. Switching
the order of layouts changes which symbols Orca speaks, matching the
“first in the list” layout rather than the active one.
Reproduction steps
1. On Debian trixie with Xorg, enable two or more input layouts: English
(first), Russian (second).
2. Switch to the Russian layout.
3. Press the physical “W” key:
- The active application shows “ц” (correct).
- Orca speaks “W” (incorrect).
4. If you change layout order to make Russian the first, Orca speaks
Cyrillic even when English is active — demonstrating that it keys off
the first layout, not the active one.
Observed vs Expected
- Observed: Under Xorg, AT-SPI emits key events where keysym/text
correspond to the first (group 0) layout rather than the active layout,
leading Orca to speak the wrong character.
- Expected: keysym/text in AT-SPI key events should reflect the active
XKB group (current layout).
Environment notes
- Reproduced on Debian trixie, MATE/Xorg and GNOME/Xorg. Not
reproducible on GNOME/Wayland.
- xev confirms Xorg produces Cyrillic keysyms when Russian layout is active:
KeyPress ... keycode 25 (keysym 0x6c3, Cyrillic_tse); XLookupString “ц”
- Orca’s character selection for speech depends on text/keysym received
via AT-SPI; Orca does not determine the layout itself. References:
- [src/orca/input_event.py:534-551]
- [src/orca/input_event_manager.py:261-321]
- [src/orca/scripts/default.py:2025-2053]
(These are context references from Orca’s codebase and included to
clarify Orca’s behavior.)
Root cause analysis (at-spi2-core, Xorg backend)
The Xorg code path in at-spi2-core resolves keysyms using XLookupString
and/or XkbKeycodeToKeysym without reliably using the current XKB group
or providing a fallback for specials:
- at-spi path in the library:
- [at-spi2-core-2.56.2/atspi/atspi-device-x11.c:366-376]:
KeyPress/KeyRelease were using XLookupString(&xevent.xkey, …, &keysym,
…) which does not respect XKB group properly for our
synthesized/use-case events.
- [at-spi2-core-2.56.2/atspi/atspi-device-x11.c:388-406]: XI2
translation xi2keyevent -> XKeyEvent, then XLookupString, same issue.
- registryd (legacy/global listeners path):
-
[at-spi2-core-2.56.2/registryd/deviceeventcontroller-x11.c:732-741]:
spi_keystroke_from_x_key_event() was using XLookupString to populate
keysym and text, ignoring current group.
This results in keysyms from group 0 (first layout) instead of the
active group. After moving to XkbKeycodeToKeysym(group, level),
printable letters became correct, but special keys (BackSpace, arrows,
Home/End, PageUp/Down, etc.) still failed when the active group was
non-zero because they may be only present at level 0 in group 0. A
robust fallback across levels and group 0 is needed.
Proposed fix (AI-assisted; submitter-tested)
Note: The following patch was drafted by an AI assistant and may be
incorrect or suboptimal. Nevertheless, the submitter has built and
installed the patched packages on Debian trixie/Xorg, verifying that:
- With multiple layouts and Russian active, Orca speaks the correct
Cyrillic letters (e.g., “ц”).
- Special keys (BackSpace, arrows, Home/End, PageUp/Down) are recognized
correctly under non-zero group activity.
- Behavior with English (first) layout remains unchanged.
Approach:
- Replace XLookupString-based keysym resolution with:
- XkbGetState(..., &st)
- XkbKeycodeToKeysym(display, keycode, st.group, level)
- Use robust fallback:
1) Try the “natural” level selected by Shift state in the current group.
2) Iterate levels 0..3 in current group.
3) Iterate levels 0..3 in group 0.
- Do not rely on text from XLookupString; leave text empty so consumers
(e.g., Orca) derive a Unicode string from keysym consistently.
Patch contents is pasted below.
Notes
- This patch applies to the Debian-packaged tree post-debian-patches for
2.56.2-1. It should be easy to rebase on top of upstream 2.56.4 (Arch
currently ships 2.56.4 and does not exhibit this issue on Xorg; they may
have functionally equivalent fixes).
- The change minimizes reliance on XLookupString for keysym/text
generation, aligning keysym with the active XKB group and ensuring
specials are found via fallback.
Testing
- Verified on Debian trixie (Xorg) with two layouts (EN first, RU
second). With RU active:
- Application inserts Cyrillic (unchanged).
- Orca now speaks Cyrillic (correct).
- Special keys (BackSpace, arrow keys, Home/End, PageUp/Down)
recognized normally.
- On EN active, behavior remains correct.
AI disclosure
This fix and report were prepared with assistance from an AI system. The
proposed patch may be incorrect or non-optimal. However, the submitter
has built and installed the patched Debian packages locally and verified
that the behavior is corrected as described above.
patch contents:
Index: a/atspi/atspi-device-x11.c
===================================================================
--- a.orig/atspi/atspi-device-x11.c
+++ a/atspi/atspi-device-x11.c
@@ -365,7 +365,30 @@ do_event_dispatch (gpointer user_data)
{
case KeyPress:
case KeyRelease:
- XLookupString (&xevent.xkey, text, sizeof (text), &keysym,
&status);
+ /* Resolve keysym using current XKB group with fallback
across levels and group 0 for specials. */
+ {
+ XkbStateRec st;
+ memset (&st, 0, sizeof (st));
+ XkbGetState (priv->display, XkbUseCoreKbd, &st);
+ /* First try the "natural" level derived from ShiftMask in
the current group. */
+ keysym = XkbKeycodeToKeysym (priv->display,
+ xevent.xkey.keycode,
+ st.group,
+ (xevent.xkey.state &
ShiftMask) ? 1 : 0);
+ if (keysym == NoSymbol || keysym == 0) {
+ /* Fallback: scan levels 0..3 in current group, then
group 0 (handles specials under non-0 group). */
+ int groups_to_try[2] = { st.group, 0 };
+ for (int gi = 0; gi < 2 && (keysym == NoSymbol || keysym
== 0); gi++) {
+ int g = groups_to_try[gi];
+ for (int lvl = 0; lvl < 4; lvl++) {
+ KeySym ks = XkbKeycodeToKeysym (priv->display,
xevent.xkey.keycode, g, lvl);
+ if (ks != NoSymbol && ks != 0) { keysym = ks; break; }
+ }
+ }
+ }
+ /* Let consumers derive text from keysym; avoid stale text
from XLookupString. */
+ text[0] = '\0';
+ }
modifiers = xevent.xkey.state | priv->virtual_mods_enabled;
if (modifiers & priv->numlock_physical_mask)
{
@@ -388,9 +411,27 @@ do_event_dispatch (gpointer user_data)
case XI_KeyPress:
case XI_KeyRelease:
xi2keyevent (xiDevEv, &keyevent);
- XLookupString ((XKeyEvent *) &keyevent, text, sizeof
(text), &keysym, &status);
- if (text[0] < ' ')
+ /* Resolve keysym using current XKB group with
fallback across levels and group 0 (XI2 path). */
+ {
+ XkbStateRec st;
+ memset (&st, 0, sizeof (st));
+ XkbGetState (priv->display, XkbUseCoreKbd, &st);
+ keysym = XkbKeycodeToKeysym (priv->display,
+ xiDevEv->detail,
+ st.group,
+ (keyevent.xkey.state & ShiftMask) ? 1 : 0);
+ if (keysym == NoSymbol || keysym == 0) {
+ int groups_to_try[2] = { st.group, 0 };
+ for (int gi = 0; gi < 2 && (keysym == NoSymbol ||
keysym == 0); gi++) {
+ int g = groups_to_try[gi];
+ for (int lvl = 0; lvl < 4; lvl++) {
+ KeySym ks = XkbKeycodeToKeysym
(priv->display, xiDevEv->detail, g, lvl);
+ if (ks != NoSymbol && ks != 0) { keysym = ks;
break; }
+ }
+ }
+ }
text[0] = '\0';
+ }
set_virtual_modifier (device, xiRawEv->detail,
xevent.xcookie.evtype == XI_KeyPress);
modifiers = keyevent.xkey.state |
priv->virtual_mods_enabled;
if (modifiers & priv->numlock_physical_mask)
Index: a/registryd/deviceeventcontroller-x11.c
===================================================================
--- a.orig/registryd/deviceeventcontroller-x11.c
+++ a/registryd/deviceeventcontroller-x11.c
@@ -729,7 +729,31 @@ spi_keystroke_from_x_key_event (XKeyEven
char cbuf[21];
int nbytes;
- nbytes = XLookupString (x_key_event, cbuf, cbuf_bytes, &keysym, NULL);
+ /* Resolve keysym using current XKB group with robust fallback across
levels and group 0.
+ * This ensures special keys (Backspace, arrows, etc.) are found even
when a non-zero group is active. */
+ {
+ XkbStateRec st;
+ memset (&st, 0, sizeof (st));
+ XkbGetState (spi_get_display (), XkbUseCoreKbd, &st);
+
+ /* First try the "natural" level based on Shift state in the
current group. */
+ keysym = XkbKeycodeToKeysym (spi_get_display (),
+ x_key_event->keycode,
+ st.group,
+ (x_key_event->state & ShiftMask) ? 1 : 0);
+
+ if (keysym == NoSymbol || keysym == 0) {
+ /* Fallback: scan levels 0..3 in current group, then group 0. */
+ int groups_to_try[2] = { st.group, 0 };
+ for (int gi = 0; gi < 2 && (keysym == NoSymbol || keysym == 0);
gi++) {
+ int g = groups_to_try[gi];
+ for (int lvl = 0; lvl < 4; lvl++) {
+ KeySym ks = XkbKeycodeToKeysym (spi_get_display (),
x_key_event->keycode, g, lvl);
+ if (ks != NoSymbol && ks != 0) { keysym = ks; break; }
+ }
+ }
+ }
+ }
key_event.id = (dbus_int32_t) (keysym);
key_event.hw_code = (dbus_int16_t) x_key_event->keycode;
if (((XEvent *) x_key_event)->type == KeyPress)
@@ -820,22 +844,21 @@ spi_keystroke_from_x_key_event (XKeyEven
key_event.event_string = g_strdup ("Right");
break;
default:
- if (nbytes > 0)
- {
- gunichar c;
- cbuf[nbytes] = '\0'; /* OK since length is cbuf_bytes+1 */
- key_event.event_string = g_strdup (cbuf);
- c = keysym2ucs (keysym);
- if (c > 0 && !g_unichar_iscntrl (c))
- {
- key_event.is_text = TRUE;
- /* incorrect for some composed chars? */
- }
- }
- else
- {
- key_event.event_string = g_strdup ("");
- }
+ {
+ gunichar uc = (gunichar) keysym2ucs (keysym);
+ if (uc > 0 && !g_unichar_iscntrl (uc))
+ {
+ char utf8[6];
+ int len = g_unichar_to_utf8 (uc, utf8);
+ utf8[len] = '\0';
+ key_event.event_string = g_strdup (utf8);
+ key_event.is_text = TRUE;
+ }
+ else
+ {
+ key_event.event_string = g_strdup ("");
+ }
+ }
}
key_event.timestamp = (dbus_uint32_t) x_key_event->time;
More information about the Pkg-a11y-devel
mailing list