SDL: x11: Add keymap support

From 974bbea20b7a87f792f7c10c26b011c0907a05fa Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 22 Jun 2024 19:09:50 -0400
Subject: [PATCH] x11: Add keymap support

XkbKeycodeToKeySym is replaced with XkbLookupKeySym, which can take the modifier states. The associated cmake check has been renamed for consistency.

Only the XKB path is currently handled. The deprecated XKeycodeToKeysym path is TODO.
---
 cmake/sdlchecks.cmake                         |   2 +-
 include/build_config/SDL_build_config.h.cmake |   2 +-
 include/build_config/SDL_build_config_macos.h |   2 +-
 src/video/x11/SDL_x11dyn.h                    |   2 +-
 src/video/x11/SDL_x11events.c                 |   1 +
 src/video/x11/SDL_x11keyboard.c               | 138 ++++++++++--------
 src/video/x11/SDL_x11keyboard.h               |   2 +-
 src/video/x11/SDL_x11sym.h                    |   6 +-
 src/video/x11/SDL_x11video.h                  |   2 +-
 9 files changed, 86 insertions(+), 71 deletions(-)

diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake
index 85816a2c52f0f..aecb98fe5394e 100644
--- a/cmake/sdlchecks.cmake
+++ b/cmake/sdlchecks.cmake
@@ -371,7 +371,7 @@ macro(CheckX11)
         set(SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS 1)
       endif()
 
-      check_symbol_exists(XkbKeycodeToKeysym "X11/Xlib.h;X11/XKBlib.h" SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM)
+      check_symbol_exists(XkbLookupKeySym "X11/Xlib.h;X11/XKBlib.h" SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM)
 
       if(SDL_X11_XCURSOR AND HAVE_XCURSOR_H AND XCURSOR_LIB)
         set(HAVE_X11_XCURSOR TRUE)
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 348856a211ccf..e12b96b36df6c 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -400,7 +400,7 @@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 @SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR @SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR@
 #cmakedefine SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS @SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS@
-#cmakedefine SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM @SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM@
+#cmakedefine SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM @SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM@
 #cmakedefine SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS @SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS@
 #cmakedefine SDL_VIDEO_DRIVER_X11_XCURSOR @SDL_VIDEO_DRIVER_X11_XCURSOR@
 #cmakedefine SDL_VIDEO_DRIVER_X11_XDBE @SDL_VIDEO_DRIVER_X11_XDBE@
diff --git a/include/build_config/SDL_build_config_macos.h b/include/build_config/SDL_build_config_macos.h
index 02cd3ae4c3292..597cf8874579a 100644
--- a/include/build_config/SDL_build_config_macos.h
+++ b/include/build_config/SDL_build_config_macos.h
@@ -194,7 +194,7 @@
 #define SDL_VIDEO_DRIVER_X11_XRANDR 1
 #define SDL_VIDEO_DRIVER_X11_XSCRNSAVER 1
 #define SDL_VIDEO_DRIVER_X11_XSHAPE 1
-#define SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM 1
+#define SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM 1
 
 #ifdef MAC_OS_X_VERSION_10_8
 /*
diff --git a/src/video/x11/SDL_x11dyn.h b/src/video/x11/SDL_x11dyn.h
index 73d8953e8fa36..b045d12cbc2ef 100644
--- a/src/video/x11/SDL_x11dyn.h
+++ b/src/video/x11/SDL_x11dyn.h
@@ -28,7 +28,7 @@
 #include <X11/Xatom.h>
 #include <X11/Xresource.h>
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
 #include <X11/XKBlib.h>
 #endif
 
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 08dddeb33e0e1..043db8de53a34 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -1131,6 +1131,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
             printf("window %p: KeymapNotify!\n", data);
 #endif
             if (SDL_GetKeyboardFocus() != NULL) {
+                X11_UpdateKeymap(_this, SDL_TRUE);
                 X11_ReconcileKeyboardState(_this);
             }
         } else if (xevent->type == MappingNotify) {
diff --git a/src/video/x11/SDL_x11keyboard.c b/src/video/x11/SDL_x11keyboard.c
index b4d8d88a8a1fe..543c36e4a866b 100644
--- a/src/video/x11/SDL_x11keyboard.c
+++ b/src/video/x11/SDL_x11keyboard.c
@@ -72,7 +72,7 @@ static SDL_bool X11_ScancodeIsRemappable(SDL_Scancode scancode)
 /* This function only correctly maps letters and numbers for keyboards in US QWERTY layout */
 static SDL_Scancode X11_KeyCodeToSDLScancode(SDL_VideoDevice *_this, KeyCode keycode)
 {
-    const KeySym keysym = X11_KeyCodeToSym(_this, keycode, 0);
+    const KeySym keysym = X11_KeyCodeToSym(_this, keycode, 0, 0);
 
     if (keysym == NoSymbol) {
         return SDL_SCANCODE_UNKNOWN;
@@ -81,24 +81,15 @@ static SDL_Scancode X11_KeyCodeToSDLScancode(SDL_VideoDevice *_this, KeyCode key
     return SDL_GetScancodeFromKeySym(keysym, keycode);
 }
 
-static Uint32 X11_KeyCodeToUcs4(SDL_VideoDevice *_this, KeyCode keycode, unsigned char group)
-{
-    KeySym keysym = X11_KeyCodeToSym(_this, keycode, group);
-
-    if (keysym == NoSymbol) {
-        return 0;
-    }
-
-    return SDL_KeySymToUcs4(keysym);
-}
-
-KeySym
-X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode keycode, unsigned char group)
+KeySym X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode keycode, unsigned char group, unsigned int mod_mask)
 {
     SDL_VideoData *data = _this->driverdata;
     KeySym keysym;
+    unsigned int mods_ret[16];
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+    SDL_zero(mods_ret);
+
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     if (data->xkb) {
         int num_groups = XkbKeyNumGroups(data->xkb, keycode);
         unsigned char info = XkbKeyGroupInfo(data->xkb, keycode);
@@ -118,13 +109,16 @@ X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode keycode, unsigned char group)
                 group %= num_groups;
             }
         }
-        keysym = X11_XkbKeycodeToKeysym(data->display, keycode, group, 0);
-    } else {
+
+        if (X11_XkbLookupKeySym(data->display, keycode, XkbBuildCoreState(mod_mask, group), mods_ret, &keysym) == NoSymbol) {
+            keysym = NoSymbol;
+        }
+    } else
+#endif
+    {
+        /* TODO: Handle groups and modifiers on the legacy path. */
         keysym = X11_XKeycodeToKeysym(data->display, keycode, 0);
     }
-#else
-    keysym = X11_XKeycodeToKeysym(data->display, keycode, 0);
-#endif
 
     return keysym;
 }
@@ -153,7 +147,7 @@ int X11_InitKeyboard(SDL_VideoDevice *_this)
     int distance;
     Bool xkb_repeat = 0;
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     {
         int xkb_major = XkbMajorVersion;
         int xkb_minor = XkbMinorVersion;
@@ -334,6 +328,21 @@ int X11_InitKeyboard(SDL_VideoDevice *_this)
 
 void X11_UpdateKeymap(SDL_VideoDevice *_this, SDL_bool send_event)
 {
+    struct Keymod_masks
+    {
+        SDL_Keymod sdl_mask;
+        unsigned int xkb_mask;
+    } const keymod_masks[] = {
+        { SDL_KMOD_NONE, 0 },
+        { SDL_KMOD_SHIFT, ShiftMask },
+        { SDL_KMOD_CAPS, LockMask },
+        { SDL_KMOD_SHIFT | SDL_KMOD_CAPS, ShiftMask | LockMask },
+        { SDL_KMOD_MODE, Mod5Mask },
+        { SDL_KMOD_MODE | SDL_KMOD_SHIFT, Mod5Mask | ShiftMask },
+        { SDL_KMOD_MODE | SDL_KMOD_CAPS, Mod5Mask | LockMask },
+        { SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, Mod5Mask | ShiftMask | LockMask }
+    };
+
     SDL_VideoData *data = _this->driverdata;
     int i;
     SDL_Scancode scancode;
@@ -342,7 +351,7 @@ void X11_UpdateKeymap(SDL_VideoDevice *_this, SDL_bool send_event)
 
     keymap = SDL_CreateKeymap();
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     if (data->xkb) {
         XkbStateRec state;
         X11_XkbGetUpdatedMap(data->display, XkbAllClientInfoMask, data->xkb);
@@ -353,49 +362,54 @@ void X11_UpdateKeymap(SDL_VideoDevice *_this, SDL_bool send_event)
     }
 #endif
 
-    // FIXME: Need to get the mapping for all modifiers, not just the first one
-    for (i = 0; i < SDL_arraysize(data->key_layout); i++) {
-        Uint32 key;
-        SDL_Keycode keycode;
+    for (int m = 0; m < SDL_arraysize(keymod_masks); ++m) {
+        for (i = 0; i < SDL_arraysize(data->key_layout); i++) {
+            SDL_Keycode keycode;
 
-        /* Make sure this is a valid scancode */
-        scancode = data->key_layout[i];
-        if (scancode == SDL_SCANCODE_UNKNOWN) {
-            continue;
-        }
+            /* Make sure this is a valid scancode */
+            scancode = data->key_layout[i];
+            if (scancode == SDL_SCANCODE_UNKNOWN) {
+                continue;
+            }
+
+            KeySym keysym = X11_KeyCodeToSym(_this, i, group, keymod_masks[m].xkb_mask);
+
+            /* Note: The default SDL scancode table sets this to right alt instead of AltGr/Mode, so handle it separately. */
+            if (keysym != XK_ISO_Level3_Shift) {
+                keycode = SDL_KeySymToUcs4(keysym);
+            } else {
+                keycode = SDLK_MODE;
+            }
+
+            if (!keycode) {
+                SDL_Scancode keyScancode = SDL_GetScancodeFromKeySym(keysym, (KeyCode)i);
 
-        /* See if there is a UCS keycode for this scancode */
-        key = X11_KeyCodeToUcs4(_this, (KeyCode)i, group);
-        if (key) {
-            keycode = (SDL_Keycode)key;
-        } else {
-            SDL_Scancode keyScancode = X11_KeyCodeToSDLScancode(_this, (KeyCode)i);
-
-            switch (keyScancode) {
-            case SDL_SCANCODE_UNKNOWN:
-                keycode = SDLK_UNKNOWN;
-                break;
-            case SDL_SCANCODE_RETURN:
-                keycode = SDLK_RETURN;
-                break;
-            case SDL_SCANCODE_ESCAPE:
-                keycode = SDLK_ESCAPE;
-                break;
-            case SDL_SCANCODE_BACKSPACE:
-                keycode = SDLK_BACKSPACE;
-                break;
-            case SDL_SCANCODE_TAB:
-                keycode = SDLK_TAB;
-                break;
-            case SDL_SCANCODE_DELETE:
-                keycode = SDLK_DELETE;
-                break;
-            default:
-                keycode = SDL_SCANCODE_TO_KEYCODE(keyScancode);
-                break;
+                switch (keyScancode) {
+                case SDL_SCANCODE_UNKNOWN:
+                    keycode = SDLK_UNKNOWN;
+                    break;
+                case SDL_SCANCODE_RETURN:
+                    keycode = SDLK_RETURN;
+                    break;
+                case SDL_SCANCODE_ESCAPE:
+                    keycode = SDLK_ESCAPE;
+                    break;
+                case SDL_SCANCODE_BACKSPACE:
+                    keycode = SDLK_BACKSPACE;
+                    break;
+                case SDL_SCANCODE_TAB:
+                    keycode = SDLK_TAB;
+                    break;
+                case SDL_SCANCODE_DELETE:
+                    keycode = SDLK_DELETE;
+                    break;
+                default:
+                    keycode = SDL_SCANCODE_TO_KEYCODE(keyScancode);
+                    break;
+                }
             }
+            SDL_SetKeymapEntry(keymap, scancode, keymod_masks[m].sdl_mask, keycode);
         }
-        SDL_SetKeymapEntry(keymap, scancode, SDL_KMOD_NONE, keycode);
     }
     SDL_SetKeymap(keymap, send_event);
 }
@@ -404,7 +418,7 @@ void X11_QuitKeyboard(SDL_VideoDevice *_this)
 {
     SDL_VideoData *data = _this->driverdata;
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     if (data->xkb) {
         X11_XkbFreeKeyboard(data->xkb, 0, True);
         data->xkb = NULL;
diff --git a/src/video/x11/SDL_x11keyboard.h b/src/video/x11/SDL_x11keyboard.h
index 9164e04ec0918..5e0ed2803efb6 100644
--- a/src/video/x11/SDL_x11keyboard.h
+++ b/src/video/x11/SDL_x11keyboard.h
@@ -33,6 +33,6 @@ extern SDL_bool X11_HasScreenKeyboardSupport(SDL_VideoDevice *_this);
 extern void X11_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
 extern SDL_bool X11_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window);
-extern KeySym X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode, unsigned char group);
+extern KeySym X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode, unsigned char group, unsigned int mod_mask);
 
 #endif /* SDL_x11keyboard_h_ */
diff --git a/src/video/x11/SDL_x11sym.h b/src/video/x11/SDL_x11sym.h
index aa1ea9991d02c..b21c8e7a5b91f 100644
--- a/src/video/x11/SDL_x11sym.h
+++ b/src/video/x11/SDL_x11sym.h
@@ -178,12 +178,12 @@ SDL_X11_SYM(Bool,XGetEventData,(Display* a,XGenericEventCookie* b),(a,b),return)
 SDL_X11_SYM(void,XFreeEventData,(Display* a,XGenericEventCookie* b),(a,b),)
 #endif
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
 SDL_X11_SYM(Bool,XkbQueryExtension,(Display* a,int * b,int * c,int * d,int * e, int *f),(a,b,c,d,e,f),return)
 #if NeedWidePrototypes
-SDL_X11_SYM(KeySym,XkbKeycodeToKeysym,(Display* a,unsigned int b,int c,int d),(a,b,c,d),return)
+SDL_X11_SYM(Bool,XkbLookupKeySym,(Display* a, unsigned int b, unsigned int c, unsigned int* d, KeySym* e),(a,b,c,d,e),return)
 #else
-SDL_X11_SYM(KeySym,XkbKeycodeToKeysym,(Display* a,KeyCode b,int c,int d),(a,b,c,d),return)
+SDL_X11_SYM(Bool,XkbLookupKeySym,(Display* a, KeyCode b, unsigned int c, unsigned int* d, KeySym* e),(a,b,c,d,e),return)
 #endif
 SDL_X11_SYM(Status,XkbGetState,(Display* a,unsigned int b,XkbStatePtr c),(a,b,c),return)
 SDL_X11_SYM(Status,XkbGetUpdatedMap,(Display* a,unsigned int b,XkbDescPtr c),(a,b,c),return)
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index dbe1a6d53e50e..80b5c47997923 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -121,7 +121,7 @@ struct SDL_VideoData
 
     int xrandr_event_base;
 
-#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
+#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     XkbDescPtr xkb;
 #endif
     int xkb_event;