From 0aeba6064b456c711193b50e6a0cc8dbc8bfd9fa Mon Sep 17 00:00:00 2001 From: David Maciejak Date: Sat, 7 Mar 2026 23:36:39 -0500 Subject: [PATCH] WPrefs: improve capture_shortcut function This patch is improving the capture_shortcut function to be able to capture a sequence of multiple key pressed. --- WPrefs.app/KeyboardShortcuts.c | 125 +++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/WPrefs.app/KeyboardShortcuts.c b/WPrefs.app/KeyboardShortcuts.c index 8df69a9e..7d18a90a 100644 --- a/WPrefs.app/KeyboardShortcuts.c +++ b/WPrefs.app/KeyboardShortcuts.c @@ -23,6 +23,8 @@ #include "WPrefs.h" #include +#include +#include #include #include @@ -307,14 +309,53 @@ static int NumLockMask(Display *dpy) return mask; } +/* Append the modifier prefix and key name to the keybuf */ +static void build_key_combo(unsigned int xkstate, const char *keyname, + unsigned int numlock_mask, char keybuf[64]) +{ + if (xkstate & ControlMask) + strcat(keybuf, "Control+"); + if (xkstate & ShiftMask) + strcat(keybuf, "Shift+"); + if ((numlock_mask != Mod1Mask) && (xkstate & Mod1Mask)) + strcat(keybuf, "Mod1+"); + if ((numlock_mask != Mod2Mask) && (xkstate & Mod2Mask)) + strcat(keybuf, "Mod2+"); + if ((numlock_mask != Mod3Mask) && (xkstate & Mod3Mask)) + strcat(keybuf, "Mod3+"); + if ((numlock_mask != Mod4Mask) && (xkstate & Mod4Mask)) + strcat(keybuf, "Mod4+"); + if ((numlock_mask != Mod5Mask) && (xkstate & Mod5Mask)) + strcat(keybuf, "Mod5+"); + wstrlcat(keybuf, keyname, 64); +} + +/* + * Interactively capture a key shortcut or keychain, + * function waits KeychainTimeoutDelay or 300 ms after + * each key press for another key in the chain, + * and returns the full key specification string. + */ char *capture_shortcut(Display *dpy, Bool *capturing, Bool convert_case) { XEvent ev; KeySym ksym, lksym, uksym; - char buffer[64]; - char *key = NULL; + /* Large enough for several chained chords */ + char buffer[512]; + char keybuf[64]; + char *key; unsigned int numlock_mask; + Bool have_key = False; + Bool chain_mode; + int timeout_ms; + timeout_ms = GetIntegerForKey("KeychainTimeoutDelay"); + if (timeout_ms <= 0) + timeout_ms = 300; + + buffer[0] = '\0'; + + /* ---- Phase 1: capture the first key (blocking) ---- */ while (*capturing) { XAllowEvents(dpy, AsyncKeyboard, CurrentTime); WMNextEvent(dpy, &ev); @@ -332,41 +373,62 @@ char *capture_shortcut(Display *dpy, Bool *capturing, Bool convert_case) key = XKeysymToString(ksym); } - *capturing = 0; + keybuf[0] = '\0'; + build_key_combo(ev.xkey.state, key, numlock_mask, keybuf); + wstrlcat(buffer, keybuf, sizeof(buffer)); + have_key = True; break; } } WMHandleEvent(&ev); } - if (!key) + /* ---- Phase 2: collect additional chain keys with timeout ---- */ + chain_mode = (timeout_ms > 0); + while (*capturing && chain_mode) { + int xfd = ConnectionNumber(dpy); + fd_set rfds; + struct timeval tv; + + if (!XPending(dpy)) { + FD_ZERO(&rfds); + FD_SET(xfd, &rfds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + XFlush(dpy); + if (select(xfd + 1, &rfds, NULL, NULL, &tv) == 0) + break; /* timeout: the chain is complete */ + } + + XAllowEvents(dpy, AsyncKeyboard, CurrentTime); + WMNextEvent(dpy, &ev); + if (ev.type == KeyPress && ev.xkey.keycode != 0) { + numlock_mask = NumLockMask(dpy); + ksym = W_KeycodeToKeysym(dpy, ev.xkey.keycode, + ev.xkey.state & numlock_mask ? 1 : 0); + + if (!IsModifierKey(ksym)) { + if (convert_case) { + XConvertCase(ksym, &lksym, &uksym); + key = XKeysymToString(uksym); + } else { + key = XKeysymToString(ksym); + } + + keybuf[0] = '\0'; + build_key_combo(ev.xkey.state, key, numlock_mask, keybuf); + wstrlcat(buffer, " ", sizeof(buffer)); + wstrlcat(buffer, keybuf, sizeof(buffer)); + } + } else { + WMHandleEvent(&ev); + } + } + + if (!have_key || !*capturing) return NULL; - buffer[0] = 0; - - if (ev.xkey.state & ControlMask) - strcat(buffer, "Control+"); - - if (ev.xkey.state & ShiftMask) - strcat(buffer, "Shift+"); - - if ((numlock_mask != Mod1Mask) && (ev.xkey.state & Mod1Mask)) - strcat(buffer, "Mod1+"); - - if ((numlock_mask != Mod2Mask) && (ev.xkey.state & Mod2Mask)) - strcat(buffer, "Mod2+"); - - if ((numlock_mask != Mod3Mask) && (ev.xkey.state & Mod3Mask)) - strcat(buffer, "Mod3+"); - - if ((numlock_mask != Mod4Mask) && (ev.xkey.state & Mod4Mask)) - strcat(buffer, "Mod4+"); - - if ((numlock_mask != Mod5Mask) && (ev.xkey.state & Mod5Mask)) - strcat(buffer, "Mod5+"); - - wstrlcat(buffer, key, sizeof(buffer)); - + *capturing = 0; return wstrdup(buffer); } @@ -444,7 +506,7 @@ static void captureClick(WMWidget * w, void *data) } panel->capturing = 0; WMSetButtonText(w, _("Capture")); - WMSetLabelText(panel->instructionsL, _("Click on Capture to interactively define the shortcut key.")); + WMSetLabelText(panel->instructionsL, _("Click on Capture to interactively define the shortcut key(s).")); XUngrabKeyboard(dpy, CurrentTime); } @@ -456,6 +518,9 @@ static void clearShortcut(WMWidget * w, void *data) /* Parameter not used, but tell the compiler that it is ok */ (void) w; + /* Cancel any ongoing capture so the keychain loop is unblocked */ + panel->capturing = 0; + WMSetTextFieldText(panel->shoT, NULL); if (row >= 0) {