1
0
mirror of https://github.com/gryf/wmaker.git synced 2026-03-21 02:43:32 +01:00

wmaker: extend default keybinding for multikeys support and add sticky-chain mode

This patch extends the existing keybindings to support multiple keys and
add an optional "sticky chain" mode that lets a prefix remain active until users press
a cancel key so users can enter the continuation key without re-pressing the prefix.

The idea is to bring Emacs shortcuts keybinding to wmaker.

Normal (existing and enhanced) mode:

Prefix behaves like a one-shot release before the next key if any.
For example: Mod1+h -> hide the active application, that is still working as usual.
But if you want for example to have all your window management keys under the same leader key
you can now do something like that:
"Mod4+w h" which is pressing the Super key with w, releasing them and pressing h.
You can assign that key sequence to an action.

Sticky chain mode:

Pressing a configured prefix enters a short-lived sticky state.
Sticky state expires on timeout or when explicitly canceled (with KeychainCancelKey).
For example, you can define:
"Mod4+a x" -> run xterm
"Mod4+a b f" -> run firefox
"Mod4+a b c" -> run google chrome

In sticky mode, "Mod4+a x x b f", then KeychainCancelKey or KeychainTimeoutDelay, will launch 2 xterm and firefox.

New options for WindowMaker conf file:

KeychainTimeoutDelay: timeout in milliseconds (can be set to 0)
Default: 500
Example: KeychainTimeoutDelay = 500;

KeychainCancelKey: explicit keybinding used to cancel an active sticky chain.
If set to None the feature has no dedicated cancel key and the chain only ends by timeout
or naturally if the keybind pressed is not defined.
Default: None
Example: KeychainCancelKey = Escape;
This commit is contained in:
David Maciejak
2026-03-09 20:07:55 -04:00
committed by Carlos R. Mafra
parent ae050ceb40
commit 29177f94ed
14 changed files with 813 additions and 163 deletions

View File

@@ -45,6 +45,8 @@ wmaker_SOURCES = \
icon.c \
icon.h \
keybind.h \
keytree.c \
keytree.h \
main.c \
main.h \
menu.c \

View File

@@ -26,6 +26,7 @@
#include <assert.h>
#include <limits.h>
#include <WINGs/WINGs.h>
#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;

View File

@@ -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;

View File

@@ -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_ */

View File

@@ -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

View File

@@ -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
/* <X11/X.h> 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 */

107
src/keytree.c Normal file
View File

@@ -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 <string.h>
#include <WINGs/WUtil.h>
#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;
}

97
src/keytree.h Normal file
View File

@@ -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 <X11/Xlib.h>
/*
* 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 */

View File

@@ -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)

View File

@@ -60,8 +60,6 @@
#include <WINGs/WUtil.h>
#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--

View File

@@ -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);

View File

@@ -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);

View File

@@ -78,9 +78,6 @@
#include "framewin.h"
#define MAX_SHORTCUT_LENGTH 32
typedef struct {
WScreen *screen;
WShortKey *key;

View File

@@ -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);