mirror of
https://github.com/labwc/labwc.git
synced 2026-02-05 04:06:33 -05:00
This was forgotten in 65bd32d625
Reported-by: @jonhiggs (thanks)
Also stop treating the synthetic layout change sym as modifier
but still prevent it from being added to the set of pressed keys.
Additionally slightly reformat the code.
557 lines
16 KiB
C
557 lines
16 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
#include <assert.h>
|
|
#include <wlr/backend/multi.h>
|
|
#include <wlr/backend/session.h>
|
|
#include <wlr/interfaces/wlr_keyboard.h>
|
|
#include "action.h"
|
|
#include "idle.h"
|
|
#include "key-state.h"
|
|
#include "labwc.h"
|
|
#include "menu/menu.h"
|
|
#include "regions.h"
|
|
#include "view.h"
|
|
#include "workspaces.h"
|
|
|
|
static bool should_cancel_cycling_on_next_key_release;
|
|
|
|
static void
|
|
change_vt(struct server *server, unsigned int vt)
|
|
{
|
|
if (!wlr_backend_is_multi(server->backend)) {
|
|
return;
|
|
}
|
|
struct wlr_session *session = wlr_backend_get_session(server->backend);
|
|
if (session) {
|
|
wlr_session_change_vt(session, vt);
|
|
}
|
|
}
|
|
|
|
bool
|
|
keyboard_any_modifiers_pressed(struct wlr_keyboard *keyboard)
|
|
{
|
|
xkb_mod_index_t i;
|
|
for (i = 0; i < xkb_keymap_num_mods(keyboard->keymap); i++) {
|
|
if (xkb_state_mod_index_is_active(keyboard->xkb_state,
|
|
i, XKB_STATE_MODS_DEPRESSED)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
end_cycling(struct server *server)
|
|
{
|
|
if (server->osd_state.cycle_view) {
|
|
desktop_focus_view(server->osd_state.cycle_view,
|
|
/*raise*/ true);
|
|
}
|
|
|
|
/* osd_finish() additionally resets cycle_view to NULL */
|
|
osd_finish(server);
|
|
should_cancel_cycling_on_next_key_release = false;
|
|
}
|
|
|
|
void
|
|
keyboard_modifiers_notify(struct wl_listener *listener, void *data)
|
|
{
|
|
struct keyboard *keyboard = wl_container_of(listener, keyboard, modifier);
|
|
struct seat *seat = keyboard->base.seat;
|
|
struct server *server = seat->server;
|
|
struct wlr_keyboard *wlr_keyboard = keyboard->wlr_keyboard;
|
|
|
|
if (server->input_mode == LAB_INPUT_STATE_MOVE) {
|
|
/* Any change to the modifier state re-enable region snap */
|
|
seat->region_prevent_snap = false;
|
|
}
|
|
|
|
if (server->osd_state.cycle_view || server->grabbed_view
|
|
|| seat->workspace_osd_shown_by_modifier) {
|
|
if (!keyboard_any_modifiers_pressed(wlr_keyboard)) {
|
|
if (server->osd_state.cycle_view) {
|
|
if (key_state_nr_bound_keys()) {
|
|
should_cancel_cycling_on_next_key_release = true;
|
|
} else {
|
|
end_cycling(server);
|
|
}
|
|
}
|
|
if (seat->workspace_osd_shown_by_modifier) {
|
|
workspaces_osd_hide(seat);
|
|
}
|
|
if (server->grabbed_view) {
|
|
regions_hide_overlay(seat);
|
|
}
|
|
}
|
|
}
|
|
wlr_seat_keyboard_notify_modifiers(seat->seat, &wlr_keyboard->modifiers);
|
|
}
|
|
|
|
static bool
|
|
handle_keybinding(struct server *server, uint32_t modifiers, xkb_keysym_t sym, xkb_keycode_t code)
|
|
{
|
|
struct keybind *keybind;
|
|
wl_list_for_each(keybind, &rc.keybinds, link) {
|
|
if (modifiers ^ keybind->modifiers) {
|
|
continue;
|
|
}
|
|
if (server->seat.nr_inhibited_keybind_views
|
|
&& server->focused_view
|
|
&& server->focused_view->inhibits_keybinds
|
|
&& !actions_contain_toggle_keybinds(&keybind->actions)) {
|
|
continue;
|
|
}
|
|
if (sym == XKB_KEY_NoSymbol) {
|
|
/* Use keycodes */
|
|
for (size_t i = 0; i < keybind->keycodes_len; i++) {
|
|
if (keybind->keycodes[i] == code) {
|
|
key_state_store_pressed_keys_as_bound();
|
|
actions_run(NULL, server, &keybind->actions, 0);
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
/* Use syms */
|
|
for (size_t i = 0; i < keybind->keysyms_len; i++) {
|
|
if (xkb_keysym_to_lower(sym) == keybind->keysyms[i]) {
|
|
key_state_store_pressed_keys_as_bound();
|
|
actions_run(NULL, server, &keybind->actions, 0);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
is_modifier_key(xkb_keysym_t sym)
|
|
{
|
|
switch (sym) {
|
|
case XKB_KEY_Shift_L: case XKB_KEY_Shift_R:
|
|
case XKB_KEY_Alt_L: case XKB_KEY_Alt_R:
|
|
case XKB_KEY_Control_L: case XKB_KEY_Control_R:
|
|
case XKB_KEY_Super_L: case XKB_KEY_Super_R:
|
|
case XKB_KEY_Hyper_L: case XKB_KEY_Hyper_R:
|
|
case XKB_KEY_Meta_L: case XKB_KEY_Meta_R:
|
|
case XKB_KEY_Mode_switch:
|
|
case XKB_KEY_ISO_Level3_Shift:
|
|
case XKB_KEY_ISO_Level5_Shift:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
struct keysyms {
|
|
const xkb_keysym_t *syms;
|
|
int nr_syms;
|
|
};
|
|
|
|
static void
|
|
handle_menu_keys(struct server *server, struct keysyms *syms)
|
|
{
|
|
assert(server->input_mode == LAB_INPUT_STATE_MENU);
|
|
|
|
for (int i = 0; i < syms->nr_syms; i++) {
|
|
switch (syms->syms[i]) {
|
|
case XKB_KEY_Down:
|
|
menu_item_select_next(server);
|
|
break;
|
|
case XKB_KEY_Up:
|
|
menu_item_select_previous(server);
|
|
break;
|
|
case XKB_KEY_Right:
|
|
menu_submenu_enter(server);
|
|
break;
|
|
case XKB_KEY_Left:
|
|
menu_submenu_leave(server);
|
|
break;
|
|
case XKB_KEY_Return:
|
|
if (menu_call_selected_actions(server)) {
|
|
menu_close_root(server);
|
|
cursor_update_focus(server);
|
|
}
|
|
break;
|
|
case XKB_KEY_Escape:
|
|
menu_close_root(server);
|
|
cursor_update_focus(server);
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
handle_compositor_keybindings(struct keyboard *keyboard,
|
|
struct wlr_keyboard_key_event *event)
|
|
{
|
|
struct seat *seat = keyboard->base.seat;
|
|
struct server *server = seat->server;
|
|
struct wlr_keyboard *wlr_keyboard = keyboard->wlr_keyboard;
|
|
|
|
/* Translate libinput keycode -> xkbcommon */
|
|
uint32_t keycode = event->keycode + 8;
|
|
|
|
/* Get a list of keysyms based on the keymap for this keyboard */
|
|
struct keysyms translated = { 0 };
|
|
translated.nr_syms = xkb_state_key_get_syms(wlr_keyboard->xkb_state,
|
|
keycode, &translated.syms);
|
|
|
|
/*
|
|
* Get keysyms from the keyboard as if there was no modifier
|
|
* translations. For example, get Shift+1 rather than Shift+! (with US
|
|
* keyboard layout).
|
|
*/
|
|
struct keysyms raw = { 0 };
|
|
xkb_layout_index_t layout_index =
|
|
xkb_state_key_get_layout(wlr_keyboard->xkb_state, keycode);
|
|
raw.nr_syms = xkb_keymap_key_get_syms_by_level(wlr_keyboard->keymap,
|
|
keycode, layout_index, 0, &raw.syms);
|
|
|
|
bool handled = false;
|
|
|
|
/*
|
|
* keyboard_key_notify() is called before keyboard_key_modifier(), so
|
|
* 'modifiers' refers to modifiers that were pressed before the key
|
|
* event in hand. Consequently, we use is_modifier_key() to find out if
|
|
* the key event being processed is a modifier.
|
|
*
|
|
* Sway solves this differently by saving the 'modifiers' state and
|
|
* checking if it has changed each time we get to the equivalent of this
|
|
* function. If it has changed, it concludes that the last key was a
|
|
* modifier and then deletes it from the buffer of pressed keycodes.
|
|
* For us the equivalent would look something like this:
|
|
*
|
|
* static uint32_t last_modifiers;
|
|
* bool last_key_was_a_modifier = last_modifiers != modifiers;
|
|
* last_modifiers = modifiers;
|
|
* if (last_key_was_a_modifier) {
|
|
* key_state_remove_last_pressed_key(last_pressed_keycode);
|
|
* }
|
|
*/
|
|
|
|
bool is_modifier = false;
|
|
bool is_layout_switch = false;
|
|
uint32_t modifiers = wlr_keyboard_get_modifiers(wlr_keyboard);
|
|
|
|
for (int i = 0; i < translated.nr_syms; i++) {
|
|
is_modifier |= is_modifier_key(translated.syms[i]);
|
|
is_layout_switch |= translated.syms[i] == XKB_KEY_ISO_Next_Group;
|
|
}
|
|
|
|
if (!is_modifier && !is_layout_switch) {
|
|
key_state_set_pressed(event->keycode,
|
|
event->state == WL_KEYBOARD_KEY_STATE_PRESSED);
|
|
}
|
|
|
|
/*
|
|
* If a user lets go of the modifier (e.g. alt) before the 'normal' key
|
|
* (e.g. tab) when window-cycling, we do not end the cycling until both
|
|
* keys have been released. If we end the window-cycling on release of
|
|
* the modifier only, some XWayland clients such as hexchat realise that
|
|
* tab is pressed (even though we did not forward the event) and because
|
|
* we absorb the equivalent release event it gets stuck on repeat.
|
|
*/
|
|
if (should_cancel_cycling_on_next_key_release
|
|
&& event->state == WL_KEYBOARD_KEY_STATE_RELEASED) {
|
|
end_cycling(server);
|
|
handled = true;
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* If a press event was handled by a compositor binding, then do not
|
|
* forward the corresponding release event to clients
|
|
*/
|
|
if (key_state_corresponding_press_event_was_bound(event->keycode)
|
|
&& event->state == WL_KEYBOARD_KEY_STATE_RELEASED) {
|
|
key_state_bound_key_remove(event->keycode);
|
|
return true;
|
|
}
|
|
|
|
/* Catch C-A-F1 to C-A-F12 to change tty */
|
|
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
for (int i = 0; i < translated.nr_syms; i++) {
|
|
unsigned int vt = translated.syms[i] - XKB_KEY_XF86Switch_VT_1 + 1;
|
|
if (vt >= 1 && vt <= 12) {
|
|
change_vt(server, vt);
|
|
/*
|
|
* Don't send any key events to clients when
|
|
* changing tty
|
|
*/
|
|
handled = true;
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Ignore labwc keybindings if input is inhibited
|
|
* It's important to do this after key_state_set_pressed() to ensure
|
|
* _all_ key press/releases are registered
|
|
*/
|
|
if (seat->active_client_while_inhibited) {
|
|
return false;
|
|
}
|
|
if (seat->server->session_lock) {
|
|
return false;
|
|
}
|
|
|
|
if (server->input_mode == LAB_INPUT_STATE_MENU) {
|
|
/*
|
|
* Usually, release events are already caught via _press_event_was_bound().
|
|
* But to be on the safe side we will simply ignore them here as well.
|
|
*/
|
|
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
handle_menu_keys(server, &translated);
|
|
}
|
|
handled = true;
|
|
goto out;
|
|
}
|
|
|
|
if (server->osd_state.cycle_view) {
|
|
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
for (int i = 0; i < translated.nr_syms; i++) {
|
|
if (translated.syms[i] == XKB_KEY_Escape) {
|
|
/*
|
|
* Cancel view-cycle
|
|
*
|
|
* osd_finish() additionally resets
|
|
* cycle_view to NULL
|
|
*/
|
|
osd_preview_restore(server);
|
|
osd_finish(server);
|
|
|
|
handled = true;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/* cycle to next */
|
|
bool backwards = modifiers & WLR_MODIFIER_SHIFT;
|
|
if (!is_modifier) {
|
|
enum lab_cycle_dir dir = backwards
|
|
? LAB_CYCLE_DIR_BACKWARD
|
|
: LAB_CYCLE_DIR_FORWARD;
|
|
server->osd_state.cycle_view = desktop_cycle_view(server,
|
|
server->osd_state.cycle_view, dir);
|
|
osd_update(server);
|
|
}
|
|
}
|
|
/* don't send any key events to clients when osd onscreen */
|
|
handled = true;
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* A keybind is not considered valid if other keys are pressed at the
|
|
* same time.
|
|
*
|
|
* In labwc, a keybind is defined by one/many modifier keys + _one_
|
|
* non-modifier key. Returning early on >1 pressed non-modifier keys
|
|
* avoids false positive matches where 'other' keys were pressed at the
|
|
* same time.
|
|
*/
|
|
if (key_state_nr_pressed_keys() > 1) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Handle compositor keybinds
|
|
*
|
|
* When matching against keybinds, we process the input keys in the
|
|
* following order of precedence:
|
|
* a. Keycodes (of physical keys) (not if keybind is layoutDependent)
|
|
* b. Translated keysyms (taking into account modifiers, so if Shift+1
|
|
* were pressed on a us keyboard, the keysym would be '!')
|
|
* c. Raw keysyms (ignoring modifiers such as shift, so in the above
|
|
* example the keysym would just be '1')
|
|
*
|
|
* The reasons for this approach are:
|
|
* 1. To make keybinds keyboard-layout agnostic (by checking keycodes
|
|
* before keysyms). This means that in a multi-layout situation,
|
|
* keybinds work regardless of which layout is active at the time
|
|
* of the key-press.
|
|
* 2. To support keybinds relating to keysyms that are only available
|
|
* in a particular layout, for example å, ä and ö.
|
|
* 3. To support keybinds that are only valid with a modifier, for
|
|
* example the numpad keys with NumLock enabled: KP_x. These would
|
|
* only be matched by the translated keysyms.
|
|
* 4. To support keybinds such as `S-1` (by checking raw keysyms).
|
|
*
|
|
* Reason 4 will also be satisfied by matching the keycodes. However,
|
|
* when a keybind is configured to be layoutDependent we still need
|
|
* the raw keysym fallback.
|
|
*/
|
|
|
|
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
/* First try keycodes */
|
|
handled |= handle_keybinding(server, modifiers, XKB_KEY_NoSymbol, keycode);
|
|
if (handled) {
|
|
wlr_log(WLR_DEBUG, "keycodes matched");
|
|
goto out;
|
|
}
|
|
|
|
/* Then fall back to keysyms */
|
|
for (int i = 0; i < translated.nr_syms; i++) {
|
|
handled |= handle_keybinding(server, modifiers,
|
|
translated.syms[i], keycode);
|
|
}
|
|
if (handled) {
|
|
wlr_log(WLR_DEBUG, "translated keysyms matched");
|
|
goto out;
|
|
}
|
|
|
|
/* And finally test for keysyms without modifier */
|
|
for (int i = 0; i < raw.nr_syms; i++) {
|
|
handled |= handle_keybinding(server, modifiers, raw.syms[i], keycode);
|
|
}
|
|
if (handled) {
|
|
wlr_log(WLR_DEBUG, "raw keysyms matched");
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (handled) {
|
|
key_state_store_pressed_keys_as_bound();
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
static int
|
|
handle_keybind_repeat(void *data)
|
|
{
|
|
struct keyboard *keyboard = data;
|
|
assert(keyboard->keybind_repeat);
|
|
assert(keyboard->keybind_repeat_rate > 0);
|
|
|
|
/* synthesize event */
|
|
struct wlr_keyboard_key_event event = {
|
|
.keycode = keyboard->keybind_repeat_keycode,
|
|
.state = WL_KEYBOARD_KEY_STATE_PRESSED
|
|
};
|
|
|
|
handle_compositor_keybindings(keyboard, &event);
|
|
int next_repeat_ms = 1000 / keyboard->keybind_repeat_rate;
|
|
wl_event_source_timer_update(keyboard->keybind_repeat,
|
|
next_repeat_ms);
|
|
|
|
return 0; /* ignored per wl_event_loop docs */
|
|
}
|
|
|
|
static void
|
|
start_keybind_repeat(struct server *server, struct keyboard *keyboard,
|
|
struct wlr_keyboard_key_event *event)
|
|
{
|
|
struct wlr_keyboard *wlr_keyboard = keyboard->wlr_keyboard;
|
|
assert(!keyboard->keybind_repeat);
|
|
|
|
if (wlr_keyboard->repeat_info.rate > 0
|
|
&& wlr_keyboard->repeat_info.delay > 0) {
|
|
keyboard->keybind_repeat_keycode = event->keycode;
|
|
keyboard->keybind_repeat_rate = wlr_keyboard->repeat_info.rate;
|
|
keyboard->keybind_repeat = wl_event_loop_add_timer(
|
|
server->wl_event_loop, handle_keybind_repeat, keyboard);
|
|
wl_event_source_timer_update(keyboard->keybind_repeat,
|
|
wlr_keyboard->repeat_info.delay);
|
|
}
|
|
}
|
|
|
|
void
|
|
keyboard_cancel_keybind_repeat(struct keyboard *keyboard)
|
|
{
|
|
if (keyboard->keybind_repeat) {
|
|
wl_event_source_remove(keyboard->keybind_repeat);
|
|
keyboard->keybind_repeat = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
keyboard_key_notify(struct wl_listener *listener, void *data)
|
|
{
|
|
/* This event is raised when a key is pressed or released. */
|
|
struct keyboard *keyboard = wl_container_of(listener, keyboard, key);
|
|
struct seat *seat = keyboard->base.seat;
|
|
struct wlr_keyboard_key_event *event = data;
|
|
struct wlr_seat *wlr_seat = seat->seat;
|
|
struct wlr_keyboard *wlr_keyboard = keyboard->wlr_keyboard;
|
|
idle_manager_notify_activity(seat->seat);
|
|
|
|
/* any new press/release cancels current keybind repeat */
|
|
keyboard_cancel_keybind_repeat(keyboard);
|
|
|
|
bool handled = handle_compositor_keybindings(keyboard, event);
|
|
if (handled) {
|
|
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
start_keybind_repeat(seat->server, keyboard, event);
|
|
}
|
|
} else {
|
|
wlr_seat_set_keyboard(wlr_seat, wlr_keyboard);
|
|
wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec,
|
|
event->keycode, event->state);
|
|
}
|
|
}
|
|
|
|
void
|
|
keyboard_set_numlock(struct wlr_keyboard *keyboard)
|
|
{
|
|
xkb_mod_index_t num_idx =
|
|
xkb_map_mod_get_index(keyboard->keymap, XKB_MOD_NAME_NUM);
|
|
if (num_idx == XKB_MOD_INVALID) {
|
|
wlr_log(WLR_INFO, "Failed to set Num Lock: not found in keymap");
|
|
return;
|
|
}
|
|
|
|
xkb_mod_mask_t locked = keyboard->modifiers.locked;
|
|
if (rc.kb_numlock_enable) {
|
|
locked |= (xkb_mod_mask_t)1 << num_idx;
|
|
} else {
|
|
locked &= ~((xkb_mod_mask_t)1 << num_idx);
|
|
}
|
|
|
|
/*
|
|
* This updates the xkb-state + kb->modifiers and also triggers the
|
|
* keyboard->events.modifiers signal (the signal has no effect in
|
|
* current labwc usage since the keyboard is not part of a
|
|
* keyboard-group yet).
|
|
*/
|
|
wlr_keyboard_notify_modifiers(keyboard, keyboard->modifiers.depressed,
|
|
keyboard->modifiers.latched, locked, keyboard->modifiers.group);
|
|
}
|
|
|
|
void
|
|
keyboard_init(struct seat *seat)
|
|
{
|
|
seat->keyboard_group = wlr_keyboard_group_create();
|
|
struct wlr_keyboard *kb = &seat->keyboard_group->keyboard;
|
|
struct xkb_rule_names rules = { 0 };
|
|
struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
struct xkb_keymap *keymap = xkb_map_new_from_names(context, &rules,
|
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
if (keymap) {
|
|
wlr_keyboard_set_keymap(kb, keymap);
|
|
xkb_keymap_unref(keymap);
|
|
} else {
|
|
wlr_log(WLR_ERROR, "Failed to create xkb keymap");
|
|
}
|
|
xkb_context_unref(context);
|
|
wlr_keyboard_set_repeat_info(kb, rc.repeat_rate, rc.repeat_delay);
|
|
|
|
keybind_update_keycodes(seat->server);
|
|
}
|
|
|
|
void
|
|
keyboard_finish(struct seat *seat)
|
|
{
|
|
/*
|
|
* All keyboard listeners must be removed before this to avoid use after
|
|
* free
|
|
*/
|
|
if (seat->keyboard_group) {
|
|
wlr_keyboard_group_destroy(seat->keyboard_group);
|
|
seat->keyboard_group = NULL;
|
|
}
|
|
}
|