1
0
mirror of https://github.com/gryf/wmaker.git synced 2026-03-24 21:53:31 +01:00

3 Commits

Author SHA1 Message Date
David Maciejak
0aeba6064b WPrefs: improve capture_shortcut function
This patch is improving the capture_shortcut function to be able
to capture a sequence of multiple key pressed.
2026-03-14 16:20:49 +00:00
David Maciejak
ec115fedf7 wmaker: revamp titlebar language button
This patch is replacing the modelock legacy hardcoded language dropdown
icons with a compact titlebar button based on the current locale.
(it adds also a detection to xkbfile library which is required to get
the short name of the locale).
Now supports up to 4 layouts, clicking on the language button will cycle
through them (XKB officially supports up to four groups).
2026-03-14 16:20:49 +00:00
David Maciejak
29177f94ed 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;
2026-03-14 16:20:49 +00:00
21 changed files with 1222 additions and 234 deletions

View File

@@ -23,6 +23,8 @@
#include "WPrefs.h" #include "WPrefs.h"
#include <ctype.h> #include <ctype.h>
#include <sys/select.h>
#include <sys/time.h>
#include <X11/keysym.h> #include <X11/keysym.h>
#include <X11/XKBlib.h> #include <X11/XKBlib.h>
@@ -307,14 +309,53 @@ static int NumLockMask(Display *dpy)
return mask; 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) char *capture_shortcut(Display *dpy, Bool *capturing, Bool convert_case)
{ {
XEvent ev; XEvent ev;
KeySym ksym, lksym, uksym; KeySym ksym, lksym, uksym;
char buffer[64]; /* Large enough for several chained chords */
char *key = NULL; char buffer[512];
char keybuf[64];
char *key;
unsigned int numlock_mask; 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) { while (*capturing) {
XAllowEvents(dpy, AsyncKeyboard, CurrentTime); XAllowEvents(dpy, AsyncKeyboard, CurrentTime);
WMNextEvent(dpy, &ev); WMNextEvent(dpy, &ev);
@@ -332,41 +373,62 @@ char *capture_shortcut(Display *dpy, Bool *capturing, Bool convert_case)
key = XKeysymToString(ksym); 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; break;
} }
} }
WMHandleEvent(&ev); 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; return NULL;
buffer[0] = 0; *capturing = 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));
return wstrdup(buffer); return wstrdup(buffer);
} }
@@ -444,7 +506,7 @@ static void captureClick(WMWidget * w, void *data)
} }
panel->capturing = 0; panel->capturing = 0;
WMSetButtonText(w, _("Capture")); 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); 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 */ /* Parameter not used, but tell the compiler that it is ok */
(void) w; (void) w;
/* Cancel any ongoing capture so the keychain loop is unblocked */
panel->capturing = 0;
WMSetTextFieldText(panel->shoT, NULL); WMSetTextFieldText(panel->shoT, NULL);
if (row >= 0) { if (row >= 0) {

View File

@@ -555,7 +555,9 @@ AC_ARG_ENABLE([modelock],
m4_divert_pop([INIT_PREPARE])dnl m4_divert_pop([INIT_PREPARE])dnl
AS_IF([test "x$enable_modelock" = "xyes"], AS_IF([test "x$enable_modelock" = "xyes"],
[AC_DEFINE([XKB_MODELOCK], [1], [whether XKB language MODELOCK should be enabled]) ]) [WM_XEXT_CHECK_XKBFILE
AS_IF([test "x$enable_modelock" = "xyes"],
[AC_DEFINE([XKB_MODELOCK], [1], [whether XKB language MODELOCK should be enabled])])])
dnl XDND Drag-nd-Drop support dnl XDND Drag-nd-Drop support

View File

@@ -232,3 +232,35 @@ AC_DEFUN_ONCE([WM_XEXT_CHECK_XRANDR],
[supported_xext], [LIBXRANDR], [], [-])dnl [supported_xext], [LIBXRANDR], [], [-])dnl
AC_SUBST([LIBXRANDR])dnl AC_SUBST([LIBXRANDR])dnl
]) dnl AC_DEFUN ]) dnl AC_DEFUN
# WM_XEXT_CHECK_XKBFILE
# ---------------------
#
# Check for the XKB File extension library (libxkbfile)
# The check depends on variable 'enable_modelock' being either:
# yes - detect, fail if not found
# no - do not detect, disable support
#
# When found, append appropriate stuff in LIBXKBFILE, and append info to
# the variable 'supported_xext'
# When not found, generate an error because it's required for modelock
AC_DEFUN_ONCE([WM_XEXT_CHECK_XKBFILE],
[WM_LIB_CHECK([XKBFile], [-lxkbfile], [XkbRF_GetNamesProp], [$XLIBS],
[wm_save_CFLAGS="$CFLAGS"
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([dnl
@%:@include <stdio.h>
@%:@include <X11/Xlib.h>
@%:@include <X11/XKBlib.h>
@%:@include <X11/extensions/XKBfile.h>
@%:@include <X11/extensions/XKBrules.h>
], [dnl
Display *dpy = NULL;
XkbRF_VarDefsRec vd;
XkbRF_GetNamesProp(dpy, NULL, &vd);])],
[],
[AC_MSG_ERROR([found $CACHEVAR but cannot compile using XKBfile header])])
CFLAGS="$wm_save_CFLAGS"],
[supported_xext], [LIBXKBFILE], [enable_modelock], [-])dnl
AC_SUBST([LIBXKBFILE])dnl
]) dnl AC_DEFUN

View File

@@ -45,6 +45,8 @@ wmaker_SOURCES = \
icon.c \ icon.c \
icon.h \ icon.h \
keybind.h \ keybind.h \
keytree.c \
keytree.h \
main.c \ main.c \
main.h \ main.h \
menu.c \ menu.c \
@@ -163,6 +165,7 @@ wmaker_LDADD = \
@XLFLAGS@ \ @XLFLAGS@ \
@LIBXRANDR@ \ @LIBXRANDR@ \
@LIBXINERAMA@ \ @LIBXINERAMA@ \
@LIBXKBFILE@ \
@XLIBS@ \ @XLIBS@ \
@LIBM@ \ @LIBM@ \
@INTLIBS@ @INTLIBS@

View File

@@ -26,6 +26,7 @@
#include <assert.h> #include <assert.h>
#include <limits.h> #include <limits.h>
#include <WINGs/WINGs.h> #include <WINGs/WINGs.h>
#include "keytree.h"
/* class codes */ /* 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_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 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 *swtileImage;
RImage *swbackImage[9]; RImage *swbackImage[9];
@@ -649,6 +652,17 @@ extern struct wmaker_global_variables {
* impact the shortcuts (typically: CapsLock, NumLock, ScrollLock) * impact the shortcuts (typically: CapsLock, NumLock, ScrollLock)
*/ */
unsigned int modifiers_mask; 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; } shortcut;
} w_global; } w_global;

View File

@@ -4,7 +4,7 @@
* *
* Copyright (c) 1997-2003 Alfredo K. Kojima * Copyright (c) 1997-2003 Alfredo K. Kojima
* Copyright (c) 1998-2003 Dan Pascu * 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 * This program is free software; you can redistribute it and/or modify
@@ -64,8 +64,7 @@
#include "properties.h" #include "properties.h"
#include "misc.h" #include "misc.h"
#include "winmenu.h" #include "winmenu.h"
#include "rootmenu.h"
#define MAX_SHORTCUT_LENGTH 32
typedef struct _WDefaultEntry WDefaultEntry; typedef struct _WDefaultEntry WDefaultEntry;
typedef int (WDECallbackConvert) (WScreen *scr, WDefaultEntry *entry, WMPropList *plvalue, void *addr, void **tdata); 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}, &wPreferences.window_list_app_icons, getBool, NULL, NULL, NULL},
{"MouseWheelFocus", "NO", NULL, {"MouseWheelFocus", "NO", NULL,
&wPreferences.mouse_wheel_focus, getBool, NULL, NULL, NULL}, &wPreferences.mouse_wheel_focus, getBool, NULL, NULL, NULL},
{"KeychainTimeoutDelay", "500", NULL,
&wPreferences.keychain_timeout_delay, getInt, NULL, NULL, NULL},
/* style options */ /* style options */
@@ -648,6 +649,8 @@ WDefaultEntry optionList[] = {
NULL, getKeybind, setKeyGrab, NULL, NULL}, NULL, getKeybind, setKeyGrab, NULL, NULL},
{"WindowMenuKey", "Control+Escape", (void *)WKBD_WINDOWMENU, {"WindowMenuKey", "Control+Escape", (void *)WKBD_WINDOWMENU,
NULL, getKeybind, setKeyGrab, NULL, NULL}, NULL, getKeybind, setKeyGrab, NULL, NULL},
{"KeychainCancelKey", "None", (void *)WKBD_KEYCHAIN_CANCEL,
NULL, getKeybind, setKeyGrab, NULL, NULL},
{"DockRaiseLowerKey", "None", (void*)WKBD_DOCKRAISELOWER, {"DockRaiseLowerKey", "None", (void*)WKBD_DOCKRAISELOWER,
NULL, getKeybind, setKeyGrab, NULL, NULL}, NULL, getKeybind, setKeyGrab, NULL, NULL},
{"ClipRaiseLowerKey", "None", (void *)WKBD_CLIPRAISELOWER, {"ClipRaiseLowerKey", "None", (void *)WKBD_CLIPRAISELOWER,
@@ -1174,6 +1177,13 @@ void wDefaultsCheckDomains(void* arg)
wwarning(_("could not load domain %s from user defaults database"), "WMRootMenu"); wwarning(_("could not load domain %s from user defaults database"), "WMRootMenu");
} }
w_global.domain.root_menu->timestamp = stbuf.st_mtime; 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 #ifndef HAVE_INOTIFY
if (!arg) if (!arg)
@@ -1243,7 +1253,6 @@ void wReadDefaults(WScreen * scr, WMPropList * new_dict)
if (entry->update) if (entry->update)
needs_refresh |= (*entry->update) (scr, entry, tdata, entry->extra_data); needs_refresh |= (*entry->update) (scr, entry, tdata, entry->extra_data);
} }
} }
} }
@@ -1314,6 +1323,7 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict)
{ {
WDefaultEntry *entry; WDefaultEntry *entry;
unsigned int i; unsigned int i;
Bool keybindings_changed = False;
void *tdata; void *tdata;
for (i = 0; i < wlengthof(optionList); i++) { for (i = 0; i < wlengthof(optionList); i++) {
@@ -1326,11 +1336,37 @@ void wReadKeybindings(WScreen *scr, WMPropList *dict)
plvalue = entry->plvalue; plvalue = entry->plvalue;
if (plvalue) { if (plvalue) {
int ok = (*entry->convert)(scr, entry, plvalue, entry->addr, &tdata); 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); (*entry->update)(scr, entry, tdata, entry->extra_data);
if (binding_changed)
keybindings_changed = True;
} }
} }
}
} }
if (keybindings_changed)
wKeyTreeRebuild(scr);
} }
void wDefaultUpdateIcons(WScreen *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 ----------------------- */ /* --------------------------- Local ----------------------- */
#define GET_STRING_OR_DEFAULT(x, var) if (!WMIsPLString(value)) { \ #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; 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 int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value, void *addr, void **ret)
{ {
static WShortKey shortcut; static WShortKey shortcut;
KeySym ksym;
const char *val; const char *val;
char *k; char buf[MAX_SHORTCUT_LENGTH];
char buf[MAX_SHORTCUT_LENGTH], *b; char *token, *saveptr;
int step;
/* Parameter not used, but tell the compiler that it is ok */ /* Parameter not used, but tell the compiler that it is ok */
(void) scr; (void) scr;
@@ -2224,9 +2349,11 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value,
GET_STRING_OR_DEFAULT("Key spec", val); GET_STRING_OR_DEFAULT("Key spec", val);
/* Free old chain arrays before overwriting */
wShortKeyFree(&shortcut);
if (!val || strcasecmp(val, "NONE") == 0) { if (!val || strcasecmp(val, "NONE") == 0) {
shortcut.keycode = 0; shortcut.chain_length = 1;
shortcut.modifier = 0;
if (ret) if (ret)
*ret = &shortcut; *ret = &shortcut;
return True; return True;
@@ -2234,37 +2361,36 @@ static int getKeybind(WScreen * scr, WDefaultEntry * entry, WMPropList * value,
wstrlcpy(buf, val, MAX_SHORTCUT_LENGTH); 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 */ if (!parseOneKey(entry, token, &mod, &kcode))
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);
return False; 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; step++;
token = strtok_r(NULL, " ", &saveptr);
b = k + 1;
} }
/* get key */ shortcut.chain_length = (step > 1) ? step : 1;
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;
}
if (ret) if (ret)
*ret = &shortcut; *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 */ /* Parameter not used, but tell the compiler that it is ok */
(void) entry; (void) entry;
/* Free old chain arrays before overwriting */
wShortKeyFree(&wKeyBindings[widx]);
/* Shallow copy, then deep-copy the heap arrays */
wKeyBindings[widx] = *shortcut; 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; 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); RImage *get_rimage_from_file(WScreen *scr, const char *file_name, int max_size);
void wDefaultPurgeInfo(const char *instance, const char *class); 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_ */ #endif /* WMDEFAULTS_H_ */

View File

@@ -384,6 +384,12 @@ static void handle_inotify_events(void)
/* move to next event in the buffer */ /* move to next event in the buffer */
i += sizeof(struct inotify_event) + pevent->len; 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 */ #endif /* HAVE_INOTIFY */
@@ -587,10 +593,12 @@ static void handleExtensions(XEvent * event)
handleShapeNotify(event); handleShapeNotify(event);
} }
#endif #endif
if (w_global.xext.xkb.supported && event->type == w_global.xext.xkb.event_base) { if (w_global.xext.xkb.supported && event->type >= w_global.xext.xkb.event_base
&& event->type <= w_global.xext.xkb.event_base + 255) {
XkbEvent *xkbevent = (XkbEvent *) event; XkbEvent *xkbevent = (XkbEvent *) event;
int xkb_type = xkbevent->any.xkb_type;
if (xkbevent->any.xkb_type == XkbNewKeyboardNotify) { if (xkb_type == XkbNewKeyboardNotify) {
int j; int j;
WScreen *scr; WScreen *scr;
@@ -601,8 +609,10 @@ static void handleExtensions(XEvent * event)
} }
#ifdef KEEP_XKB_LOCK_STATUS #ifdef KEEP_XKB_LOCK_STATUS
else { else {
if (wPreferences.modelock && (xkbevent->any.xkb_type == XkbIndicatorStateNotify)) { /* Listen not only for IndicatorStateNotify but also for StateNotify
handleXkbIndicatorStateNotify((XkbEvent *) event); * which is commonly emitted on group (layout) changes. */
if (wPreferences.modelock && (xkb_type == XkbIndicatorStateNotify || xkb_type == XkbStateNotify)) {
handleXkbIndicatorStateNotify(xkbevent);
} }
} }
#endif /*KEEP_XKB_LOCK_STATUS */ #endif /*KEEP_XKB_LOCK_STATUS */
@@ -1318,6 +1328,8 @@ static void handleXkbIndicatorStateNotify(XkbEvent *event)
if (wwin->frame->languagemode != staterec.group) { if (wwin->frame->languagemode != staterec.group) {
wwin->frame->last_languagemode = wwin->frame->languagemode; wwin->frame->last_languagemode = wwin->frame->languagemode;
wwin->frame->languagemode = staterec.group; wwin->frame->languagemode = staterec.group;
wWindowGetLanguageLabel(wwin->frame->languagemode, wwin->frame->language_label);
wFrameWindowUpdateLanguageButton(wwin->frame);
} }
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
if (wwin->frame->titlebar) { if (wwin->frame->titlebar) {
@@ -1422,56 +1434,55 @@ static int CheckFullScreenWindowFocused(WScreen * scr)
return 0; 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); (void)data;
WWindow *wwin = scr->focused_window;
short i, widx; XUngrabKeyboard(dpy, CurrentTime);
int modifiers; w_global.shortcut.curpos = NULL;
int command = -1; 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 #ifdef KEEP_XKB_LOCK_STATUS
XkbStateRec staterec; XkbStateRec staterec;
#endif /*KEEP_XKB_LOCK_STATUS */ #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) { switch (command) {
case WKBD_ROOTMENU: case WKBD_ROOTMENU:
@@ -1962,7 +1973,8 @@ static void handleKeyPress(XEvent * event)
wwin->frame->languagemode = wwin->frame->last_languagemode; wwin->frame->languagemode = wwin->frame->last_languagemode;
wwin->frame->last_languagemode = staterec.group; wwin->frame->last_languagemode = staterec.group;
XkbLockGroup(dpy, XkbUseCoreKbd, wwin->frame->languagemode); XkbLockGroup(dpy, XkbUseCoreKbd, wwin->frame->languagemode);
/* Update the language label text */
wWindowGetLanguageLabel(wwin->frame->languagemode, wwin->frame->language_label);
} }
} }
break; break;
@@ -1970,6 +1982,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_NONE 0
#define CORNER_TOPLEFT 1 #define CORNER_TOPLEFT 1
#define CORNER_TOPRIGHT 2 #define CORNER_TOPRIGHT 2

View File

@@ -54,7 +54,7 @@ static void resizebarMouseDown(WObjDescriptor * desc, XEvent * event);
static void checkTitleSize(WFrameWindow * fwin); static void checkTitleSize(WFrameWindow * fwin);
static void paintButton(WCoreWindow * button, WTexture * texture, static void paintButton(WCoreWindow * button, WTexture * texture,
unsigned long color, WPixmap * image, int pushed); unsigned long color, WPixmap * image, int pushed, int from_xpm);
static void updateTitlebar(WFrameWindow * fwin); static void updateTitlebar(WFrameWindow * fwin);
@@ -98,6 +98,7 @@ WFrameWindow *wFrameWindowCreate(WScreen * scr, int wlevel, int x, int y,
#ifdef KEEP_XKB_LOCK_STATUS #ifdef KEEP_XKB_LOCK_STATUS
fwin->languagemode = XkbGroup1Index; fwin->languagemode = XkbGroup1Index;
fwin->last_languagemode = XkbGroup2Index; fwin->last_languagemode = XkbGroup2Index;
wWindowGetLanguageLabel(fwin->languagemode, fwin->language_label);
#endif #endif
fwin->depth = depth; fwin->depth = depth;
@@ -145,6 +146,7 @@ void wFrameWindowUpdateBorders(WFrameWindow * fwin, int flags)
theight = *fwin->title_min_height; theight = *fwin->title_min_height;
} else { } else {
theight = 0; theight = 0;
fwin->flags.titlebar = 0;
} }
if (wPreferences.new_style == TS_NEW) { if (wPreferences.new_style == TS_NEW) {
@@ -536,6 +538,8 @@ static void updateTitlebar(WFrameWindow * fwin)
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
else { else {
int bsize = theight - 7; int bsize = theight - 7;
if (wPreferences.new_style == TS_NEXT)
bsize -= 1;
if (fwin->flags.hide_left_button || !fwin->left_button || fwin->flags.lbutton_dont_fit) { if (fwin->flags.hide_left_button || !fwin->left_button || fwin->flags.lbutton_dont_fit) {
if (fwin->language_button) if (fwin->language_button)
wCoreConfigure(fwin->language_button, 3, (theight - bsize) / 2, wCoreConfigure(fwin->language_button, 3, (theight - bsize) / 2,
@@ -950,6 +954,10 @@ void wFrameWindowPaint(WFrameWindow * fwin)
remakeTexture(fwin, i); remakeTexture(fwin, i);
} }
} }
#ifdef XKB_BUTTON_HINT
if (wPreferences.modelock)
wFrameWindowUpdateLanguageButton(fwin);
#endif
} }
if (fwin->flags.need_texture_change) { if (fwin->flags.need_texture_change) {
@@ -1023,7 +1031,8 @@ void wFrameWindowPaint(WFrameWindow * fwin)
allButtons = 0; allButtons = 0;
} }
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
fwin->languagebutton_image = scr->b_pixmaps[WBUT_XKBGROUP1 + fwin->languagemode]; if (fwin->flags.language_button && !fwin->languagebutton_image[0])
wFrameWindowUpdateLanguageButton(fwin);
#endif #endif
if (fwin->title) { if (fwin->title) {
@@ -1228,10 +1237,145 @@ int wFrameWindowChangeTitle(WFrameWindow *fwin, const char *new_title)
} }
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
static int wFrameWindowSetLanguageButtonImages(WFrameWindow *fwin, Pixmap *pixmaps, int count)
{
int i;
for (i = 0; i < count; i++) {
WPixmap *wp = wPixmapCreate(pixmaps[i], None);
if (!wp) {
int j;
XFreePixmap(dpy, pixmaps[i]);
for (j = i + 1; j < count; j++)
XFreePixmap(dpy, pixmaps[j]);
return 0;
}
wp->client_owned = 0;
wp->client_owned_mask = 0;
if (fwin->languagebutton_image[i] && !fwin->languagebutton_image[i]->shared)
wPixmapDestroy(fwin->languagebutton_image[i]);
fwin->languagebutton_image[i] = wp;
}
return 1;
}
void wFrameWindowUpdateLanguageButton(WFrameWindow * fwin) void wFrameWindowUpdateLanguageButton(WFrameWindow * fwin)
{ {
paintButton(fwin->language_button, fwin->title_texture[fwin->flags.state], WScreen *scr = fwin->screen_ptr;
WMColorPixel(fwin->title_color[fwin->flags.state]), fwin->languagebutton_image, True); WCoreWindow *button = fwin->language_button;
GC gc = scr->copy_gc;
int i, text_width, text_height;
int border_thickness = 3;
int key_width, key_height;
int group_index;
Pixmap tmp[2]; /* focused, unfocused */
if (!fwin->flags.titlebar || fwin->core->descriptor.parent_type != WCLASS_WINDOW)
return;
if (!button || fwin->language_label[0] == '\0')
return;
group_index = fwin->languagemode;
if (group_index < 0 || group_index >= 4)
return;
/* Calculate text dimensions */
int small_px = WMFontHeight(*fwin->font) * 55 / 100;
WMFont *small = WMBoldSystemFontOfSize(scr->wmscreen, small_px);
text_width = WMWidthOfString(small, fwin->language_label, strlen(fwin->language_label));
text_height = WMFontHeight(small);
key_width = button->width - border_thickness;
key_height = button->height - border_thickness;
/* Ensure dimensions are valid */
if (key_width < 1 || key_height < 1) {
WMReleaseFont(small);
return;
}
/* Create temporary pixmaps for immediate drawing */
tmp[0] = XCreatePixmap(dpy, button->window, 2*key_width, key_height, scr->w_depth);
if (tmp[0] == None) {
WMReleaseFont(small);
return;
}
tmp[1] = None;
if (wPreferences.new_style == TS_NEW) {
tmp[1] = XCreatePixmap(dpy, button->window, 2*key_width, key_height, scr->w_depth);
if (tmp[1] == None) {
XFreePixmap(dpy, tmp[0]);
WMReleaseFont(small);
return;
}
}
/* Reset GC to ensure clean state for drawing */
XSetClipMask(dpy, gc, None);
/* Draw the language label centered in the button */
int text_x = (key_width - text_width) / 2;
int text_y = (key_height - text_height) / 2;
/* Fill the color pixmap depending on the style */
if (wPreferences.new_style == TS_NEW) {
for (i = 0; i < 2; i++) {
if (fwin->title_texture[i]->any.type != WTEX_SOLID && fwin->title_back[i] != None) {
XCopyArea(dpy, fwin->languagebutton_back[i], tmp[i], scr->copy_gc,
0, 0, button->width, button->height, -1, -1);
} else {
unsigned long bg_pixel = fwin->title_texture[i]->solid.normal.pixel;
XSetForeground(dpy, gc, bg_pixel);
XFillRectangle(dpy, tmp[i], gc, 0, 0, key_width, key_height);
}
WMDrawString(scr->wmscreen, tmp[i], fwin->title_color[i],
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
}
} else if (wPreferences.new_style == TS_OLD) {
unsigned long bg_pixel = scr->widget_texture->normal.pixel;
XSetForeground(dpy, gc, bg_pixel);
XFillRectangle(dpy, tmp[0], gc, 0, 0, key_width, key_height);
WMDrawString(scr->wmscreen, tmp[0], WMBlackColor(scr->wmscreen),
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
} else {
unsigned long bg_pixel = scr->widget_texture->dark.pixel;
XSetForeground(dpy, gc, bg_pixel);
XFillRectangle(dpy, tmp[0], gc, 0, 0, key_width, key_height);
WMColor *silver = WMCreateRGBColor(scr->wmscreen, 0xc0c0, 0xc0c0, 0xc0c0, True);
WMDrawString(scr->wmscreen, tmp[0], silver,
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
WMReleaseColor(silver);
}
/* pushed button next to normal for easy access when painting */
text_x = key_width + (key_width - text_width) / 2;
if (wPreferences.new_style == TS_NEW) {
XSetForeground(dpy, gc, scr->white_pixel);
for (i = 0; i < 2; i++) {
XFillRectangle(dpy, tmp[i], gc, key_width, 0, key_width, key_height);
WMDrawString(scr->wmscreen, tmp[i], WMBlackColor(scr->wmscreen),
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
}
} else if (wPreferences.new_style == TS_OLD) {
XSetForeground(dpy, gc, scr->white_pixel);
XFillRectangle(dpy, tmp[0], gc, key_width, 0, key_width, key_height);
WMDrawString(scr->wmscreen, tmp[0], WMDarkGrayColor(scr->wmscreen),
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
} else {
unsigned long bg_pixel = scr->widget_texture->dark.pixel;
WMColor *silver = WMCreateRGBColor(scr->wmscreen, 0xc0c0, 0xc0c0, 0xc0c0, True);
XSetForeground(dpy, gc, bg_pixel);
XFillRectangle(dpy, tmp[0], gc, key_width, 0, key_width, key_height);
WMDrawString(scr->wmscreen, tmp[0], silver,
small, text_x, text_y, fwin->language_label, strlen(fwin->language_label));
WMReleaseColor(silver);
}
WMReleaseFont(small);
wFrameWindowSetLanguageButtonImages(fwin, tmp, (wPreferences.new_style == TS_NEW) ? 2 : 1);
} }
#endif /* XKB_BUTTON_HINT */ #endif /* XKB_BUTTON_HINT */
@@ -1286,7 +1430,7 @@ static void checkTitleSize(WFrameWindow * fwin)
fwin->flags.incomplete_title = 0; fwin->flags.incomplete_title = 0;
} }
static void paintButton(WCoreWindow * button, WTexture * texture, unsigned long color, WPixmap * image, int pushed) static void paintButton(WCoreWindow * button, WTexture * texture, unsigned long color, WPixmap * image, int pushed, int from_xpm)
{ {
WScreen *scr = button->screen_ptr; WScreen *scr = button->screen_ptr;
GC copy_gc = scr->copy_gc; GC copy_gc = scr->copy_gc;
@@ -1364,8 +1508,12 @@ static void paintButton(WCoreWindow * button, WTexture * texture, unsigned long
} else { } else {
if (wPreferences.new_style == TS_OLD) { if (wPreferences.new_style == TS_OLD) {
XSetForeground(dpy, copy_gc, scr->dark_pixel); XSetForeground(dpy, copy_gc, scr->dark_pixel);
XFillRectangle(dpy, button->window, copy_gc, 0, 0, if (from_xpm)
button->width, button->height); XFillRectangle(dpy, button->window, copy_gc, 0, 0,
button->width, button->height);
else
XCopyArea(dpy, image->image, button->window, copy_gc,
left, 0, width, image->height, x, y);
} else { } else {
XSetForeground(dpy, copy_gc, scr->black_pixel); XSetForeground(dpy, copy_gc, scr->black_pixel);
XCopyArea(dpy, image->image, button->window, copy_gc, XCopyArea(dpy, image->image, button->window, copy_gc,
@@ -1379,7 +1527,11 @@ static void paintButton(WCoreWindow * button, WTexture * texture, unsigned long
XSetForeground(dpy, copy_gc, color); XSetForeground(dpy, copy_gc, color);
XSetBackground(dpy, copy_gc, texture->any.color.pixel); XSetBackground(dpy, copy_gc, texture->any.color.pixel);
} }
XFillRectangle(dpy, button->window, copy_gc, 0, 0, button->width, button->height); if (from_xpm)
XFillRectangle(dpy, button->window, copy_gc, 0, 0, button->width, button->height);
else
XCopyArea(dpy, image->image, button->window, copy_gc,
left, 0, width, image->height, x, y);
} }
} }
} }
@@ -1394,18 +1546,23 @@ static void handleButtonExpose(WObjDescriptor * desc, XEvent * event)
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
if (button == fwin->language_button) { if (button == fwin->language_button) {
if (!fwin->flags.hide_language_button) if (!fwin->flags.hide_language_button) {
/* map focused and pfocused states to focus language button image */
int lb_index = wPreferences.new_style == TS_NEW && (fwin->flags.state == 1) ? 1 : 0;
paintButton(button, fwin->title_texture[fwin->flags.state], paintButton(button, fwin->title_texture[fwin->flags.state],
WMColorPixel(fwin->title_color[fwin->flags.state]), WMColorPixel(fwin->title_color[fwin->flags.state]),
fwin->languagebutton_image, False); fwin->languagebutton_image[lb_index],
False, False);
}
} else } else
#endif #endif
if (button == fwin->left_button) if (button == fwin->left_button)
paintButton(button, fwin->title_texture[fwin->flags.state], paintButton(button, fwin->title_texture[fwin->flags.state],
WMColorPixel(fwin->title_color[fwin->flags.state]), fwin->lbutton_image, False); WMColorPixel(fwin->title_color[fwin->flags.state]), fwin->lbutton_image, False, True);
else else
paintButton(button, fwin->title_texture[fwin->flags.state], paintButton(button, fwin->title_texture[fwin->flags.state],
WMColorPixel(fwin->title_color[fwin->flags.state]), fwin->rbutton_image, False); WMColorPixel(fwin->title_color[fwin->flags.state]), fwin->rbutton_image, False, True);
} }
static void titlebarMouseDown(WObjDescriptor * desc, XEvent * event) static void titlebarMouseDown(WObjDescriptor * desc, XEvent * event)
@@ -1437,7 +1594,7 @@ static void buttonMouseDown(WObjDescriptor * desc, XEvent * event)
WCoreWindow *button = desc->self; WCoreWindow *button = desc->self;
WPixmap *image; WPixmap *image;
XEvent ev; XEvent ev;
int done = 0, execute = 1; int done = 0, execute = 1, from_xpm = True;
WTexture *texture; WTexture *texture;
unsigned long pixel; unsigned long pixel;
int clickButton = event->xbutton.button; int clickButton = event->xbutton.button;
@@ -1458,13 +1615,18 @@ static void buttonMouseDown(WObjDescriptor * desc, XEvent * event)
if (button == fwin->language_button) { if (button == fwin->language_button) {
if (!wPreferences.modelock) if (!wPreferences.modelock)
return; return;
image = fwin->languagebutton_image;
/* map focused and pfocused states to focus language button image */
int lb_index = wPreferences.new_style == TS_NEW && (fwin->flags.state == 1) ? 1 : 0;
image = fwin->languagebutton_image[lb_index];
from_xpm = False;
} }
#endif #endif
pixel = WMColorPixel(fwin->title_color[fwin->flags.state]); pixel = WMColorPixel(fwin->title_color[fwin->flags.state]);
texture = fwin->title_texture[fwin->flags.state]; texture = fwin->title_texture[fwin->flags.state];
paintButton(button, texture, pixel, image, True); paintButton(button, texture, pixel, image, True, from_xpm);
while (!done) { while (!done) {
WMMaskEvent(dpy, LeaveWindowMask | EnterWindowMask | ButtonReleaseMask WMMaskEvent(dpy, LeaveWindowMask | EnterWindowMask | ButtonReleaseMask
@@ -1472,12 +1634,12 @@ static void buttonMouseDown(WObjDescriptor * desc, XEvent * event)
switch (ev.type) { switch (ev.type) {
case LeaveNotify: case LeaveNotify:
execute = 0; execute = 0;
paintButton(button, texture, pixel, image, False); paintButton(button, texture, pixel, image, False, from_xpm);
break; break;
case EnterNotify: case EnterNotify:
execute = 1; execute = 1;
paintButton(button, texture, pixel, image, True); paintButton(button, texture, pixel, image, True, from_xpm);
break; break;
case ButtonPress: case ButtonPress:
@@ -1492,7 +1654,7 @@ static void buttonMouseDown(WObjDescriptor * desc, XEvent * event)
WMHandleEvent(&ev); WMHandleEvent(&ev);
} }
} }
paintButton(button, texture, pixel, image, False); paintButton(button, texture, pixel, image, False, from_xpm);
if (execute) { if (execute) {
if (button == fwin->left_button) { if (button == fwin->left_button) {

View File

@@ -80,7 +80,7 @@ typedef struct WFrameWindow {
WPixmap *lbutton_image; WPixmap *lbutton_image;
WPixmap *rbutton_image; WPixmap *rbutton_image;
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
WPixmap *languagebutton_image; WPixmap *languagebutton_image[2]; /* focused, unfocused */
#endif #endif
union WTexture **title_texture; union WTexture **title_texture;
@@ -93,6 +93,7 @@ typedef struct WFrameWindow {
#ifdef KEEP_XKB_LOCK_STATUS #ifdef KEEP_XKB_LOCK_STATUS
int languagemode; int languagemode;
int last_languagemode; int last_languagemode;
char language_label[3]; /* 2-letter language code */
#endif /* KEEP_XKB_LOCK_STATUS */ #endif /* KEEP_XKB_LOCK_STATUS */
/* thing that uses this frame. passed as data to callbacks */ /* thing that uses this frame. passed as data to callbacks */

View File

@@ -2,7 +2,7 @@
* Window Maker window manager * Window Maker window manager
* *
* Copyright (c) 1997-2003 Alfredo K. Kojima * 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 * 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 * it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@
#ifndef WMKEYBIND_H #ifndef WMKEYBIND_H
#define WMKEYBIND_H #define WMKEYBIND_H
#define MAX_SHORTCUT_LENGTH 64
/* <X11/X.h> doesn't define these, even though XFree supports them */ /* <X11/X.h> doesn't define these, even though XFree supports them */
#ifndef Button6 #ifndef Button6
#define Button6 6 #define Button6 6
@@ -44,6 +46,7 @@ enum {
WKBD_ROOTMENU, WKBD_ROOTMENU,
WKBD_WINDOWMENU, WKBD_WINDOWMENU,
WKBD_WINDOWLIST, WKBD_WINDOWLIST,
WKBD_KEYCHAIN_CANCEL,
/* window */ /* window */
WKBD_MINIATURIZE, WKBD_MINIATURIZE,
@@ -166,8 +169,13 @@ enum {
}; };
typedef struct WShortKey { typedef struct WShortKey {
unsigned int modifier; unsigned int modifier; /* leader (or only) key modifier - always valid */
KeyCode keycode; 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; } WShortKey;
/* ---[ Global Variables ]------------------------------------------------ */ /* ---[ Global Variables ]------------------------------------------------ */
@@ -177,5 +185,6 @@ extern WShortKey wKeyBindings[WKBD_LAST];
/* ---[ Functions ]------------------------------------------------------- */ /* ---[ Functions ]------------------------------------------------------- */
void wKeyboardInitialize(void); void wKeyboardInitialize(void);
void wShortKeyFree(WShortKey *key);
#endif /* WMKEYBIND_H */ #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) char *GetShortcutKey(WShortKey key)
{ {
const char *key_name; char buf[MAX_SHORTCUT_LENGTH];
char buffer[256]; char *wr, *result, *seg;
char *wr; int step;
void append_string(const char *text) void append_string(const char *text)
{ {
const char *string = text; const char *s = text;
while (*string) { while (*s) {
if (wr >= buffer + sizeof(buffer) - 1) if (wr >= buf + sizeof(buf) - 1)
break; break;
*wr++ = *string++; *wr++ = *s++;
} }
} }
void append_modifier(int modifier_index, const char *fallback_name) 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]); append_string(wPreferences.modifier_labels[modifier_index]);
} else { else
append_string(fallback_name); append_string(fallback_name);
}
} }
key_name = XKeysymToString(W_KeycodeToKeysym(dpy, key.keycode, 0)); Bool build_token(unsigned int mod, KeyCode kcode)
if (!key_name) {
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; return NULL;
wr = buffer; /* Convert the leader token to its display string */
if (key.modifier & ControlMask) append_modifier(1, "Control+"); result = GetShortcutString(buf);
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';
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) char *EscapeWM_CLASS(const char *name, const char *class)

View File

@@ -60,8 +60,6 @@
#include <WINGs/WUtil.h> #include <WINGs/WUtil.h>
#define MAX_SHORTCUT_LENGTH 32
static WMenu *readMenuPipe(WScreen * scr, char **file_name); static WMenu *readMenuPipe(WScreen * scr, char **file_name);
static WMenu *readPLMenuPipe(WScreen * scr, char **file_name); static WMenu *readPLMenuPipe(WScreen * scr, char **file_name);
static WMenu *readMenuFile(WScreen *scr, const char *file_name); static WMenu *readMenuFile(WScreen *scr, const char *file_name);
@@ -75,6 +73,11 @@ typedef struct Shortcut {
KeyCode keycode; KeyCode keycode;
WMenuEntry *entry; WMenuEntry *entry;
WMenu *menu; 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; } Shortcut;
static Shortcut *shortcutList = NULL; static Shortcut *shortcutList = NULL;
@@ -320,27 +323,44 @@ static char *getLocalizedMenuFile(const char *menu)
return NULL; 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; 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) { 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; continue;
if (ptr->keycode == event->xkey.keycode && ptr->modifier == modifiers) { len = (ptr->chain_length > 1) ? ptr->chain_length : 1;
(*ptr->entry->callback) (ptr->menu, ptr->entry); mods = wmalloc(len * sizeof(unsigned int));
done = True; 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) 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) static void removeShortcutsForMenu(WMenu * menu)
{ {
Shortcut *ptr, *tmp; Shortcut *ptr, *tmp;
@@ -386,7 +416,7 @@ static void removeShortcutsForMenu(WMenu * menu)
while (ptr != NULL) { while (ptr != NULL) {
tmp = ptr->next; tmp = ptr->next;
if (ptr->menu == menu) { if (ptr->menu == menu) {
wfree(ptr); freeShortcut(ptr);
} else { } else {
ptr->next = newList; ptr->next = newList;
newList = ptr; newList = ptr;
@@ -400,51 +430,76 @@ static void removeShortcutsForMenu(WMenu * menu)
static Bool addShortcut(const char *file, const char *shortcutDefinition, WMenu *menu, WMenuEntry *entry) static Bool addShortcut(const char *file, const char *shortcutDefinition, WMenu *menu, WMenuEntry *entry)
{ {
Shortcut *ptr; Shortcut *ptr;
KeySym ksym; char buf[MAX_SHORTCUT_LENGTH];
char *k; char *token, *saveptr;
char buf[MAX_SHORTCUT_LENGTH], *b; int step;
ptr = wmalloc(sizeof(Shortcut)); ptr = wmalloc(sizeof(Shortcut));
wstrlcpy(buf, shortcutDefinition, MAX_SHORTCUT_LENGTH); wstrlcpy(buf, shortcutDefinition, MAX_SHORTCUT_LENGTH);
b = (char *)buf;
/* get modifiers */ /*
ptr->modifier = 0; * Parse space-separated tokens.
while ((k = strchr(b, '+')) != NULL) { * The first token is the leader key, subsequent tokens are chain steps
int mod; */
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; wstrlcpy(tmp, token, MAX_SHORTCUT_LENGTH);
mod = wXModifierFromKey(b); b = tmp;
if (mod < 0) {
wwarning(_("%s: invalid key modifier \"%s\""), file, b); while ((k = strchr(b, '+')) != NULL) {
wfree(ptr); 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; 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 */ ptr->chain_length = (step > 1) ? step : 1;
ksym = XStringToKeysym(b); ptr->menu = menu;
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->entry = entry; ptr->entry = entry;
ptr->next = shortcutList; ptr->next = shortcutList;
@@ -1563,6 +1618,47 @@ WMenu *configureMenu(WScreen *scr, WMPropList *definition)
return menu; 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-- * OpenRootMenu--

View File

@@ -22,7 +22,8 @@
#ifndef WMROOTMENU_H #ifndef WMROOTMENU_H
#define WMROOTMENU_H #define WMROOTMENU_H
Bool wRootMenuPerformShortcut(XEvent * event); void wRootMenuInsertIntoTree(void);
void wRootMenuReparse(WScreen *scr);
void wRootMenuBindShortcuts(Window window); void wRootMenuBindShortcuts(Window window);
void OpenRootMenu(WScreen * scr, int x, int y, int keyboard); void OpenRootMenu(WScreen * scr, int x, int y, int keyboard);
WMenu *configureMenu(WScreen *scr, WMPropList *definition); WMenu *configureMenu(WScreen *scr, WMPropList *definition);

View File

@@ -658,11 +658,10 @@ WScreen *wScreenInit(int screen_number)
XSelectInput(dpy, scr->root_win, event_mask); XSelectInput(dpy, scr->root_win, event_mask);
#ifdef KEEP_XKB_LOCK_STATUS #ifdef KEEP_XKB_LOCK_STATUS
/* Only GroupLock doesn't work correctly in my system since right-alt
* can change mode while holding it too - ]d
*/
if (w_global.xext.xkb.supported) if (w_global.xext.xkb.supported)
XkbSelectEvents(dpy, XkbUseCoreKbd, XkbIndicatorStateNotifyMask|XkbNewKeyboardNotifyMask, XkbIndicatorStateNotifyMask|XkbNewKeyboardNotifyMask); XkbSelectEvents(dpy, XkbUseCoreKbd,
XkbIndicatorStateNotifyMask | XkbStateNotifyMask | XkbNewKeyboardNotifyMask,
XkbIndicatorStateNotifyMask | XkbStateNotifyMask | XkbNewKeyboardNotifyMask);
#else #else
if (w_global.xext.xkb.supported) if (w_global.xext.xkb.supported)
XkbSelectEvents(dpy, XkbUseCoreKbd, XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask); XkbSelectEvents(dpy, XkbUseCoreKbd, XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask);

View File

@@ -425,6 +425,10 @@ void StartUp(Bool defaultScreenOnly)
*/ */
w_global.shortcut.modifiers_mask &= ~(_NumLockMask | _ScrollLockMask); 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)); memset(&wKeyBindings, 0, sizeof(wKeyBindings));
w_global.context.client_win = XUniqueContext(); w_global.context.client_win = XUniqueContext();
@@ -704,6 +708,10 @@ void StartUp(Bool defaultScreenOnly)
wSessionRestoreLastWorkspace(wScreen[j]); wSessionRestoreLastWorkspace(wScreen[j]);
} }
for (j = 0; j < w_global.screen_count; j++) {
wKeyTreeRebuild(wScreen[j]);
}
if (w_global.screen_count == 0) { if (w_global.screen_count == 0) {
wfatal(_("could not manage any screen")); wfatal(_("could not manage any screen"));
Exit(1); Exit(1);

View File

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

View File

@@ -24,6 +24,7 @@
#include <X11/Xlib.h> #include <X11/Xlib.h>
#include <X11/Xutil.h> #include <X11/Xutil.h>
#include <X11/Xatom.h>
#ifdef USE_XSHAPE #ifdef USE_XSHAPE
#include <X11/extensions/shape.h> #include <X11/extensions/shape.h>
#endif #endif
@@ -38,6 +39,12 @@
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <math.h> #include <math.h>
#include <ctype.h>
#ifdef XKB_BUTTON_HINT
#include <X11/extensions/XKBfile.h> // Required for XkbRF_VarDefsRec
#include <X11/extensions/XKBrules.h> // Required for XkbRF_GetNamesProp
#endif
/* For getting mouse wheel mappings from WINGs */ /* For getting mouse wheel mappings from WINGs */
#include <WINGs/WINGs.h> #include <WINGs/WINGs.h>
@@ -2304,14 +2311,6 @@ void wWindowUpdateButtonImages(WWindow *wwin)
fwin->lbutton_image = scr->b_pixmaps[WBUT_ICONIFY]; fwin->lbutton_image = scr->b_pixmaps[WBUT_ICONIFY];
} }
} }
#ifdef XKB_BUTTON_HINT
if (!WFLAGP(wwin, no_language_button)) {
if (fwin->languagebutton_image && !fwin->languagebutton_image->shared)
wPixmapDestroy(fwin->languagebutton_image);
fwin->languagebutton_image = scr->b_pixmaps[WBUT_XKBGROUP1 + fwin->languagemode];
}
#endif
/* close button */ /* close button */
@@ -2639,6 +2638,11 @@ void wWindowSetKeyGrabs(WWindow * wwin)
if (key->keycode == 0) if (key->keycode == 0)
continue; continue;
/* WKBD_KEYCHAIN_CANCEL is only meaningful while inside an active key chain */
if (i == WKBD_KEYCHAIN_CANCEL)
continue;
if (key->modifier != AnyModifier) { if (key->modifier != AnyModifier) {
XGrabKey(dpy, key->keycode, key->modifier | LockMask, XGrabKey(dpy, key->keycode, key->modifier | LockMask,
wwin->frame->core->window, True, GrabModeAsync, GrabModeAsync); wwin->frame->core->window, True, GrabModeAsync, GrabModeAsync);
@@ -3140,6 +3144,37 @@ static void windowCloseDblClick(WCoreWindow *sender, void *data, XEvent *event)
} }
#ifdef XKB_BUTTON_HINT #ifdef XKB_BUTTON_HINT
/* Helper function to extract the 2-letter language code for a given XKB group index */
void wWindowGetLanguageLabel(int group_index, char *label)
{
XkbRF_VarDefsRec vd;
/* Default to empty - will fallback to pixmap if we can't get the label */
label[0] = '\0';
if (XkbRF_GetNamesProp(dpy, NULL, &vd) && vd.layout) {
int i;
char *layout_list = strdup(vd.layout);
char *tok = strtok(layout_list, ",");
/* Iterate to the requested group index */
for (i = 0; i < group_index && tok != NULL; i++) {
tok = strtok(NULL, ",");
}
if (tok) {
/* Copy exactly the first two bytes, then format: first uppercase, second lowercase */
strncpy(label, tok, 2);
label[2] = '\0';
if (label[0])
label[0] = (char) toupper((unsigned char) label[0]);
if (label[1])
label[1] = (char) tolower((unsigned char) label[1]);
}
free(layout_list);
}
}
static void windowLanguageClick(WCoreWindow *sender, void *data, XEvent *event) static void windowLanguageClick(WCoreWindow *sender, void *data, XEvent *event)
{ {
WWindow *wwin = data; WWindow *wwin = data;
@@ -3153,11 +3188,45 @@ static void windowLanguageClick(WCoreWindow *sender, void *data, XEvent *event)
if (event->xbutton.button != Button1 && event->xbutton.button != Button3) if (event->xbutton.button != Button1 && event->xbutton.button != Button3)
return; return;
tl = wwin->frame->languagemode; tl = wwin->frame->languagemode;
wwin->frame->languagemode = wwin->frame->last_languagemode;
wwin->frame->last_languagemode = tl; /* Try to advance to the next available XKB group */
XkbDescPtr desc = XkbGetKeyboard(dpy, XkbAllComponentsMask, XkbUseCoreKbd);
int newgroup = -1;
if (desc && desc->names) {
int i;
const int MAX_GROUPS = 4; /* typical XKB max groups */
for (i = 1; i <= MAX_GROUPS; i++) {
int cand = (tl + i) % MAX_GROUPS;
Atom a = desc->names->groups[cand];
if (a != None) {
/* Use XGetAtomName to ensure the atom actually has a name */
char *nm = XGetAtomName(dpy, a);
if (nm && nm[0] != '\0') {
newgroup = cand;
XFree(nm);
break;
}
if (nm)
XFree(nm);
}
}
}
if (newgroup >= 0) {
wwin->frame->last_languagemode = tl;
wwin->frame->languagemode = newgroup;
XkbLockGroup(dpy, XkbUseCoreKbd, wwin->frame->languagemode);
} else {
/* fallback to previous toggle behaviour for setups with only two
* groups or when group info is not available */
wwin->frame->languagemode = wwin->frame->last_languagemode;
wwin->frame->last_languagemode = tl;
XkbLockGroup(dpy, XkbUseCoreKbd, wwin->frame->languagemode);
}
/* Update label */
wWindowGetLanguageLabel(wwin->frame->languagemode, wwin->frame->language_label);
wSetFocusTo(scr, wwin); wSetFocusTo(scr, wwin);
wwin->frame->languagebutton_image =
wwin->frame->screen_ptr->b_pixmaps[WBUT_XKBGROUP1 + wwin->frame->languagemode];
wFrameWindowUpdateLanguageButton(wwin->frame); wFrameWindowUpdateLanguageButton(wwin->frame);
if (event->xbutton.button == Button3) if (event->xbutton.button == Button3)
return; return;

View File

@@ -405,4 +405,9 @@ void wWindowDeleteSavedState(WMagicNumber id);
Bool wWindowObscuresWindow(WWindow *wwin, WWindow *obscured); Bool wWindowObscuresWindow(WWindow *wwin, WWindow *obscured);
void wWindowSetOmnipresent(WWindow *wwin, Bool flag); void wWindowSetOmnipresent(WWindow *wwin, Bool flag);
#ifdef XKB_BUTTON_HINT
void wWindowGetLanguageLabel(int group_index, char *label);
#endif
#endif #endif