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