diff --git a/src/Makefile.am b/src/Makefile.am index 37e4d360..8782191b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -45,6 +45,8 @@ wmaker_SOURCES = \ icon.c \ icon.h \ keybind.h \ + keytree.c \ + keytree.h \ main.c \ main.h \ menu.c \ diff --git a/src/WindowMaker.h b/src/WindowMaker.h index f06f8015..7255ab33 100644 --- a/src/WindowMaker.h +++ b/src/WindowMaker.h @@ -26,6 +26,7 @@ #include #include #include +#include "keytree.h" /* class codes */ @@ -470,6 +471,8 @@ extern struct WPreferences { int clip_auto_expand_delay; /* Delay after which the clip will expand when entered */ int clip_auto_collapse_delay; /* Delay after which the clip will collapse when leaved */ + int keychain_timeout_delay; /* Delay after which a keychain is reset, 0 means disabled */ + RImage *swtileImage; RImage *swbackImage[9]; @@ -649,6 +652,17 @@ extern struct wmaker_global_variables { * impact the shortcuts (typically: CapsLock, NumLock, ScrollLock) */ unsigned int modifiers_mask; + + /* + * Key-chain trie cursor. + * + * curpos == NULL : idle, no active chain. + * curpos != NULL : inside a chain; curpos points to the last matched + * internal node in wKeyTreeRoot. The next expected + * key is one of curpos->first_child's siblings. + */ + WKeyNode *curpos; + WMHandlerID chain_timeout_handler; /* non-NULL while chain timer is armed */ } shortcut; } w_global; diff --git a/src/defaults.c b/src/defaults.c index 8472eac3..f8fae418 100644 --- a/src/defaults.c +++ b/src/defaults.c @@ -4,7 +4,7 @@ * * Copyright (c) 1997-2003 Alfredo K. Kojima * Copyright (c) 1998-2003 Dan Pascu - * Copyright (c) 2014-2023 Window Maker Team + * Copyright (c) 2014-2026 Window Maker Team * * This program is free software; you can redistribute it and/or modify @@ -64,8 +64,7 @@ #include "properties.h" #include "misc.h" #include "winmenu.h" - -#define MAX_SHORTCUT_LENGTH 32 +#include "rootmenu.h" typedef struct _WDefaultEntry WDefaultEntry; typedef int (WDECallbackConvert) (WScreen *scr, WDefaultEntry *entry, WMPropList *plvalue, void *addr, void **tdata); @@ -539,6 +538,8 @@ WDefaultEntry optionList[] = { &wPreferences.window_list_app_icons, getBool, NULL, NULL, NULL}, {"MouseWheelFocus", "NO", NULL, &wPreferences.mouse_wheel_focus, getBool, NULL, NULL, NULL}, + {"KeychainTimeoutDelay", "500", NULL, + &wPreferences.keychain_timeout_delay, getInt, NULL, NULL, NULL}, /* style options */ @@ -648,6 +649,8 @@ WDefaultEntry optionList[] = { NULL, getKeybind, setKeyGrab, NULL, NULL}, {"WindowMenuKey", "Control+Escape", (void *)WKBD_WINDOWMENU, NULL, getKeybind, setKeyGrab, NULL, NULL}, + {"KeychainCancelKey", "None", (void *)WKBD_KEYCHAIN_CANCEL, + NULL, getKeybind, setKeyGrab, NULL, NULL}, {"DockRaiseLowerKey", "None", (void*)WKBD_DOCKRAISELOWER, NULL, getKeybind, setKeyGrab, NULL, NULL}, {"ClipRaiseLowerKey", "None", (void *)WKBD_CLIPRAISELOWER, @@ -1174,6 +1177,13 @@ void wDefaultsCheckDomains(void* arg) wwarning(_("could not load domain %s from user defaults database"), "WMRootMenu"); } w_global.domain.root_menu->timestamp = stbuf.st_mtime; + + /* Rebuild the root menu (without mapping) so that shortcuts take effect immediately. */ + for (i = 0; i < w_global.screen_count; i++) { + WScreen *s = wScreenWithNumber(i); + if (s) + wRootMenuReparse(s); + } } #ifndef HAVE_INOTIFY if (!arg) @@ -1243,7 +1253,6 @@ void wReadDefaults(WScreen * scr, WMPropList * new_dict) if (entry->update) needs_refresh |= (*entry->update) (scr, entry, tdata, entry->extra_data); - } } } @@ -1314,6 +1323,7 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict) { WDefaultEntry *entry; unsigned int i; + Bool keybindings_changed = False; void *tdata; for (i = 0; i < wlengthof(optionList); i++) { @@ -1326,11 +1336,37 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict) plvalue = entry->plvalue; if (plvalue) { int ok = (*entry->convert)(scr, entry, plvalue, entry->addr, &tdata); - if (ok && entry->update) + if (ok && entry->update) { + /* Check whether the (re-)computed binding differs from + * the one already in wKeyBindings[] */ + long widx = (long)entry->extra_data; + WShortKey *nw = (WShortKey *)tdata; + WShortKey *cur = &wKeyBindings[widx]; + Bool binding_changed = + (cur->modifier != nw->modifier || + cur->keycode != nw->keycode || + cur->chain_length != nw->chain_length); + + if (!binding_changed && nw->chain_length > 1 && + cur->chain_modifiers && cur->chain_keycodes) { + int n = nw->chain_length - 1; + + binding_changed = + (memcmp(cur->chain_modifiers, nw->chain_modifiers, + n * sizeof(unsigned int)) != 0 || + memcmp(cur->chain_keycodes, nw->chain_keycodes, + n * sizeof(KeyCode)) != 0); + } (*entry->update)(scr, entry, tdata, entry->extra_data); + if (binding_changed) + keybindings_changed = True; } } + } } + + if (keybindings_changed) + wKeyTreeRebuild(scr); } void wDefaultUpdateIcons(WScreen *scr) @@ -1359,6 +1395,57 @@ void wDefaultUpdateIcons(WScreen *scr) } } +/* Rebuild the global key-binding trie from scratch */ +void wKeyTreeRebuild(WScreen *scr) +{ + int i; + + /* Parameter not used */ + (void)scr; + + wKeyTreeDestroy(wKeyTreeRoot); + wKeyTreeRoot = NULL; + + /* Insert all wKeyBindings[] entries */ + for (i = 0; i < WKBD_LAST; i++) { + WShortKey *k = &wKeyBindings[i]; + WKeyAction *act; + KeyCode *keys; + WKeyNode *leaf; + int len, j; + unsigned int *mods; + + /* WKBD_KEYCHAIN_CANCEL is only meaningful while inside an active key chain */ + if (i == WKBD_KEYCHAIN_CANCEL) + continue; + + if (k->keycode == 0) + continue; + + len = (k->chain_length > 1) ? k->chain_length : 1; + mods = wmalloc(len * sizeof(unsigned int)); + keys = wmalloc(len * sizeof(KeyCode)); + mods[0] = k->modifier; + keys[0] = k->keycode; + + for (j = 1; j < len; j++) { + mods[j] = k->chain_modifiers[j - 1]; + keys[j] = k->chain_keycodes[j - 1]; + } + + leaf = wKeyTreeInsert(&wKeyTreeRoot, mods, keys, len); + wfree(mods); + wfree(keys); + + act = wKeyNodeAddAction(leaf, WKN_WKBD); + if (act) + act->u.wkbd_idx = i; + } + + /* Insert root-menu shortcuts */ + wRootMenuInsertIntoTree(); +} + /* --------------------------- Local ----------------------- */ #define GET_STRING_OR_DEFAULT(x, var) if (!WMIsPLString(value)) { \ @@ -2210,13 +2297,51 @@ static int getColor(WScreen * scr, WDefaultEntry * entry, WMPropList * value, vo return True; } +static Bool parseOneKey(WDefaultEntry *entry, const char *token, + unsigned int *out_mod, KeyCode *out_code) +{ + char tmp[MAX_SHORTCUT_LENGTH]; + char *b, *k; + KeySym ksym; + + wstrlcpy(tmp, token, MAX_SHORTCUT_LENGTH); + b = tmp; + + *out_mod = 0; + while ((k = strchr(b, '+')) != NULL) { + int mod; + *k = 0; + mod = wXModifierFromKey(b); + if (mod < 0) { + wwarning(_("%s: invalid key modifier \"%s\""), entry->key, b); + return False; + } + *out_mod |= mod; + b = k + 1; + } + + ksym = XStringToKeysym(b); + if (ksym == NoSymbol) { + wwarning(_("%s: invalid kbd shortcut specification \"%s\""), entry->key, token); + return False; + } + + *out_code = XKeysymToKeycode(dpy, ksym); + if (*out_code == 0) { + wwarning(_("%s: invalid key in shortcut \"%s\""), entry->key, token); + return False; + } + + return True; +} + static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value, void *addr, void **ret) { static WShortKey shortcut; - KeySym ksym; const char *val; - char *k; - char buf[MAX_SHORTCUT_LENGTH], *b; + char buf[MAX_SHORTCUT_LENGTH]; + char *token, *saveptr; + int step; /* Parameter not used, but tell the compiler that it is ok */ (void) scr; @@ -2224,9 +2349,11 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value, GET_STRING_OR_DEFAULT("Key spec", val); + /* Free old chain arrays before overwriting */ + wShortKeyFree(&shortcut); + if (!val || strcasecmp(val, "NONE") == 0) { - shortcut.keycode = 0; - shortcut.modifier = 0; + shortcut.chain_length = 1; if (ret) *ret = &shortcut; return True; @@ -2234,37 +2361,36 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value, wstrlcpy(buf, val, MAX_SHORTCUT_LENGTH); - b = (char *)buf; + /* + * Support both the traditional single-key syntax and the + * key-chain syntax where space-separated tokens represent + * keys that must be pressed in sequence + */ + step = 0; + token = strtok_r(buf, " ", &saveptr); + while (token != NULL) { + unsigned int mod; + KeyCode kcode; - /* get modifiers */ - shortcut.modifier = 0; - while ((k = strchr(b, '+')) != NULL) { - int mod; - - *k = 0; - mod = wXModifierFromKey(b); - if (mod < 0) { - wwarning(_("%s: invalid key modifier \"%s\""), entry->key, b); + if (!parseOneKey(entry, token, &mod, &kcode)) return False; + + if (step == 0) { + shortcut.modifier = mod; + shortcut.keycode = kcode; + } else { + shortcut.chain_modifiers = wrealloc(shortcut.chain_modifiers, + step * sizeof(unsigned int)); + shortcut.chain_keycodes = wrealloc(shortcut.chain_keycodes, + step * sizeof(KeyCode)); + shortcut.chain_modifiers[step - 1] = mod; + shortcut.chain_keycodes[step - 1] = kcode; } - shortcut.modifier |= mod; - - b = k + 1; + step++; + token = strtok_r(NULL, " ", &saveptr); } - /* get key */ - ksym = XStringToKeysym(b); - - if (ksym == NoSymbol) { - wwarning(_("%s:invalid kbd shortcut specification \"%s\""), entry->key, val); - return False; - } - - shortcut.keycode = XKeysymToKeycode(dpy, ksym); - if (shortcut.keycode == 0) { - wwarning(_("%s:invalid key in shortcut \"%s\""), entry->key, val); - return False; - } + shortcut.chain_length = (step > 1) ? step : 1; if (ret) *ret = &shortcut; @@ -3267,7 +3393,25 @@ static int setKeyGrab(WScreen * scr, WDefaultEntry * entry, void *tdata, void *e /* Parameter not used, but tell the compiler that it is ok */ (void) entry; + /* Free old chain arrays before overwriting */ + wShortKeyFree(&wKeyBindings[widx]); + + /* Shallow copy, then deep-copy the heap arrays */ wKeyBindings[widx] = *shortcut; + if (shortcut->chain_length > 1) { + int n = shortcut->chain_length - 1; + + wKeyBindings[widx].chain_modifiers = wmalloc(n * sizeof(unsigned int)); + wKeyBindings[widx].chain_keycodes = wmalloc(n * sizeof(KeyCode)); + + memcpy(wKeyBindings[widx].chain_modifiers, shortcut->chain_modifiers, + n * sizeof(unsigned int)); + memcpy(wKeyBindings[widx].chain_keycodes, shortcut->chain_keycodes, + n * sizeof(KeyCode)); + } else { + wKeyBindings[widx].chain_modifiers = NULL; + wKeyBindings[widx].chain_keycodes = NULL; + } wwin = scr->focused_window; diff --git a/src/defaults.h b/src/defaults.h index 5aaf448a..492459ba 100644 --- a/src/defaults.h +++ b/src/defaults.h @@ -57,5 +57,5 @@ void wDefaultChangeIcon(const char *instance, const char* class, const char *fil RImage *get_rimage_from_file(WScreen *scr, const char *file_name, int max_size); void wDefaultPurgeInfo(const char *instance, const char *class); - +void wKeyTreeRebuild(WScreen *scr); /* Rebuild the key-chain trie from the current key bindings */ #endif /* WMDEFAULTS_H_ */ diff --git a/src/event.c b/src/event.c index a541a962..0fcbf0e7 100644 --- a/src/event.c +++ b/src/event.c @@ -384,6 +384,12 @@ static void handle_inotify_events(void) /* move to next event in the buffer */ i += sizeof(struct inotify_event) + pevent->len; } + + for (i = 0; i < w_global.screen_count; i++) { + WScreen *scr = wScreenWithNumber(i); + if (scr) + wKeyTreeRebuild(scr); + } } #endif /* HAVE_INOTIFY */ @@ -1422,56 +1428,55 @@ static int CheckFullScreenWindowFocused(WScreen * scr) return 0; } -static void handleKeyPress(XEvent * event) +/* ------------------------------------------------------------------ * + * Key-chain timeout support * + * * + * wPreferences.keychain_timeout_delay in milliseconds after a chain * + * leader is pressed, the chain is automatically cancelled so the * + * user is not stuck in a half-entered sequence. Set to 0 to disable. * + * ------------------------------------------------------------------ */ + +/* Cancels the chain on inactivity */ +static void chainTimeoutCallback(void *data) { - WScreen *scr = wScreenForRootWindow(event->xkey.root); - WWindow *wwin = scr->focused_window; - short i, widx; - int modifiers; - int command = -1; + (void)data; + + XUngrabKeyboard(dpy, CurrentTime); + w_global.shortcut.curpos = NULL; + w_global.shortcut.chain_timeout_handler = NULL; +} + +/* Start (or restart) the chain inactivity timer */ +static void wStartChainTimer(void) +{ + if (wPreferences.keychain_timeout_delay > 0) { + if (w_global.shortcut.chain_timeout_handler) + WMDeleteTimerHandler(w_global.shortcut.chain_timeout_handler); + w_global.shortcut.chain_timeout_handler = + WMAddTimerHandler(wPreferences.keychain_timeout_delay, chainTimeoutCallback, NULL); + } +} + +/* Cancel the chain inactivity timer, if armed */ +static void wCancelChainTimer(void) +{ + if (w_global.shortcut.chain_timeout_handler) { + WMDeleteTimerHandler(w_global.shortcut.chain_timeout_handler); + w_global.shortcut.chain_timeout_handler = NULL; + } +} + +#define ISMAPPED(w) ((w) && !(w)->flags.miniaturized && ((w)->flags.mapped || (w)->flags.shaded)) +#define ISFOCUSED(w) ((w) && (w)->flags.focused) + +static void dispatchWKBDCommand(int command, WScreen *scr, WWindow *wwin, XEvent *event) +{ + short widx; + int i; #ifdef KEEP_XKB_LOCK_STATUS XkbStateRec staterec; #endif /*KEEP_XKB_LOCK_STATUS */ - /* ignore CapsLock */ - modifiers = event->xkey.state & w_global.shortcut.modifiers_mask; - - for (i = 0; i < WKBD_LAST; i++) { - if (wKeyBindings[i].keycode == 0) - continue; - - if (wKeyBindings[i].keycode == event->xkey.keycode && ( /*wKeyBindings[i].modifier==0 - || */ wKeyBindings[i].modifier == - modifiers)) { - command = i; - break; - } - } - - if (command < 0) { - - if (!wRootMenuPerformShortcut(event)) { - static int dontLoop = 0; - - if (dontLoop > 10) { - wwarning("problem with key event processing code"); - return; - } - dontLoop++; - /* if the focused window is an internal window, try redispatching - * the event to the managed window, as it can be a WINGs window */ - if (wwin && wwin->flags.internal_window && wwin->client_leader != None) { - /* client_leader contains the WINGs toplevel */ - event->xany.window = wwin->client_leader; - WMHandleEvent(event); - } - dontLoop--; - } - return; - } -#define ISMAPPED(w) ((w) && !(w)->flags.miniaturized && ((w)->flags.mapped || (w)->flags.shaded)) -#define ISFOCUSED(w) ((w) && (w)->flags.focused) - switch (command) { case WKBD_ROOTMENU: @@ -1970,6 +1975,132 @@ static void handleKeyPress(XEvent * event) } } +static void handleKeyPress(XEvent * event) +{ + WScreen *scr = wScreenForRootWindow(event->xkey.root); + WWindow *wwin = scr->focused_window; + WKeyNode *siblings; + WKeyNode *match; + WKeyAction *act; + int modifiers; + + /* ignore CapsLock */ + modifiers = event->xkey.state & w_global.shortcut.modifiers_mask; + + /* ------------------------------------------------------------------ * + * Trie-based key-chain matching * + * * + * wKeyTreeRoot is a prefix trie covering ALL key bindings * + * (wKeyBindings and root-menu shortcuts combined). * + * curpos tracks the last matched internal node. * + * NULL means we are at the root (idle). * + * ------------------------------------------------------------------ */ + + if (w_global.shortcut.curpos != NULL) { + /* Inside a chain: look for the next key among children */ + if (event->xkey.keycode == wKeyBindings[WKBD_KEYCHAIN_CANCEL].keycode && + modifiers == wKeyBindings[WKBD_KEYCHAIN_CANCEL].modifier) { + wCancelChainTimer(); + XUngrabKeyboard(dpy, CurrentTime); + w_global.shortcut.curpos = NULL; + return; + } + + siblings = w_global.shortcut.curpos->first_child; + match = wKeyTreeFind(siblings, modifiers, event->xkey.keycode); + + if (match != NULL && match->first_child != NULL) { + /* Internal node: advance and keep waiting */ + w_global.shortcut.curpos = match; + wStartChainTimer(); + return; + } + + if (match == NULL) { + /* Unrecognized key inside chain: exit chain mode */ + wCancelChainTimer(); + XUngrabKeyboard(dpy, CurrentTime); + w_global.shortcut.curpos = NULL; + return; + } + + /* + * Sticky-chain mode: when a KeychainCancelKey is configured, + * stay at the parent level after executing a leaf instead of always + * returning to root. + */ + if (wKeyBindings[WKBD_KEYCHAIN_CANCEL].keycode != 0) { + WKeyNode *parent = match->parent; + WKeyNode *child; + int nchildren = 0; + + for (child = parent->first_child; child != NULL; child = child->next_sibling) + nchildren++; + + if (nchildren > 1) { + /* Multi-branch parent: stay in chain mode at this level */ + w_global.shortcut.curpos = parent; + wStartChainTimer(); + } else { + /* Single-branch parent: nothing left to wait for, exit chain */ + wCancelChainTimer(); + XUngrabKeyboard(dpy, CurrentTime); + w_global.shortcut.curpos = NULL; + } + } else { + /* No cancel key configured: always exit chain after a leaf */ + wCancelChainTimer(); + XUngrabKeyboard(dpy, CurrentTime); + w_global.shortcut.curpos = NULL; + } + } else { + /* Idle: look for a root-level match */ + match = wKeyTreeFind(wKeyTreeRoot, modifiers, event->xkey.keycode); + + if (match == NULL) { + /* Not a known shortcut: try to redispatch it */ + static int dontLoop = 0; + + if (dontLoop > 10) { + wwarning("problem with key event processing code"); + return; + } + dontLoop++; + if (wwin && wwin->flags.internal_window && + wwin->client_leader != None) { + event->xany.window = wwin->client_leader; + WMHandleEvent(event); + } + dontLoop--; + + return; + } + + if (match->first_child != NULL) { + /* Internal node: enter chain mode */ + w_global.shortcut.curpos = match; + XGrabKeyboard(dpy, scr->root_win, False, + GrabModeAsync, GrabModeAsync, CurrentTime); + wStartChainTimer(); + + return; + } + } + + /* Execute all leaf actions for this key sequence */ + for (act = match->actions; act != NULL; act = act->next) { + if (act->type == WKN_MENU) { + WMenu *menu = (WMenu *) act->u.menu.menu; + WMenuEntry *entry = (WMenuEntry *) act->u.menu.entry; + + (*entry->callback)(menu, entry); + } else { + dispatchWKBDCommand(act->u.wkbd_idx, scr, wwin, event); + } + } + return; +} + #define CORNER_NONE 0 #define CORNER_TOPLEFT 1 #define CORNER_TOPRIGHT 2 diff --git a/src/keybind.h b/src/keybind.h index a9478574..292cbb71 100644 --- a/src/keybind.h +++ b/src/keybind.h @@ -2,7 +2,7 @@ * Window Maker window manager * * Copyright (c) 1997-2003 Alfredo K. Kojima - * Copyright (c) 2014-2023 Window Maker Team + * Copyright (c) 2014-2026 Window Maker Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +22,8 @@ #ifndef WMKEYBIND_H #define WMKEYBIND_H +#define MAX_SHORTCUT_LENGTH 64 + /* doesn't define these, even though XFree supports them */ #ifndef Button6 #define Button6 6 @@ -44,6 +46,7 @@ enum { WKBD_ROOTMENU, WKBD_WINDOWMENU, WKBD_WINDOWLIST, + WKBD_KEYCHAIN_CANCEL, /* window */ WKBD_MINIATURIZE, @@ -166,8 +169,13 @@ enum { }; typedef struct WShortKey { - unsigned int modifier; - KeyCode keycode; + unsigned int modifier; /* leader (or only) key modifier - always valid */ + KeyCode keycode; /* leader (or only) key code - always valid */ + + /* Key-chain support */ + int chain_length; + unsigned int *chain_modifiers; /* heap-allocated, NULL for single keys */ + KeyCode *chain_keycodes; /* heap-allocated, NULL for single keys */ } WShortKey; /* ---[ Global Variables ]------------------------------------------------ */ @@ -177,5 +185,6 @@ extern WShortKey wKeyBindings[WKBD_LAST]; /* ---[ Functions ]------------------------------------------------------- */ void wKeyboardInitialize(void); +void wShortKeyFree(WShortKey *key); #endif /* WMKEYBIND_H */ diff --git a/src/keytree.c b/src/keytree.c new file mode 100644 index 00000000..c440e5a8 --- /dev/null +++ b/src/keytree.c @@ -0,0 +1,107 @@ +/* keytree.c - Trie (prefix tree) for key-chain bindings + * + * Window Maker window manager + * + * Copyright (c) 2026 Window Maker Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "wconfig.h" +#include +#include +#include "keytree.h" + +/* Global trie root */ +WKeyNode *wKeyTreeRoot = NULL; + +WKeyNode *wKeyTreeFind(WKeyNode *siblings, unsigned int mod, KeyCode key) +{ + WKeyNode *p = siblings; + + while (p != NULL) { + if (p->modifier == mod && p->keycode == key) + return p; + p = p->next_sibling; + } + return NULL; +} + +WKeyNode *wKeyTreeInsert(WKeyNode **root, unsigned int *mods, KeyCode *keys, int nkeys) +{ + WKeyNode **slot = root; + WKeyNode *parent = NULL; + int i; + + if (nkeys <= 0) + return NULL; + + for (i = 0; i < nkeys; i++) { + WKeyNode *node = wKeyTreeFind(*slot, mods[i], keys[i]); + + if (node == NULL) { + node = wmalloc(sizeof(WKeyNode)); + memset(node, 0, sizeof(WKeyNode)); + node->modifier = mods[i]; + node->keycode = keys[i]; + node->parent = parent; + node->next_sibling = *slot; + *slot = node; + } + + parent = node; + slot = &node->first_child; + } + + return parent; /* leaf */ +} + + +void wKeyTreeDestroy(WKeyNode *node) +{ + /* Iterates siblings at each level, recurses only into children */ + while (node != NULL) { + WKeyNode *next = node->next_sibling; + WKeyAction *act, *next_act; + + wKeyTreeDestroy(node->first_child); + for (act = node->actions; act != NULL; act = next_act) { + next_act = act->next; + wfree(act); + } + wfree(node); + node = next; + } +} + +WKeyAction *wKeyNodeAddAction(WKeyNode *leaf, WKeyActionType type) +{ + WKeyAction *act = wmalloc(sizeof(WKeyAction)); + WKeyAction *p; + + memset(act, 0, sizeof(WKeyAction)); + act->type = type; + + /* Append to end of list to preserve insertion order */ + if (leaf->actions == NULL) { + leaf->actions = act; + } else { + p = leaf->actions; + while (p->next) + p = p->next; + p->next = act; + } + return act; +} \ No newline at end of file diff --git a/src/keytree.h b/src/keytree.h new file mode 100644 index 00000000..fcf223ae --- /dev/null +++ b/src/keytree.h @@ -0,0 +1,97 @@ +/* keytree.h - Trie (prefix tree) for key-chain bindings + * + * Window Maker window manager + * + * Copyright (c) 2026 Window Maker Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef WMKEYTREE_H +#define WMKEYTREE_H + +#include + +/* + * Each key in a binding sequence occupies one node in the trie. + * Internal nodes (first_child != NULL) represent a prefix that has been + * typed so far, leaf nodes carry the action payload. + */ + +typedef enum { + WKN_WKBD, /* action: wKeyBindings command (WKBD_* index) */ + WKN_MENU /* action: root-menu entry callback */ +} WKeyActionType; + +/* + * A single action attached to a trie leaf node. Multiple actions may share + * the same key sequence and are chained through the 'next' pointer, allowing + * one key press to trigger several commands simultaneously. + */ +typedef struct WKeyAction { + WKeyActionType type; + union { + int wkbd_idx; /* WKN_WKBD: WKBD_* enum value */ + struct { + void *menu; /* WKN_MENU: cast to WMenu */ + void *entry; /* WKN_MENU: cast to WMenuEntry */ + } menu; + } u; + struct WKeyAction *next; /* next action for this key sequence, or NULL */ +} WKeyAction; + +typedef struct WKeyNode { + unsigned int modifier; + KeyCode keycode; + + WKeyAction *actions; /* non-NULL only for leaf nodes (first_child == NULL) */ + + struct WKeyNode *parent; + struct WKeyNode *first_child; /* first key of next step in chain */ + struct WKeyNode *next_sibling; /* alternative binding at same depth */ +} WKeyNode; + +/* Global trie root */ +extern WKeyNode *wKeyTreeRoot; + +/* + * Insert a key sequence into *root. + * mods[0]/keys[0] - root (leader) key + * mods[1..n-1]/keys[1..n-1] - follower keys + * Shared prefixes are merged automatically. + * Returns the leaf node (caller must set its type/payload). + * Returns NULL if nkeys <= 0. + */ +WKeyNode *wKeyTreeInsert(WKeyNode **root, unsigned int *mods, KeyCode *keys, int nkeys); + +/* + * Find the first sibling in the list starting at 'siblings' that matches + * (mod, key). Returns NULL if not found. + */ +WKeyNode *wKeyTreeFind(WKeyNode *siblings, unsigned int mod, KeyCode key); + +/* + * Recursively free the entire subtree rooted at 'node'. + */ +void wKeyTreeDestroy(WKeyNode *node); + +/* + * Allocate a new WKeyAction of the given type, append it to leaf->actions, + * and return it for the caller to set the payload (wkbd_idx or menu.*). + * Multiple calls with the same leaf accumulate actions in insertion order. + */ +WKeyAction *wKeyNodeAddAction(WKeyNode *leaf, WKeyActionType type); + +#endif /* WMKEYTREE_H */ diff --git a/src/misc.c b/src/misc.c index 007f71c9..38d6238c 100644 --- a/src/misc.c +++ b/src/misc.c @@ -881,46 +881,85 @@ char *GetShortcutString(const char *shortcut) char *GetShortcutKey(WShortKey key) { - const char *key_name; - char buffer[256]; - char *wr; + char buf[MAX_SHORTCUT_LENGTH]; + char *wr, *result, *seg; + int step; void append_string(const char *text) { - const char *string = text; + const char *s = text; - while (*string) { - if (wr >= buffer + sizeof(buffer) - 1) + while (*s) { + if (wr >= buf + sizeof(buf) - 1) break; - *wr++ = *string++; + *wr++ = *s++; } } void append_modifier(int modifier_index, const char *fallback_name) { - if (wPreferences.modifier_labels[modifier_index]) { + if (wPreferences.modifier_labels[modifier_index]) append_string(wPreferences.modifier_labels[modifier_index]); - } else { + else append_string(fallback_name); - } } - key_name = XKeysymToString(W_KeycodeToKeysym(dpy, key.keycode, 0)); - if (!key_name) + Bool build_token(unsigned int mod, KeyCode kcode) + { + const char *kname = XKeysymToString(W_KeycodeToKeysym(dpy, kcode, 0)); + + if (!kname) + return False; + + wr = buf; + if (mod & ControlMask) append_modifier(1, "Control+"); + if (mod & ShiftMask) append_modifier(0, "Shift+"); + if (mod & Mod1Mask) append_modifier(2, "Mod1+"); + if (mod & Mod2Mask) append_modifier(3, "Mod2+"); + if (mod & Mod3Mask) append_modifier(4, "Mod3+"); + if (mod & Mod4Mask) append_modifier(5, "Mod4+"); + if (mod & Mod5Mask) append_modifier(6, "Mod5+"); + append_string(kname); + *wr = '\0'; + + return True; + } + + if (!build_token(key.modifier, key.keycode)) return NULL; - wr = buffer; - if (key.modifier & ControlMask) append_modifier(1, "Control+"); - if (key.modifier & ShiftMask) append_modifier(0, "Shift+"); - if (key.modifier & Mod1Mask) append_modifier(2, "Mod1+"); - if (key.modifier & Mod2Mask) append_modifier(3, "Mod2+"); - if (key.modifier & Mod3Mask) append_modifier(4, "Mod3+"); - if (key.modifier & Mod4Mask) append_modifier(5, "Mod4+"); - if (key.modifier & Mod5Mask) append_modifier(6, "Mod5+"); - append_string(key_name); - *wr = '\0'; + /* Convert the leader token to its display string */ + result = GetShortcutString(buf); - return GetShortcutString(buffer); + /* Append each chain follower separated by a space */ + for (step = 0; step < key.chain_length - 1; step++) { + char *combined; + + if (key.chain_keycodes[step] == 0) + break; + + if (!build_token(key.chain_modifiers[step], key.chain_keycodes[step])) + break; + + seg = GetShortcutString(buf); + combined = wstrconcat(result, " "); + wfree(result); + result = wstrconcat(combined, seg); + wfree(combined); + wfree(seg); + } + + return result; +} + +void wShortKeyFree(WShortKey *key) +{ + if (!key) + return; + + wfree(key->chain_modifiers); + wfree(key->chain_keycodes); + memset(key, 0, sizeof(*key)); } char *EscapeWM_CLASS(const char *name, const char *class) diff --git a/src/rootmenu.c b/src/rootmenu.c index 213c30e8..fb42c30f 100644 --- a/src/rootmenu.c +++ b/src/rootmenu.c @@ -60,8 +60,6 @@ #include -#define MAX_SHORTCUT_LENGTH 32 - static WMenu *readMenuPipe(WScreen * scr, char **file_name); static WMenu *readPLMenuPipe(WScreen * scr, char **file_name); static WMenu *readMenuFile(WScreen *scr, const char *file_name); @@ -75,6 +73,11 @@ typedef struct Shortcut { KeyCode keycode; WMenuEntry *entry; WMenu *menu; + + /* Key-chain support */ + int chain_length; + unsigned int *chain_modifiers; /* heap-allocated, NULL for single keys */ + KeyCode *chain_keycodes; /* heap-allocated, NULL for single keys */ } Shortcut; static Shortcut *shortcutList = NULL; @@ -320,27 +323,44 @@ static char *getLocalizedMenuFile(const char *menu) return NULL; } -Bool wRootMenuPerformShortcut(XEvent * event) +/* + * Insert all root-menu shortcuts into the + * global key-binding trie (wKeyTreeRoot) + */ +void wRootMenuInsertIntoTree(void) { - WScreen *scr = wScreenForRootWindow(event->xkey.root); Shortcut *ptr; - int modifiers; - int done = 0; - - /* ignore CapsLock */ - modifiers = event->xkey.state & w_global.shortcut.modifiers_mask; for (ptr = shortcutList; ptr != NULL; ptr = ptr->next) { - if (ptr->keycode == 0 || ptr->menu->menu->screen_ptr != scr) + unsigned int *mods; + KeyCode *keys; + int len, j; + WKeyNode *leaf; + + if (ptr->keycode == 0) continue; - if (ptr->keycode == event->xkey.keycode && ptr->modifier == modifiers) { - (*ptr->entry->callback) (ptr->menu, ptr->entry); - done = True; + len = (ptr->chain_length > 1) ? ptr->chain_length : 1; + mods = wmalloc(len * sizeof(unsigned int)); + keys = wmalloc(len * sizeof(KeyCode)); + mods[0] = ptr->modifier; + keys[0] = ptr->keycode; + + for (j = 1; j < len; j++) { + mods[j] = ptr->chain_modifiers[j - 1]; + keys[j] = ptr->chain_keycodes[j - 1]; + } + + leaf = wKeyTreeInsert(&wKeyTreeRoot, mods, keys, len); + wfree(mods); + wfree(keys); + + if (leaf) { + WKeyAction *act = wKeyNodeAddAction(leaf, WKN_MENU); + act->u.menu.menu = ptr->menu; + act->u.menu.entry = ptr->entry; } } - - return done; } void wRootMenuBindShortcuts(Window window) @@ -377,6 +397,16 @@ static void rebindKeygrabs(WScreen * scr) } } +static void freeShortcut(Shortcut *s) +{ + if (!s) + return; + + wfree(s->chain_modifiers); + wfree(s->chain_keycodes); + wfree(s); +} + static void removeShortcutsForMenu(WMenu * menu) { Shortcut *ptr, *tmp; @@ -386,7 +416,7 @@ static void removeShortcutsForMenu(WMenu * menu) while (ptr != NULL) { tmp = ptr->next; if (ptr->menu == menu) { - wfree(ptr); + freeShortcut(ptr); } else { ptr->next = newList; newList = ptr; @@ -400,51 +430,76 @@ static void removeShortcutsForMenu(WMenu * menu) static Bool addShortcut(const char *file, const char *shortcutDefinition, WMenu *menu, WMenuEntry *entry) { Shortcut *ptr; - KeySym ksym; - char *k; - char buf[MAX_SHORTCUT_LENGTH], *b; + char buf[MAX_SHORTCUT_LENGTH]; + char *token, *saveptr; + int step; ptr = wmalloc(sizeof(Shortcut)); - wstrlcpy(buf, shortcutDefinition, MAX_SHORTCUT_LENGTH); - b = (char *)buf; - /* get modifiers */ - ptr->modifier = 0; - while ((k = strchr(b, '+')) != NULL) { - int mod; + /* + * Parse space-separated tokens. + * The first token is the leader key, subsequent tokens are chain steps + */ + step = 0; + token = strtok_r(buf, " ", &saveptr); + while (token != NULL) { + KeySym ksym; + KeyCode kcode; + unsigned int mod = 0; + char tmp[MAX_SHORTCUT_LENGTH]; + char *b, *k; - *k = 0; - mod = wXModifierFromKey(b); - if (mod < 0) { - wwarning(_("%s: invalid key modifier \"%s\""), file, b); - wfree(ptr); + wstrlcpy(tmp, token, MAX_SHORTCUT_LENGTH); + b = tmp; + + while ((k = strchr(b, '+')) != NULL) { + int m; + *k = 0; + m = wXModifierFromKey(b); + if (m < 0) { + wwarning(_("%s: invalid key modifier \"%s\""), file, b); + freeShortcut(ptr); + return False; + } + mod |= m; + b = k + 1; + } + + ksym = XStringToKeysym(b); + if (ksym == NoSymbol) { + wwarning(_("%s: invalid kbd shortcut specification \"%s\" for entry %s"), + file, shortcutDefinition, entry->text); + freeShortcut(ptr); return False; } - ptr->modifier |= mod; - b = k + 1; + kcode = XKeysymToKeycode(dpy, ksym); + if (kcode == 0) { + wwarning(_("%s: invalid key in shortcut \"%s\" for entry %s"), + file, shortcutDefinition, entry->text); + freeShortcut(ptr); + return False; + } + + if (step == 0) { + ptr->modifier = mod; + ptr->keycode = kcode; + } else { + ptr->chain_modifiers = wrealloc(ptr->chain_modifiers, + step * sizeof(unsigned int)); + ptr->chain_keycodes = wrealloc(ptr->chain_keycodes, + step * sizeof(KeyCode)); + ptr->chain_modifiers[step - 1] = mod; + ptr->chain_keycodes[step - 1] = kcode; + } + + step++; + token = strtok_r(NULL, " ", &saveptr); } - /* get key */ - ksym = XStringToKeysym(b); - - if (ksym == NoSymbol) { - wwarning(_("%s:invalid kbd shortcut specification \"%s\" for entry %s"), - file, shortcutDefinition, entry->text); - wfree(ptr); - return False; - } - - ptr->keycode = XKeysymToKeycode(dpy, ksym); - if (ptr->keycode == 0) { - wwarning(_("%s:invalid key in shortcut \"%s\" for entry %s"), file, - shortcutDefinition, entry->text); - wfree(ptr); - return False; - } - - ptr->menu = menu; + ptr->chain_length = (step > 1) ? step : 1; + ptr->menu = menu; ptr->entry = entry; ptr->next = shortcutList; @@ -1563,6 +1618,47 @@ WMenu *configureMenu(WScreen *scr, WMPropList *definition) return menu; } +/* + *---------------------------------------------------------------------- + * wRootMenuReparse-- + * Rebuild the root menu (and its shortcuts / key-grabs) from the + * current WMRootMenu dictionary without mapping the menu. + *---------------------------------------------------------------------- + */ +void wRootMenuReparse(WScreen *scr) +{ + WMenu *menu = NULL; + WMPropList *definition; + + definition = w_global.domain.root_menu->dictionary; + if (!definition) + return; + + scr->flags.root_menu_changed_shortcuts = 0; + scr->flags.added_workspace_menu = 0; + scr->flags.added_windows_menu = 0; + + if (WMIsPLArray(definition)) { + if (!scr->root_menu || + w_global.domain.root_menu->timestamp > scr->root_menu->timestamp) { + menu = configureMenu(scr, definition); + if (menu) + menu->timestamp = w_global.domain.root_menu->timestamp; + } + } else { + menu = configureMenu(scr, definition); + } + + if (menu) { + if (scr->root_menu) + wMenuDestroy(scr->root_menu, True); + scr->root_menu = menu; + } + + if (scr->flags.root_menu_changed_shortcuts) + rebindKeygrabs(scr); +} + /* *---------------------------------------------------------------------- * OpenRootMenu-- diff --git a/src/rootmenu.h b/src/rootmenu.h index 44475b09..69d0aa70 100644 --- a/src/rootmenu.h +++ b/src/rootmenu.h @@ -22,7 +22,8 @@ #ifndef WMROOTMENU_H #define WMROOTMENU_H -Bool wRootMenuPerformShortcut(XEvent * event); +void wRootMenuInsertIntoTree(void); +void wRootMenuReparse(WScreen *scr); void wRootMenuBindShortcuts(Window window); void OpenRootMenu(WScreen * scr, int x, int y, int keyboard); WMenu *configureMenu(WScreen *scr, WMPropList *definition); diff --git a/src/startup.c b/src/startup.c index f74df8f2..7b86746b 100644 --- a/src/startup.c +++ b/src/startup.c @@ -425,6 +425,10 @@ void StartUp(Bool defaultScreenOnly) */ w_global.shortcut.modifiers_mask &= ~(_NumLockMask | _ScrollLockMask); + /* No active key chain at startup */ + w_global.shortcut.curpos = NULL; + w_global.shortcut.chain_timeout_handler = NULL; + memset(&wKeyBindings, 0, sizeof(wKeyBindings)); w_global.context.client_win = XUniqueContext(); @@ -704,6 +708,10 @@ void StartUp(Bool defaultScreenOnly) wSessionRestoreLastWorkspace(wScreen[j]); } + for (j = 0; j < w_global.screen_count; j++) { + wKeyTreeRebuild(wScreen[j]); + } + if (w_global.screen_count == 0) { wfatal(_("could not manage any screen")); Exit(1); diff --git a/src/usermenu.c b/src/usermenu.c index 7bf6ed19..c5542884 100644 --- a/src/usermenu.c +++ b/src/usermenu.c @@ -78,9 +78,6 @@ #include "framewin.h" -#define MAX_SHORTCUT_LENGTH 32 - - typedef struct { WScreen *screen; WShortKey *key; diff --git a/src/window.c b/src/window.c index 1f688cdc..71d14a63 100644 --- a/src/window.c +++ b/src/window.c @@ -2639,6 +2639,11 @@ void wWindowSetKeyGrabs(WWindow * wwin) if (key->keycode == 0) continue; + + /* WKBD_KEYCHAIN_CANCEL is only meaningful while inside an active key chain */ + if (i == WKBD_KEYCHAIN_CANCEL) + continue; + if (key->modifier != AnyModifier) { XGrabKey(dpy, key->keycode, key->modifier | LockMask, wwin->frame->core->window, True, GrabModeAsync, GrabModeAsync);