From: Michael Stapelberg Date: Sun, 23 Aug 2015 20:49:32 +0000 (+0200) Subject: Use libxkbcommon for translating keysyms, support all XKB groups. X-Git-Tag: 4.11~40 X-Git-Url: https://git.sur5r.net/?a=commitdiff_plain;h=bf3cd41b5ddf1e757515ab5fbf811be56e5f69cc;p=i3%2Fi3 Use libxkbcommon for translating keysyms, support all XKB groups. fixes #1835 This commit improves the translation of keysyms to keycodes by loading keymaps using libxkbcommon-x11 and using libxkbcommon for figuring out the keymap, depending on each keybinding’s modifiers. This way, the upper layers of complex layouts are now usable with i3’s bindsym directive, such as de_neo’s layer 3 and higher. Furthermore, the commit generalizes the handling of different XKB groups. We formerly had support only for two separate groups, the default group 1, and group 2. While Mode_switch is only one way to switch to group 2, we called the binding option Mode_switch. With this commit, the new names Group1, Group2 (an alias for Mode_switch), Group3 and Group4 are introduced for configuring bindings. This is only useful for advanced keyboard layouts, such as people loading two keyboard layouts and switching between them (us, ru seems to be a popular combination). When grabbing keys, one can only specify the modifier mask, but not an XKB state mask (or value), so we still dynamically unbind and re-bind keys whenever the XKB group changes. The commit was manually tested using the following i3 config: bindsym Group4+n nop heya from group 4 bindsym Group3+n nop heya from group 3 bindsym Group2+n nop heya from group 2 bindsym n nop heya bindsym shift+N nop explicit shift binding bindsym shift+r nop implicit shift binding bindcode Group2+38 nop fallback overwritten in group 2 only bindcode 38 nop fallback …with the following layout: setxkbmap -layout "us,ua,ru,de" -variant ",winkeys,,neo" \ -option "grp:shift_caps_toggle,grp_led:scroll" \ -model pc104 -rules evdev By default (xkb group 1, us layout), pressing “n” will result in the “heya” message appearing. Pressing “a” will result in the “fallback” message appearing. “j” is not triggered. By pressing Shift+CapsLock you switch to the next group (xkb group 2, ua layout). Pressing “a” will result in the “fallback overwritten in group 2 only” message, pressing “n” will still result in “heya”. “j” is not triggered. In the next group (xkb group 3, ru layout), pressing “a” will result in the “fallback” message again, pressing “n” will result in “heya”, “j” is not triggered. In the last group (xkb group 4, de_neo layout), pressing “a” will still result in “fallback”, pressing “n” will result in “heya”, pressing “j” will result in “heya from group 4”. Pressing shift+n results in “explicit shift binding”, pressing shift+r results in “implicit shift binding”. This ensures that keysym translation falls back to looking at non-shift keys (“r” can be used instead of ”R”) and that the order of keybindings doesn’t play a role (“bindsym n” does not override “bindsym shift+n”, even though it’s specified earlier in the config). The fallback behavior ensures use-cases such as ticket #1775 are still covered. Only binding keys when the X server is in the corresponding XKB group ensures use-cases such as ticket #585 are still covered. --- diff --git a/docs/ipc b/docs/ipc index 4462e772..5113d79b 100644 --- a/docs/ipc +++ b/docs/ipc @@ -773,8 +773,8 @@ The +binding (object)+ field contains details about the binding that was run: command (string):: The i3 command that is configured to run for this binding. -mods (array of strings):: - The modifier keys that were configured with this binding. +event_state_mask (array of strings):: + The group and modifier keys that were configured with this binding. input_code (integer):: If the binding was configured with +bindcode+, this will be the key code that was given for the binding. If the binding is a mouse binding, it will be @@ -792,7 +792,7 @@ input_type (string):: "change": "run", "binding": { "command": "nop", - "mods": [ + "event_state_mask": [ "shift", "ctrl" ], diff --git a/docs/userguide b/docs/userguide index 9316a4d5..25f5a4ba 100644 --- a/docs/userguide +++ b/docs/userguide @@ -368,8 +368,8 @@ after the keys have been released. *Syntax*: ---------------------------------- -bindsym [--release] [+] command -bindcode [--release] [+] command +bindsym [--release] [+][+] command +bindcode [--release] [+][+] command ---------------------------------- *Examples*: @@ -395,12 +395,13 @@ Available Modifiers: Mod1-Mod5, Shift, Control:: Standard modifiers, see +xmodmap(1)+ -Mode_switch:: -Unlike other window managers, i3 can use Mode_switch as a modifier. This allows -you to remap capslock (for example) to Mode_switch and use it for both: typing -umlauts or special characters 'and' having some comfortably reachable key -bindings. For example, when typing, capslock+1 or capslock+2 for switching -workspaces is totally convenient. Try it :-). +Group1, Group2, Group3, Group4:: +When using multiple keyboard layouts (e.g. with `setxkbmap -layout us,ru`), you +can specify in which XKB group (also called “layout”) a keybinding should be +active. By default, keybindings are translated in Group1 and are active in all +groups. If you want to override keybindings in one of your layouts, specify the +corresponding group. For backwards compatibility, the group “Mode_switch” is an +alias for Group2. [[mousebindings]] diff --git a/include/bindings.h b/include/bindings.h index 75a719e9..88b4a6cc 100644 --- a/include/bindings.h +++ b/include/bindings.h @@ -31,7 +31,7 @@ Binding *configure_binding(const char *bindtype, const char *modifiers, const ch * Grab the bound keys (tell X to send us keypress events for those keycodes) * */ -void grab_all_keys(xcb_connection_t *conn, bool bind_mode_switch); +void grab_all_keys(xcb_connection_t *conn); /** * Returns a pointer to the Binding that matches the given xcb event or NULL if @@ -52,6 +52,21 @@ void translate_keysyms(void); */ void switch_mode(const char *new_mode); +/** + * Reorders bindings by event_state_mask descendingly so that get_binding() + * correctly matches more specific bindings before more generic bindings. Take + * the following binding configuration as an example: + * + * bindsym n nop lower-case n pressed + * bindsym Shift+n nop upper-case n pressed + * + * Without reordering, the first binding’s event_state_mask of 0x0 would match + * the actual event_stat_mask of 0x1 and hence trigger instead of the second + * keybinding. + * + */ +void reorder_bindings(void); + /** * Checks for duplicate key bindings (the same keycode or keysym is configured * more than once). If a duplicate binding is found, a message is printed to @@ -74,3 +89,9 @@ void binding_free(Binding *bind); * */ CommandResult *run_binding(Binding *bind, Con *con); + +/** + * Loads the XKB keymap from the X11 server and feeds it to xkbcommon. + * + */ +bool load_keymap(void); diff --git a/include/config_directives.h b/include/config_directives.h index fc34a85f..72646651 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -12,10 +12,10 @@ #include "config_parser.h" /** - * A utility function to convert a string of modifiers to the corresponding bit - * mask. + * A utility function to convert a string containing the group and modifiers to + * the corresponding bit mask. */ -uint32_t modifiers_from_str(const char *str); +i3_event_state_mask_t event_state_from_str(const char *str); /** The beginning of the prototype for every cfg_ function. */ #define I3_CFG Match *current_match, struct ConfigResultIR *result diff --git a/include/data.h b/include/data.h index 66000e45..634cca67 100644 --- a/include/data.h +++ b/include/data.h @@ -74,18 +74,6 @@ typedef enum { ADJ_NONE = 0, ADJ_UPPER_SCREEN_EDGE = (1 << 2), ADJ_LOWER_SCREEN_EDGE = (1 << 4) } adjacent_t; -enum { - BIND_NONE = 0, - BIND_SHIFT = XCB_MOD_MASK_SHIFT, /* (1 << 0) */ - BIND_CONTROL = XCB_MOD_MASK_CONTROL, /* (1 << 2) */ - BIND_MOD1 = XCB_MOD_MASK_1, /* (1 << 3) */ - BIND_MOD2 = XCB_MOD_MASK_2, /* (1 << 4) */ - BIND_MOD3 = XCB_MOD_MASK_3, /* (1 << 5) */ - BIND_MOD4 = XCB_MOD_MASK_4, /* (1 << 6) */ - BIND_MOD5 = XCB_MOD_MASK_5, /* (1 << 7) */ - BIND_MODE_SWITCH = (1 << 8) -}; - /** * Container layouts. See Con::layout. */ @@ -107,6 +95,25 @@ typedef enum { B_MOUSE = 1 } input_type_t; +/** + * Bitmask for matching XCB_XKB_GROUP_1 to XCB_XKB_GROUP_4. + */ +typedef enum { + I3_XKB_GROUP_MASK_ANY = 0, + I3_XKB_GROUP_MASK_1 = (1 << 0), + I3_XKB_GROUP_MASK_2 = (1 << 1), + I3_XKB_GROUP_MASK_3 = (1 << 2), + I3_XKB_GROUP_MASK_4 = (1 << 3) +} i3_xkb_group_mask_t; + +/** + * The lower 16 bits contain a xcb_key_but_mask_t, the higher 16 bits contain + * an i3_xkb_group_mask_t. This type is necessary for the fallback logic to + * work when handling XKB groups (see ticket #1775) and makes the code which + * locates keybindings upon KeyPress/KeyRelease events simpler. + */ +typedef uint32_t i3_event_state_mask_t; + /** * Mouse pointer warping modes. */ @@ -269,8 +276,10 @@ struct Binding { /** Keycode to bind */ uint32_t keycode; - /** Bitmask consisting of BIND_MOD_1, BIND_MODE_SWITCH, … */ - uint32_t mods; + /** Bitmask which is applied against event->state for KeyPress and + * KeyRelease events to determine whether this binding applies to the + * current state. */ + i3_event_state_mask_t event_state_mask; /** Symbol the user specified in configfile, if any. This needs to be * stored with the binding to be able to re-convert it into a keycode diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 3767acf8..1191157c 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -316,7 +316,7 @@ state BINDING: -> whole_window = '--whole-window' -> - modifiers = 'Mod1', 'Mod2', 'Mod3', 'Mod4', 'Mod5', 'Shift', 'Control', 'Ctrl', 'Mode_switch', '$mod' + modifiers = 'Mod1', 'Mod2', 'Mod3', 'Mod4', 'Mod5', 'Shift', 'Control', 'Ctrl', 'Mode_switch', 'Group1', 'Group2', 'Group3', 'Group4', '$mod' -> '+' -> @@ -369,7 +369,7 @@ state MODE_BINDING: -> whole_window = '--whole-window' -> - modifiers = 'Mod1', 'Mod2', 'Mod3', 'Mod4', 'Mod5', 'Shift', 'Control', 'Ctrl', 'Mode_switch', '$mod' + modifiers = 'Mod1', 'Mod2', 'Mod3', 'Mod4', 'Mod5', 'Shift', 'Control', 'Ctrl', 'Mode_switch', 'Group1', 'Group2', 'Group3', 'Group4', '$mod' -> '+' -> diff --git a/src/bindings.c b/src/bindings.c index adca5546..f6ed086b 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -9,6 +9,10 @@ #include "all.h" #include +#include + +static struct xkb_context *xkb_context; +static struct xkb_keymap *xkb_keymap; pid_t command_error_nagbar_pid = -1; @@ -73,8 +77,19 @@ Binding *configure_binding(const char *bindtype, const char *modifiers, const ch return NULL; } } - new_binding->mods = modifiers_from_str(modifiers); new_binding->command = sstrdup(command); + new_binding->event_state_mask = event_state_from_str(modifiers); + int group_bits_set = 0; + if (new_binding->event_state_mask & I3_XKB_GROUP_MASK_1) + group_bits_set++; + if (new_binding->event_state_mask & I3_XKB_GROUP_MASK_2) + group_bits_set++; + if (new_binding->event_state_mask & I3_XKB_GROUP_MASK_3) + group_bits_set++; + if (new_binding->event_state_mask & I3_XKB_GROUP_MASK_4) + group_bits_set++; + if (group_bits_set > 1) + ELOG("Keybinding has more than one Group specified, but your X server is always in precisely one group. The keybinding can never trigger.\n"); struct Mode *mode = mode_from_name(modename); TAILQ_INSERT_TAIL(mode->bindings, new_binding, bindings); @@ -86,18 +101,23 @@ static void grab_keycode_for_binding(xcb_connection_t *conn, Binding *bind, uint if (bind->input_type != B_KEYBOARD) return; - DLOG("Grabbing %d with modifiers %d (with mod_mask_lock %d)\n", keycode, bind->mods, bind->mods | XCB_MOD_MASK_LOCK); /* Grab the key in all combinations */ #define GRAB_KEY(modifier) \ do { \ xcb_grab_key(conn, 0, root, modifier, keycode, XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); \ } while (0) - int mods = bind->mods; - if ((bind->mods & BIND_MODE_SWITCH) != 0) { - mods &= ~BIND_MODE_SWITCH; - if (mods == 0) - mods = XCB_MOD_MASK_ANY; - } + int mods = bind->event_state_mask; + if (((mods >> 16) & I3_XKB_GROUP_MASK_1) && xkb_current_group != XCB_XKB_GROUP_1) + return; + if (((mods >> 16) & I3_XKB_GROUP_MASK_2) && xkb_current_group != XCB_XKB_GROUP_2) + return; + if (((mods >> 16) & I3_XKB_GROUP_MASK_3) && xkb_current_group != XCB_XKB_GROUP_3) + return; + if (((mods >> 16) & I3_XKB_GROUP_MASK_4) && xkb_current_group != XCB_XKB_GROUP_4) + return; + mods &= 0xFFFF; + DLOG("Grabbing keycode %d with event state mask 0x%x (mods 0x%x)\n", + keycode, bind->event_state_mask, mods); GRAB_KEY(mods); GRAB_KEY(mods | xcb_numlock_mask); GRAB_KEY(mods | XCB_MOD_MASK_LOCK); @@ -108,7 +128,7 @@ static void grab_keycode_for_binding(xcb_connection_t *conn, Binding *bind, uint * Grab the bound keys (tell X to send us keypress events for those keycodes) * */ -void grab_all_keys(xcb_connection_t *conn, bool bind_mode_switch) { +void grab_all_keys(xcb_connection_t *conn) { Binding *bind; TAILQ_FOREACH(bind, bindings, bindings) { if (bind->input_type != B_KEYBOARD) @@ -120,9 +140,8 @@ void grab_all_keys(xcb_connection_t *conn, bool bind_mode_switch) { continue; } - xcb_keycode_t *walk = bind->translated_to; for (uint32_t i = 0; i < bind->number_keycodes; i++) - grab_keycode_for_binding(conn, bind, *walk++); + grab_keycode_for_binding(conn, bind, bind->translated_to[i]); } } @@ -131,7 +150,7 @@ void grab_all_keys(xcb_connection_t *conn, bool bind_mode_switch) { * keycode or NULL if no such binding exists. * */ -static Binding *get_binding(uint16_t modifiers, bool is_release, uint16_t input_code, input_type_t input_type) { +static Binding *get_binding(i3_event_state_mask_t state_filtered, bool is_release, uint16_t input_code, input_type_t input_type) { Binding *bind; if (!is_release) { @@ -146,12 +165,15 @@ static Binding *get_binding(uint16_t modifiers, bool is_release, uint16_t input_ } TAILQ_FOREACH(bind, bindings, bindings) { - /* First compare the modifiers (unless this is a + DLOG("binding with event_state_mask 0x%x, state_filtered 0x%x, match: %s\n", + bind->event_state_mask, state_filtered, + ((state_filtered & bind->event_state_mask) == bind->event_state_mask) ? "yes" : "no"); + /* First compare the state_filtered (unless this is a * B_UPON_KEYRELEASE_IGNORE_MODS binding and this is a KeyRelease * event) */ if (bind->input_type != input_type) continue; - if (bind->mods != modifiers && + if ((state_filtered & bind->event_state_mask) != bind->event_state_mask && (bind->release != B_UPON_KEYRELEASE_IGNORE_MODS || !is_release)) continue; @@ -195,49 +217,89 @@ static Binding *get_binding(uint16_t modifiers, bool is_release, uint16_t input_ * */ Binding *get_binding_from_xcb_event(xcb_generic_event_t *event) { - bool is_release = (event->response_type == XCB_KEY_RELEASE || event->response_type == XCB_BUTTON_RELEASE); - - input_type_t input_type = ((event->response_type == XCB_BUTTON_RELEASE || event->response_type == XCB_BUTTON_PRESS) - ? B_MOUSE - : B_KEYBOARD); - - uint16_t event_state = ((xcb_key_press_event_t *)event)->state; - uint16_t event_detail = ((xcb_key_press_event_t *)event)->detail; + const bool is_release = (event->response_type == XCB_KEY_RELEASE || + event->response_type == XCB_BUTTON_RELEASE); + + const input_type_t input_type = ((event->response_type == XCB_BUTTON_RELEASE || + event->response_type == XCB_BUTTON_PRESS) + ? B_MOUSE + : B_KEYBOARD); + + const uint16_t event_state = ((xcb_key_press_event_t *)event)->state; + const uint16_t event_detail = ((xcb_key_press_event_t *)event)->detail; + + /* Remove the numlock bit */ + i3_event_state_mask_t state_filtered = event_state & ~(xcb_numlock_mask | XCB_MOD_MASK_LOCK); + DLOG("(removed numlock, state = 0x%x)\n", state_filtered); + /* Transform the keyboard_group from bit 13 and bit 14 into an + * i3_xkb_group_mask_t, so that get_binding() can just bitwise AND the + * configured bindings against |state_filtered|. + * + * These bits are only set because we set the XKB client flags + * XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE and + * XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED. See also doc/kbproto + * section 2.2.2: + * http://www.x.org/releases/X11R7.7/doc/kbproto/xkbproto.html#Computing_A_State_Field_from_an_XKB_State */ + switch ((event_state & 0x6000) >> 13) { + case XCB_XKB_GROUP_1: + state_filtered |= (I3_XKB_GROUP_MASK_1 << 16); + break; + case XCB_XKB_GROUP_2: + state_filtered |= (I3_XKB_GROUP_MASK_2 << 16); + break; + case XCB_XKB_GROUP_3: + state_filtered |= (I3_XKB_GROUP_MASK_3 << 16); + break; + case XCB_XKB_GROUP_4: + state_filtered |= (I3_XKB_GROUP_MASK_4 << 16); + break; + } + state_filtered &= ~0x6000; + DLOG("(transformed keyboard group, state = 0x%x)\n", state_filtered); + return get_binding(state_filtered, is_release, event_detail, input_type); +} - /* Remove the numlock bit, all other bits are modifiers we can bind to */ - uint16_t state_filtered = event_state & ~(xcb_numlock_mask | XCB_MOD_MASK_LOCK); - DLOG("(removed numlock, state = %d)\n", state_filtered); - /* Only use the lower 8 bits of the state (modifier masks) so that mouse - * button masks are filtered out */ - state_filtered &= 0xFF; - DLOG("(removed upper 8 bits, state = %d)\n", state_filtered); +struct resolve { + /* The binding which we are resolving. */ + Binding *bind; - if (xkb_current_group == XCB_XKB_GROUP_2) - state_filtered |= BIND_MODE_SWITCH; + /* |bind|’s keysym (translated to xkb_keysym_t), e.g. XKB_KEY_R. */ + xkb_keysym_t keysym; - DLOG("(checked mode_switch, state %d)\n", state_filtered); + /* The xkb state built from the user-provided modifiers and group. */ + struct xkb_state *xkb_state; - /* Find the binding */ - Binding *bind = get_binding(state_filtered, is_release, event_detail, input_type); + /* Like |xkb_state|, just without the shift modifier, if shift was specified. */ + struct xkb_state *xkb_state_no_shift; +}; - /* No match? Then the user has Mode_switch enabled but does not have a - * specific keybinding. Fall back to the default keybindings (without - * Mode_switch). Makes it much more convenient for users of a hybrid - * layout (like {us, ru} or {dvorak, us}, see e.g. ticket #1775). */ - if (bind == NULL) { - state_filtered &= ~(BIND_MODE_SWITCH); - DLOG("no match, new state_filtered = %d\n", state_filtered); - if ((bind = get_binding(state_filtered, is_release, event_detail, input_type)) == NULL) { - /* This is not a real error since we can have release and - * non-release bindings. On a press event for which there is only a - * !release-binding, but no release-binding, the corresponding - * release event will trigger this. No problem, though. */ - DLOG("Could not lookup key binding (modifiers %d, keycode %d)\n", - state_filtered, event_detail); - } +/* + * add_keycode_if_matches is called for each keycode in the keymap and will add + * the keycode to |data->bind| if the keycode can result in the keysym + * |data->resolving|. + * + */ +static void add_keycode_if_matches(struct xkb_keymap *keymap, xkb_keycode_t key, void *data) { + const struct resolve *resolving = data; + xkb_keysym_t sym = xkb_state_key_get_one_sym(resolving->xkb_state, key); + if (sym != resolving->keysym) { + /* Check if Shift was specified, and try resolving the symbol without + * shift, so that “bindsym $mod+Shift+a nop” actually works. */ + const xkb_layout_index_t layout = xkb_state_key_get_layout(resolving->xkb_state, key); + if (layout == XKB_LAYOUT_INVALID) + return; + if (xkb_state_key_get_level(resolving->xkb_state, key, layout) != 1) + return; + sym = xkb_state_key_get_one_sym(resolving->xkb_state_no_shift, key); + if (sym != resolving->keysym) + return; } - - return bind; + Binding *bind = resolving->bind; + bind->number_keycodes++; + bind->translated_to = srealloc(bind->translated_to, + (sizeof(xcb_keycode_t) * + bind->number_keycodes)); + bind->translated_to[bind->number_keycodes - 1] = key; } /* @@ -245,16 +307,19 @@ Binding *get_binding_from_xcb_event(xcb_generic_event_t *event) { * */ void translate_keysyms(void) { - Binding *bind; - xcb_keysym_t keysym; - int col; - xcb_keycode_t i, min_keycode, max_keycode; - - const bool mode_switch = (xkb_current_group == XCB_XKB_GROUP_2); + struct xkb_state *dummy_state = xkb_state_new(xkb_keymap); + if (dummy_state == NULL) { + ELOG("Could not create XKB state, cannot translate keysyms.\n"); + return; + } - min_keycode = xcb_get_setup(conn)->min_keycode; - max_keycode = xcb_get_setup(conn)->max_keycode; + struct xkb_state *dummy_state_no_shift = xkb_state_new(xkb_keymap); + if (dummy_state_no_shift == NULL) { + ELOG("Could not create XKB state, cannot translate keysyms.\n"); + return; + } + Binding *bind; TAILQ_FOREACH(bind, bindings, bindings) { if (bind->input_type == B_MOUSE) { char *endptr; @@ -271,36 +336,66 @@ void translate_keysyms(void) { continue; /* We need to translate the symbol to a keycode */ - keysym = xkb_keysym_from_name(bind->symbol, XKB_KEYSYM_NO_FLAGS); + const xkb_keysym_t keysym = xkb_keysym_from_name(bind->symbol, XKB_KEYSYM_NO_FLAGS); if (keysym == XKB_KEY_NoSymbol) { ELOG("Could not translate string to key symbol: \"%s\"\n", bind->symbol); continue; } - /* Base column we use for looking up key symbols. We always consider - * the base column and the corresponding shift column, so without - * mode_switch, we look in 0 and 1, with mode_switch we look in 2 and - * 3. */ - col = (bind->mods & BIND_MODE_SWITCH || mode_switch ? 2 : 0); - + xkb_layout_index_t group = XCB_XKB_GROUP_1; + if ((bind->event_state_mask >> 16) & I3_XKB_GROUP_MASK_2) + group = XCB_XKB_GROUP_2; + else if ((bind->event_state_mask >> 16) & I3_XKB_GROUP_MASK_3) + group = XCB_XKB_GROUP_3; + else if ((bind->event_state_mask >> 16) & I3_XKB_GROUP_MASK_4) + group = XCB_XKB_GROUP_4; + + DLOG("group = %d, event_state_mask = %d, &2 = %s, &3 = %s, &4 = %s\n", group, + bind->event_state_mask, + (bind->event_state_mask & I3_XKB_GROUP_MASK_2) ? "yes" : "no", + (bind->event_state_mask & I3_XKB_GROUP_MASK_3) ? "yes" : "no", + (bind->event_state_mask & I3_XKB_GROUP_MASK_4) ? "yes" : "no"); + (void)xkb_state_update_mask( + dummy_state, + (bind->event_state_mask & 0x1FFF) /* xkb_mod_mask_t base_mods, */, + 0 /* xkb_mod_mask_t latched_mods, */, + 0 /* xkb_mod_mask_t locked_mods, */, + 0 /* xkb_layout_index_t base_group, */, + 0 /* xkb_layout_index_t latched_group, */, + group /* xkb_layout_index_t locked_group, */); + + (void)xkb_state_update_mask( + dummy_state_no_shift, + (bind->event_state_mask & 0x1FFF) & ~XCB_KEY_BUT_MASK_SHIFT /* xkb_mod_mask_t base_mods, */, + 0 /* xkb_mod_mask_t latched_mods, */, + 0 /* xkb_mod_mask_t locked_mods, */, + 0 /* xkb_layout_index_t base_group, */, + 0 /* xkb_layout_index_t latched_group, */, + group /* xkb_layout_index_t locked_group, */); + + struct resolve resolving = { + .bind = bind, + .keysym = keysym, + .xkb_state = dummy_state, + .xkb_state_no_shift = dummy_state_no_shift, + }; FREE(bind->translated_to); bind->number_keycodes = 0; - - for (i = min_keycode; i && i <= max_keycode; i++) { - if ((xcb_key_symbols_get_keysym(keysyms, i, col) != keysym) && - (xcb_key_symbols_get_keysym(keysyms, i, col + 1) != keysym)) - continue; - bind->number_keycodes++; - bind->translated_to = srealloc(bind->translated_to, - (sizeof(xcb_keycode_t) * - bind->number_keycodes)); - bind->translated_to[bind->number_keycodes - 1] = i; + xkb_keymap_key_for_each(xkb_keymap, add_keycode_if_matches, &resolving); + char *keycodes = sstrdup(""); + for (uint32_t n = 0; n < bind->number_keycodes; n++) { + char *tmp; + sasprintf(&tmp, "%s %d", keycodes, bind->translated_to[n]); + free(keycodes); + keycodes = tmp; } - - DLOG("Translated symbol \"%s\" to %d keycode (mods %d)\n", bind->symbol, - bind->number_keycodes, bind->mods); + DLOG("state=0x%x, cfg=\"%s\", sym=0x%x → keycodes%s (%d)\n", + bind->event_state_mask, bind->symbol, keysym, keycodes, bind->number_keycodes); + free(keycodes); } + + xkb_state_unref(dummy_state); } /* @@ -319,7 +414,7 @@ void switch_mode(const char *new_mode) { ungrab_all_keys(conn); bindings = mode->bindings; translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); char *event_msg; sasprintf(&event_msg, "{\"change\":\"%s\"}", mode->name); @@ -333,6 +428,61 @@ void switch_mode(const char *new_mode) { ELOG("ERROR: Mode not found\n"); } +static void reorder_bindings_of_mode(struct Mode *mode) { + struct bindings_head *reordered = scalloc(1, sizeof(struct bindings_head)); + TAILQ_INIT(reordered); + /* 20 bits are in use in an i3_event_state_mask_t. */ + for (int n = 19; n >= 0; n--) { + Binding *current; + for (current = TAILQ_FIRST(mode->bindings); current != TAILQ_END(mode->bindings); ) { + /* Advance |current| so that we can use TAILQ_REMOVE safely. */ + Binding *bind = current; + current = TAILQ_NEXT(current, bindings); + if ((bind->event_state_mask & (1 << n)) == 0) + continue; + TAILQ_REMOVE(mode->bindings, bind, bindings); + TAILQ_INSERT_TAIL(reordered, bind, bindings); + } + } + /* Move over the bindings with event_state_mask == 0x0. */ + Binding *current; + for (current = TAILQ_FIRST(mode->bindings); current != TAILQ_END(mode->bindings); ) { + /* Advance |current| so that we can use TAILQ_REMOVE safely. */ + Binding *bind = current; + current = TAILQ_NEXT(current, bindings); + assert(bind->event_state_mask == 0); + TAILQ_REMOVE(mode->bindings, bind, bindings); + TAILQ_INSERT_TAIL(reordered, bind, bindings); + } + assert(TAILQ_EMPTY(mode->bindings)); + /* Free the old bindings_head, which is now empty. */ + free(mode->bindings); + mode->bindings = reordered; +} + +/* + * Reorders bindings by event_state_mask descendingly so that get_binding() + * correctly matches more specific bindings before more generic bindings. Take + * the following binding configuration as an example: + * + * bindsym n nop lower-case n pressed + * bindsym Shift+n nop upper-case n pressed + * + * Without reordering, the first binding’s event_state_mask of 0x0 would match + * the actual event_stat_mask of 0x1 and hence trigger instead of the second + * keybinding. + * + */ +void reorder_bindings(void) { + struct Mode *mode; + SLIST_FOREACH(mode, &modes, modes) { + const bool current_mode = (mode->bindings == bindings); + reorder_bindings_of_mode(mode); + if (current_mode) + bindings = mode->bindings; + } +} + /* * Checks for duplicate key bindings (the same keycode or keysym is configured * more than once). If a duplicate binding is found, a message is printed to @@ -370,17 +520,17 @@ void check_for_duplicate_bindings(struct context *context) { /* Check if the keycodes or modifiers are different. If so, they * can't be duplicate */ if (bind->keycode != current->keycode || - bind->mods != current->mods || + bind->event_state_mask != current->event_state_mask || bind->release != current->release) continue; context->has_errors = true; if (current->keycode != 0) { - ELOG("Duplicate keybinding in config file:\n modmask %d with keycode %d, command \"%s\"\n", - current->mods, current->keycode, current->command); + ELOG("Duplicate keybinding in config file:\n state mask 0x%x with keycode %d, command \"%s\"\n", + current->event_state_mask, current->keycode, current->command); } else { - ELOG("Duplicate keybinding in config file:\n modmask %d with keysym %s, command \"%s\"\n", - current->mods, current->symbol, current->command); + ELOG("Duplicate keybinding in config file:\n state mask 0x%x with keysym %s, command \"%s\"\n", + current->event_state_mask, current->symbol, current->command); } } } @@ -466,3 +616,28 @@ CommandResult *run_binding(Binding *bind, Con *con) { return result; } + +/* + * Loads the XKB keymap from the X11 server and feeds it to xkbcommon. + * + */ +bool load_keymap(void) { + if (xkb_context == NULL) { + if ((xkb_context = xkb_context_new(0)) == NULL) { + ELOG("Could not create xkbcommon context\n"); + return false; + } + } + + struct xkb_keymap *new_keymap; + const int32_t device_id = xkb_x11_get_core_keyboard_device_id(conn); + DLOG("device_id = %d\n", device_id); + if ((new_keymap = xkb_x11_keymap_new_from_device(xkb_context, conn, device_id, 0)) == NULL) { + ELOG("xkb_x11_keymap_new_from_device failed\n"); + return false; + } + xkb_keymap_unref(xkb_keymap); + xkb_keymap = new_keymap; + + return true; +} diff --git a/src/click.c b/src/click.c index bd2dcb9f..92a1a1fa 100644 --- a/src/click.c +++ b/src/click.c @@ -229,7 +229,7 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod /* get the floating con */ Con *floatingcon = con_inside_floating(con); - const bool proportional = (event->state & BIND_SHIFT); + const bool proportional = (event->state & XCB_KEY_BUT_MASK_SHIFT) == XCB_KEY_BUT_MASK_SHIFT; const bool in_stacked = (con->parent->layout == L_STACKED || con->parent->layout == L_TABBED); /* 1: see if the user scrolled on the decoration of a stacked/tabbed con */ diff --git a/src/config.c b/src/config.c index b0c6f1a4..0dc59365 100644 --- a/src/config.c +++ b/src/config.c @@ -214,7 +214,7 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, if (reload) { translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); } if (config.font.type == FONT_TYPE_NONE) { diff --git a/src/config_directives.c b/src/config_directives.c index 6032dc11..463b38e7 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -161,32 +161,40 @@ static bool eval_boolstr(const char *str) { } /* - * A utility function to convert a string of modifiers to the corresponding bit - * mask. + * A utility function to convert a string containing the group and modifiers to + * the corresponding bit mask. */ -uint32_t modifiers_from_str(const char *str) { +i3_event_state_mask_t event_state_from_str(const char *str) { /* It might be better to use strtok() here, but the simpler strstr() should * do for now. */ - uint32_t result = 0; + i3_event_state_mask_t result = 0; if (str == NULL) return result; if (strstr(str, "Mod1") != NULL) - result |= BIND_MOD1; + result |= XCB_KEY_BUT_MASK_MOD_1; if (strstr(str, "Mod2") != NULL) - result |= BIND_MOD2; + result |= XCB_KEY_BUT_MASK_MOD_2; if (strstr(str, "Mod3") != NULL) - result |= BIND_MOD3; + result |= XCB_KEY_BUT_MASK_MOD_3; if (strstr(str, "Mod4") != NULL) - result |= BIND_MOD4; + result |= XCB_KEY_BUT_MASK_MOD_4; if (strstr(str, "Mod5") != NULL) - result |= BIND_MOD5; + result |= XCB_KEY_BUT_MASK_MOD_5; if (strstr(str, "Control") != NULL || strstr(str, "Ctrl") != NULL) - result |= BIND_CONTROL; + result |= XCB_KEY_BUT_MASK_CONTROL; if (strstr(str, "Shift") != NULL) - result |= BIND_SHIFT; - if (strstr(str, "Mode_switch") != NULL) - result |= BIND_MODE_SWITCH; + result |= XCB_KEY_BUT_MASK_SHIFT; + + if (strstr(str, "Group1") != NULL) + result |= (I3_XKB_GROUP_MASK_1 << 16); + if (strstr(str, "Group2") != NULL || + strstr(str, "Mode_switch") != NULL) + result |= (I3_XKB_GROUP_MASK_2 << 16); + if (strstr(str, "Group3") != NULL) + result |= (I3_XKB_GROUP_MASK_3 << 16); + if (strstr(str, "Group4") != NULL) + result |= (I3_XKB_GROUP_MASK_4 << 16); return result; } @@ -260,7 +268,7 @@ CFGFUN(floating_maximum_size, const long width, const long height) { } CFGFUN(floating_modifier, const char *modifiers) { - config.floating_modifier = modifiers_from_str(modifiers); + config.floating_modifier = event_state_from_str(modifiers); } CFGFUN(default_orientation, const char *orientation) { diff --git a/src/config_parser.c b/src/config_parser.c index 6393efe9..04423483 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -997,6 +997,7 @@ bool parse_file(const char *f, bool use_nagbar) { yajl_gen_free(config_output->json_gen); check_for_duplicate_bindings(context); + reorder_bindings(); if (use_nagbar && (context->has_errors || context->has_warnings)) { ELOG("FYI: You are using i3 version %s\n", i3_version); diff --git a/src/handlers.c b/src/handlers.c index 714e183b..1daefbc9 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -16,7 +16,6 @@ #include #include #include -#include #define SN_API_NOT_YET_FROZEN 1 #include @@ -264,7 +263,7 @@ static void handle_mapping_notify(xcb_mapping_notify_event_t *event) { ungrab_all_keys(conn); translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); return; } @@ -1338,7 +1337,9 @@ void handle_event(int type, xcb_generic_event_t *event) { keysyms = xcb_key_symbols_alloc(conn); ungrab_all_keys(conn); translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); + if (((xcb_xkb_new_keyboard_notify_event_t *)event)->changed & XCB_XKB_NKN_DETAIL_KEYCODES) + (void)load_keymap(); } else if (state->xkbType == XCB_XKB_MAP_NOTIFY) { if (event_is_ignored(event->sequence, type)) { DLOG("Ignoring map notify event for sequence %d.\n", state->sequence); @@ -1349,22 +1350,16 @@ void handle_event(int type, xcb_generic_event_t *event) { keysyms = xcb_key_symbols_alloc(conn); ungrab_all_keys(conn); translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); + (void)load_keymap(); } } else if (state->xkbType == XCB_XKB_STATE_NOTIFY) { DLOG("xkb state group = %d\n", state->group); - - /* See The XKB Extension: Library Specification, section 14.1 */ - /* We check if the current group (each group contains - * two levels) has been changed. Mode_switch activates - * group XCB_XKB_GROUP_2 */ if (xkb_current_group == state->group) return; xkb_current_group = state->group; - DLOG("Mode_switch %s\n", (xkb_current_group == XCB_XKB_GROUP_1 ? "disabled" : "enabled")); ungrab_all_keys(conn); - translate_keysyms(); - grab_all_keys(conn, (xkb_current_group == XCB_XKB_GROUP_2)); + grab_all_keys(conn); } return; diff --git a/src/i3.mk b/src/i3.mk index 76c1da89..8472106e 100644 --- a/src/i3.mk +++ b/src/i3.mk @@ -5,8 +5,8 @@ CLEAN_TARGETS += clean-i3 i3_SOURCES := $(filter-out $(i3_SOURCES_GENERATED),$(wildcard src/*.c)) i3_HEADERS_CMDPARSER := $(wildcard include/GENERATED_*.h) i3_HEADERS := $(filter-out $(i3_HEADERS_CMDPARSER),$(wildcard include/*.h)) -i3_CFLAGS = $(XKB_COMMON_CFLAGS) $(XCB_CFLAGS) $(XCB_KBD_CFLAGS) $(XCB_WM_CFLAGS) $(XCURSOR_CFLAGS) $(PANGO_CFLAGS) $(YAJL_CFLAGS) $(LIBEV_CFLAGS) $(PCRE_CFLAGS) $(LIBSN_CFLAGS) -i3_LIBS = $(XKB_COMMON_LIBS) $(XCB_LIBS) $(XCB_XKB_LIBS) $(XCB_KBD_LIBS) $(XCB_WM_LIBS) $(XCURSOR_LIBS) $(PANGO_LIBS) $(YAJL_LIBS) $(LIBEV_LIBS) $(PCRE_LIBS) $(LIBSN_LIBS) -lm -lpthread +i3_CFLAGS = $(XKB_COMMON_CFLAGS) $(XKB_COMMON_X11_CFLAGS) $(XCB_CFLAGS) $(XCB_KBD_CFLAGS) $(XCB_WM_CFLAGS) $(XCURSOR_CFLAGS) $(PANGO_CFLAGS) $(YAJL_CFLAGS) $(LIBEV_CFLAGS) $(PCRE_CFLAGS) $(LIBSN_CFLAGS) +i3_LIBS = $(XKB_COMMON_LIBS) $(XKB_COMMON_X11_LIBS) $(XCB_LIBS) $(XCB_XKB_LIBS) $(XCB_KBD_LIBS) $(XCB_WM_LIBS) $(XCURSOR_LIBS) $(PANGO_LIBS) $(YAJL_LIBS) $(LIBEV_LIBS) $(PCRE_LIBS) $(LIBSN_LIBS) -lm -lpthread # When using clang, we use pre-compiled headers to speed up the build. With # gcc, this actually makes the build slower. diff --git a/src/ipc.c b/src/ipc.c index 021bdd7b..ad7ef1cb 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -121,56 +121,92 @@ static void dump_rect(yajl_gen gen, const char *name, Rect r) { y(map_close); } -static void dump_binding(yajl_gen gen, Binding *bind) { - y(map_open); - ystr("input_code"); - y(integer, bind->keycode); - - ystr("input_type"); - ystr((const char *)(bind->input_type == B_KEYBOARD ? "keyboard" : "mouse")); - - ystr("symbol"); - if (bind->symbol == NULL) - y(null); - else - ystr(bind->symbol); - - ystr("command"); - ystr(bind->command); - - ystr("mods"); +static void dump_event_state_mask(yajl_gen gen, Binding *bind) { y(array_open); - for (int i = 0; i < 8; i++) { - if (bind->mods & (1 << i)) { + for (int i = 0; i < 20; i++) { + if (bind->event_state_mask & (1 << i)) { switch (1 << i) { - case XCB_MOD_MASK_SHIFT: + case XCB_KEY_BUT_MASK_SHIFT: ystr("shift"); break; - case XCB_MOD_MASK_LOCK: + case XCB_KEY_BUT_MASK_LOCK: ystr("lock"); break; - case XCB_MOD_MASK_CONTROL: + case XCB_KEY_BUT_MASK_CONTROL: ystr("ctrl"); break; - case XCB_MOD_MASK_1: + case XCB_KEY_BUT_MASK_MOD_1: ystr("Mod1"); break; - case XCB_MOD_MASK_2: + case XCB_KEY_BUT_MASK_MOD_2: ystr("Mod2"); break; - case XCB_MOD_MASK_3: + case XCB_KEY_BUT_MASK_MOD_3: ystr("Mod3"); break; - case XCB_MOD_MASK_4: + case XCB_KEY_BUT_MASK_MOD_4: ystr("Mod4"); break; - case XCB_MOD_MASK_5: + case XCB_KEY_BUT_MASK_MOD_5: ystr("Mod5"); break; + case XCB_KEY_BUT_MASK_BUTTON_1: + ystr("Button1"); + break; + case XCB_KEY_BUT_MASK_BUTTON_2: + ystr("Button2"); + break; + case XCB_KEY_BUT_MASK_BUTTON_3: + ystr("Button3"); + break; + case XCB_KEY_BUT_MASK_BUTTON_4: + ystr("Button4"); + break; + case XCB_KEY_BUT_MASK_BUTTON_5: + ystr("Button5"); + break; + case (I3_XKB_GROUP_MASK_1 << 16): + ystr("Group1"); + break; + case (I3_XKB_GROUP_MASK_2 << 16): + ystr("Group2"); + break; + case (I3_XKB_GROUP_MASK_3 << 16): + ystr("Group3"); + break; + case (I3_XKB_GROUP_MASK_4 << 16): + ystr("Group4"); + break; } } } y(array_close); +} + +static void dump_binding(yajl_gen gen, Binding *bind) { + y(map_open); + ystr("input_code"); + y(integer, bind->keycode); + + ystr("input_type"); + ystr((const char *)(bind->input_type == B_KEYBOARD ? "keyboard" : "mouse")); + + ystr("symbol"); + if (bind->symbol == NULL) + y(null); + else + ystr(bind->symbol); + + ystr("command"); + ystr(bind->command); + + // This key is only provided for compatibility, new programs should use + // event_state_mask instead. + ystr("mods"); + dump_event_state_mask(gen, bind); + + ystr("event_state_mask"); + dump_event_state_mask(gen, bind); y(map_close); } diff --git a/src/key_press.c b/src/key_press.c index 88d09a0c..aa6d8150 100644 --- a/src/key_press.c +++ b/src/key_press.c @@ -18,11 +18,11 @@ * */ void handle_key_press(xcb_key_press_event_t *event) { - bool key_release = (event->response_type == XCB_KEY_RELEASE); + const bool key_release = (event->response_type == XCB_KEY_RELEASE); last_timestamp = event->time; - DLOG("%s %d, state raw = %d\n", (key_release ? "KeyRelease" : "KeyPress"), event->detail, event->state); + DLOG("%s %d, state raw = 0x%x\n", (key_release ? "KeyRelease" : "KeyPress"), event->detail, event->state); Binding *bind = get_binding_from_xcb_event((xcb_generic_event_t *)event); diff --git a/src/main.c b/src/main.c index 5281d19c..b4ec3720 100644 --- a/src/main.c +++ b/src/main.c @@ -556,6 +556,37 @@ int main(int argc, char *argv[]) { 0xff, 0xff, NULL); + + /* Setting both, XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE and + * XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED, will lead to the + * X server sending us the full XKB state in KeyPress and KeyRelease: + * https://sources.debian.net/src/xorg-server/2:1.17.2-1.1/xkb/xkbEvents.c/?hl=927#L927 + */ + xcb_xkb_per_client_flags_reply_t *pcf_reply; + /* The last three parameters are unset because they are only relevant + * when using a feature called “automatic reset of boolean controls”: + * http://www.x.org/releases/X11R7.7/doc/kbproto/xkbproto.html#Automatic_Reset_of_Boolean_Controls + * */ + pcf_reply = xcb_xkb_per_client_flags_reply( + conn, + xcb_xkb_per_client_flags( + conn, + XCB_XKB_ID_USE_CORE_KBD, + XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE | XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED, + XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE | XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED, + 0 /* uint32_t ctrlsToChange */, + 0 /* uint32_t autoCtrls */, + 0 /* uint32_t autoCtrlsValues */), + NULL); + if (pcf_reply == NULL || + !(pcf_reply->value & XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE)) { + ELOG("Could not set XCB_XKB_PER_CLIENT_FLAG_GRABS_USE_XKB_STATE\n"); + } + if (pcf_reply == NULL || + !(pcf_reply->value & XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED)) { + ELOG("Could not set XCB_XKB_PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED\n"); + } + free(pcf_reply); xkb_base = extreply->first_event; } @@ -569,8 +600,11 @@ int main(int argc, char *argv[]) { xcb_numlock_mask = aio_get_mod_mask_for(XCB_NUM_LOCK, keysyms); + if (!load_keymap()) + die("Could not load keymap\n"); + translate_keysyms(); - grab_all_keys(conn, false); + grab_all_keys(conn); bool needs_tree_init = true; if (layout_path) {