Skip to content

Commit

Permalink
state: add server API for updating latched and locked mods & layout
Browse files Browse the repository at this point in the history
Up to now, the “server state” `xkb_state` API only offered one entry
point to update the server state – `xkb_state_update_key`, which reflects
the direct keyboard keys state. But some updates come out-of-band from
keyboard input events stream, for example, a GUI layout switcher.

The X11 XKB protocol has a request which allows for such updates,
`XkbLatchLockState`[^1], but xkbcommon does not have similar
functionality. So server applications ended up using
`xkb_state_update_state` for this, but that’s a function intended for
client applications, not servers.

Add support for updating the latched & locked state of the mods and
layout. Note that the depressed states cannot be updated in this way --
XKB does not expect them to be updated out of band.

[^1]: https://www.x.org/releases/X11R7.7/doc/kbproto/xkbproto.html#Querying_and_Changing_Keyboard_State

Fixes: #310
Signed-off-by: Ran Benita <[email protected]>
Co-authored-by: Ran Benita <[email protected]>
Co-authored-by: Pierre Le Marre <[email protected]>
  • Loading branch information
wismill and bluetech committed Dec 3, 2024
1 parent 3a54d92 commit 01b64bb
Show file tree
Hide file tree
Showing 8 changed files with 602 additions and 6 deletions.
57 changes: 57 additions & 0 deletions include/xkbcommon/xkbcommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
#define _XKBCOMMON_H_

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdarg.h>

Expand Down Expand Up @@ -1471,6 +1472,62 @@ enum xkb_state_component
xkb_state_update_key(struct xkb_state *state, xkb_keycode_t key,
enum xkb_key_direction direction);

/**
* Update the keyboard state to change the latched and locked state of
* the modifiers and layout.
*
* This entry point is intended for *server* applications and should not be used
* by *client* applications; see @ref server-client-state for details.
*
* Use this function to update the latched and locked state according to
* “out of band” (non-device) inputs, such as UI layout switchers.
*
* @par Layout out of range
* @parblock
*
* If the effective layout, after taking into account the depressed, latched and
* locked layout, is out of range (negative or greater than the maximum layout),
* it is brought into range. Currently, the layout is wrapped using integer
* modulus (with negative values wrapping from the end). The wrapping behavior
* may be made configurable in the future.
*
* @endparblock
*
* @param affect_latched_mods
* @param latched_mods
* Modifiers to set as latched or unlatched. Only modifiers in
* `affect_latched_mods` are considered.
* @param affect_latched_layout
* @param latched_layout
* Layout to latch. Only considered if `affect_latched_layout` is true.
* Maybe be out of range (including negative) -- see note above.
* @param affect_locked_mods
* @param locked_mods
* Modifiers to set as locked or unlocked. Only modifiers in
* `affect_locked_mods` are considered.
* @param affect_locked_layout
* @param locked_layout
* Layout to lock. Only considered if `affect_locked_layout` is true.
* Maybe be out of range (including negative) -- see note above.
*
* @returns A mask of state components that have changed as a result of
* the update. If nothing in the state has changed, returns 0.
*
* @memberof xkb_state
*
* @sa xkb_state_update_mask()
*/
enum xkb_state_component
xkb_state_update_latched_locked(struct xkb_state *state,
xkb_mod_mask_t affect_latched_mods,
xkb_mod_mask_t latched_mods,
bool affect_latched_layout,
int32_t latched_layout,
xkb_mod_mask_t affect_locked_mods,
xkb_mod_mask_t locked_mods,
bool affect_locked_layout,
int32_t locked_layout);

/**
* Update a keyboard state from a set of explicit masks.
*
Expand Down
13 changes: 13 additions & 0 deletions src/keymap.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ enum xkb_action_type {
ACTION_TYPE_CTRL_SET,
ACTION_TYPE_CTRL_LOCK,
ACTION_TYPE_PRIVATE,
ACTION_TYPE_INTERNAL, /* Action specific and internal to xkbcommon */
_ACTION_TYPE_NUM_ENTRIES
};

Expand Down Expand Up @@ -234,6 +235,17 @@ struct xkb_private_action {
uint8_t data[7];
};

enum xkb_internal_action_flags {
INTERNAL_BREAKS_GROUP_LATCH = (1 << 0),
INTERNAL_BREAKS_MOD_LATCH = (1 << 1),
};

/* Action specific and internal to xkbcommon */
struct xkb_internal_action {
enum xkb_action_type type;
enum xkb_internal_action_flags flags;
};

union xkb_action {
enum xkb_action_type type;
struct xkb_mod_action mods;
Expand All @@ -244,6 +256,7 @@ union xkb_action {
struct xkb_pointer_action ptr;
struct xkb_pointer_button_action btn;
struct xkb_private_action priv;
struct xkb_internal_action internal;
};

struct xkb_key_type_entry {
Expand Down
160 changes: 155 additions & 5 deletions src/state.c
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ xkb_filter_group_lock_func(struct xkb_state *state,
}

static bool
xkb_action_breaks_latch(const union xkb_action *action)
xkb_action_breaks_latch(const union xkb_action *action,
enum xkb_internal_action_flags flag)
{
switch (action->type) {
case ACTION_TYPE_NONE:
Expand All @@ -333,6 +334,8 @@ xkb_action_breaks_latch(const union xkb_action *action)
case ACTION_TYPE_SWITCH_VT:
case ACTION_TYPE_TERMINATE:
return true;
case ACTION_TYPE_INTERNAL:
return action->internal.flags & flag;
default:
return false;
}
Expand Down Expand Up @@ -415,7 +418,8 @@ xkb_filter_group_latch_func(struct xkb_state *state,
/* XXX beep beep! */
return XKB_FILTER_CONSUME;
}
else if (xkb_action_breaks_latch(&(actions[k]))) {
else if (xkb_action_breaks_latch(&(actions[k]),
INTERNAL_BREAKS_GROUP_LATCH)) {
/* Breaks the latch */
state->components.latched_group = 0;
filter->func = NULL;
Expand Down Expand Up @@ -571,9 +575,10 @@ xkb_filter_mod_latch_func(struct xkb_state *state,
/* XXX beep beep! */
return XKB_FILTER_CONSUME;
}
else if (xkb_action_breaks_latch(&(actions[k]))) {
else if (xkb_action_breaks_latch(&(actions[k]),
INTERNAL_BREAKS_MOD_LATCH)) {
/* XXX: This may be totally broken, we might need to break the
* latch in the next run after this press? */
* latch in the next run after this press? */
state->components.latched_mods &= ~filter->action.mods.mods.mask;
filter->func = NULL;
return XKB_FILTER_CONTINUE;
Expand Down Expand Up @@ -903,6 +908,151 @@ xkb_state_update_key(struct xkb_state *state, xkb_keycode_t kc,
return get_state_component_changes(&prev_components, &state->components);
}

/* We need fake keys for update_latch_modifiers and update_latch_group.
* These keys must have at least one level in order to break latches. We need 2
* keys with specific actions in order to update group/mod latches without
* affecting each other. */
static struct xkb_key_type_entry synthetic_key_level_entry = { 0 };
static struct xkb_key_type synthetic_key_type = {
.num_entries = 1,
.num_levels = 1,
.entries = &synthetic_key_level_entry
};
static struct xkb_level synthetic_key_level_break_group_latch = {
.num_syms = 1,
.s = { XKB_KEY_NoSymbol },
.a = { { .internal = {
.type = ACTION_TYPE_INTERNAL,
.flags = INTERNAL_BREAKS_GROUP_LATCH
} } }
};
static struct xkb_group synthetic_key_group_break_group_latch = {
.type = &synthetic_key_type,
.levels = &synthetic_key_level_break_group_latch
};
static const struct xkb_key synthetic_key_break_group_latch = {
.num_groups = 1,
.groups = &synthetic_key_group_break_group_latch
};
static struct xkb_level synthetic_key_level_break_mod_latch = {
.num_syms = 1,
.s = { XKB_KEY_NoSymbol },
.a = { { .internal = {
.type = ACTION_TYPE_INTERNAL,
.flags = INTERNAL_BREAKS_MOD_LATCH
} } }
};
static struct xkb_group synthetic_key_group_break_mod_latch = {
.type = &synthetic_key_type,
.levels = &synthetic_key_level_break_mod_latch
};
static const struct xkb_key synthetic_key_break_mod_latch = {
.num_groups = 1,
.groups = &synthetic_key_group_break_mod_latch
};

/* Transcription from xserver: XkbLatchModifiers */
static void
update_latch_modifiers(struct xkb_state *state,
xkb_mod_mask_t mask, xkb_mod_mask_t latches)
{
const struct xkb_key *key = &synthetic_key_break_mod_latch;

/* Clear affected latched modifiers */
const xkb_mod_mask_t clear =
mod_mask_get_effective(state->keymap, mask & ~latches);
state->components.latched_mods &= ~clear;

/* Clear any pending latch to locks. */
xkb_filter_apply_all(state, key, XKB_KEY_DOWN);

/* Simulate tapping a key with a modifier latch action */
const union xkb_action latch_mods = {
.mods = {
.type = ACTION_TYPE_MOD_LATCH,
.mods = {
.mask = mod_mask_get_effective(state->keymap, mask & latches)
},
.flags = 0,
},
};
struct xkb_filter *filter = xkb_filter_new(state);
filter->key = key;
filter->func = xkb_filter_mod_latch_func;
filter->action = latch_mods;
xkb_filter_mod_latch_new(state, filter);
/* We added the filter manually, so only fire “up” event */
xkb_filter_mod_latch_func(state, filter, key, XKB_KEY_UP);
}

/* Transcription from xserver: XkbLatchGroup */
static void
update_latch_group(struct xkb_state *state, int32_t group)
{
/* Simulate tapping a key with a group latch action, but in isolation: i.e.
* without affecting the other filters. */
const struct xkb_key *key = &synthetic_key_break_group_latch;

/* Clear any pending latch to locks. */
xkb_filter_apply_all(state, key, XKB_KEY_DOWN);

const union xkb_action latch_group = {
.group = {
.type = ACTION_TYPE_GROUP_LATCH,
.flags = 0,
.group = group,
},
};
struct xkb_filter *filter = xkb_filter_new(state);
filter->key = key;
filter->func = xkb_filter_group_latch_func;
filter->action = latch_group;
xkb_filter_group_latch_new(state, filter);
/* We added the filter manually, so only fire “up” event */
xkb_filter_group_latch_func(state, filter, key, XKB_KEY_UP);
}

XKB_EXPORT enum xkb_state_component
xkb_state_update_latched_locked(struct xkb_state *state,
xkb_mod_mask_t affect_latched_mods,
xkb_mod_mask_t latched_mods,
bool affect_latched_layout,
int32_t latched_layout,
xkb_mod_mask_t affect_locked_mods,
xkb_mod_mask_t locked_mods,
bool affect_locked_layout,
int32_t locked_layout)
{
const struct state_components prev_components = state->components;
/* Only include modifiers which exist in the keymap. */
const xkb_mod_mask_t mask =
(xkb_mod_mask_t) ((1ull << xkb_keymap_num_mods(state->keymap)) - 1u);

/* Update locks */
affect_locked_mods &= mask;
if (affect_locked_mods) {
state->components.locked_mods &= ~affect_locked_mods;
state->components.locked_mods |= locked_mods & affect_locked_mods;
state->components.locked_mods =
mod_mask_get_effective(state->keymap, state->components.locked_mods);
}
if (affect_locked_layout) {
state->components.locked_group = locked_layout;
}

/* Update latches */
affect_latched_mods &= mask;
if (affect_latched_mods) {
update_latch_modifiers(state, affect_latched_mods, latched_mods);
}
if (affect_latched_layout) {
update_latch_group(state, latched_layout);
}

xkb_state_update_derived(state);
return get_state_component_changes(&prev_components, &state->components);
}

/**
* Updates the state from a set of explicit masks as gained from
* xkb_state_serialize_mods and xkb_state_serialize_groups. As noted in the
Expand Down Expand Up @@ -936,7 +1086,7 @@ xkb_state_update_mask(struct xkb_state *state,
*
* It might seem more reasonable to do this only for components.mods
* in xkb_state_update_derived(), rather than for each component
* seperately. That would allow to distinguish between "really"
* separately. That would allow to distinguish between "really"
* depressed mods (would be in MODS_DEPRESSED) and indirectly
* depressed to to a mapping (would only be in MODS_EFFECTIVE).
* However, the traditional behavior of xkb_state_update_key() is that
Expand Down
2 changes: 2 additions & 0 deletions test/data/rules/evdev
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,9 @@
grp:alt_space_toggle = +group(alt_space_toggle)
grp:menu_toggle = +group(menu_toggle)
grp:lwin_toggle = +group(lwin_toggle)
grp:lwin_latch = +group(lwin_latch)
grp:rwin_toggle = +group(rwin_toggle)
grp:rwin_latch_lock_clear = +group(rwin_latch_lock_clear)
grp:lshift_toggle = +group(lshift_toggle)
grp:rshift_toggle = +group(rshift_toggle)
grp:rctrl_switch = +group(rctrl_switch)
Expand Down
16 changes: 16 additions & 0 deletions test/data/symbols/group
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ xkb_symbols "lwin_switch" {
};
};

partial modifier_keys
xkb_symbols "lwin_latch" {
key <LWIN> {
symbols[1] = [ ISO_Group_Latch ],
actions[1] = [ LatchGroup(group=+1) ]
};
};

// The right Win key (while pressed) chooses the second keyboard group.
// (Using this map, you should declare your keyboard as pc101 or pc102
// instead of pc104 or pc105.)
Expand All @@ -45,6 +53,14 @@ xkb_symbols "rwin_switch" {
};
};

partial modifier_keys
xkb_symbols "rwin_latch_lock_clear" {
key <RWIN> {
symbols[1] = [ ISO_Group_Latch ],
actions[1] = [ LatchGroup(group=+1, latchToLock, clearLocks) ]
};
};

// The right Menu key (while pressed) chooses the second keyboard group.
// while Shift+Menu acts as Menu.
partial modifier_keys
Expand Down
Loading

0 comments on commit 01b64bb

Please sign in to comment.