Skip to content

Commit 81bc29b

Browse files
committed
state: Enable shortcuts tweak
Currently each toolkit (Gtk, Qt, etc.) implements its strategy to deal with keyboard shortcuts on non-Latin layouts. There are some edge cases where this does not work: - Punctuation is usually not remapped. E.g. the standard Israeli layout cannot remap its key <AD01> “slash” to the US layout “q”. - If one have multiple Latin layouts, shorcuts may be positioned differently. E.g. US Qwerty and French Azerty have different positions for Ctrl+Q, while the user might want all shortcuts to be positioned independently of the layout. This commit enables tweaking keyboard shortcuts by adding the option to switch to a different layout when some modifiers are active, typically `Control`, `Alt` or `Super`. The target layout can be set individually for each source layout, although the most common use case would probably be the same target for all layouts. Added to the keymap compile options API: - `xkb_state_machine_options_shortcuts_update_mods()` - `xkb_state_machine_options_shortcuts_set_mapping()`
1 parent 199cdb7 commit 81bc29b

File tree

8 files changed

+438
-10
lines changed

8 files changed

+438
-10
lines changed

changes/api/+753.feature.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Added options to configure the active layout when specific modifiers are active,
2+
enabling controlling which layout is used with keyboard shortcuts.
3+
4+
Examples of use cases (they may overlap):
5+
- Latin shortcuts for non-Latin layouts
6+
- Qwerty/Qwertz/Azerty/etc. shortcuts for non-standard Latin layouts
7+
- Shortcuts independent of the active layout in setups with multiple layouts
8+
9+
It works by specifying:
10+
- A *modifier mask* to watch, typically `Control`, `Alt` and `Super`.
11+
- A *mapping*: source layout → target shortcuts layout.
12+
13+
Then whenever the active modifiers contain *some* of the modifiers of the mask
14+
defined hereinabove, the active layout is switched to the target layout defined
15+
in the mapping, if any, otherwise it is left unchanged.
16+
17+
Added API:
18+
- `xkb_state_machine_options::xkb_state_machine_options_shortcuts_update_mods()`
19+
- `xkb_state_machine_options::xkb_state_machine_options_shortcuts_set_mapping()`

include/xkbcommon/xkbcommon.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,6 +2114,45 @@ xkb_state_machine_options_update_a11y_flags(
21142114
enum xkb_state_accessibility_flags flags
21152115
);
21162116

2117+
/**
2118+
* Set the modifiers that trigger keyboard shortcuts tweak.
2119+
*
2120+
* @param options The state options object to modify.
2121+
* @param affect See @p mask.
2122+
* @param mask Modifiers to set. Only the modifiers in @p affect
2123+
* are considered.
2124+
*
2125+
* @sa `xkb_state_machine_options_shortcuts_set_mapping()`
2126+
* @since 1.14.0
2127+
* @memberof xkb_state_machine_options
2128+
*/
2129+
XKB_EXPORT int
2130+
xkb_state_machine_options_shortcuts_update_mods(
2131+
struct xkb_state_machine_options* restrict options,
2132+
xkb_mod_mask_t affect, xkb_mod_mask_t mask
2133+
);
2134+
2135+
/**
2136+
* Set layout mapping for keyboard shortcuts tweak.
2137+
*
2138+
* Enable tweaking keyboard shortcuts by switching the effective layout when
2139+
* any modifier set by `xkb_state_machine_options_shortcuts_update_mods()`
2140+
* is active.
2141+
*
2142+
* @param options The state options object to modify.
2143+
* @param source Source layout.
2144+
* @param target Target layout.
2145+
*
2146+
* @sa `xkb_state_machine_options_shortcuts_update_mods()`
2147+
* @since 1.14.0
2148+
* @memberof xkb_state_machine_options
2149+
*/
2150+
XKB_EXPORT int
2151+
xkb_state_machine_options_shortcuts_set_mapping(
2152+
struct xkb_state_machine_options* restrict options,
2153+
xkb_layout_index_t source, xkb_layout_index_t target
2154+
);
2155+
21172156
/**
21182157
* @struct xkb_event
21192158
* Opaque keyboard state event object.

src/state.c

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2216,19 +2216,53 @@ xkb_state_key_get_consumed_mods(struct xkb_state *state, xkb_keycode_t kc)
22162216
* bloated for *clients* applications.
22172217
*/
22182218
struct xkb_state_machine {
2219+
/** Internal state */
22192220
struct xkb_state state;
2221+
2222+
/** Extra event API-specific state data */
2223+
struct state_machine_data {
2224+
/** Configuration */
2225+
struct state_machine_config {
2226+
/** Shortcuts tweak */
2227+
struct state_machine_shortcuts_config {
2228+
/** Modifier mask to trigger tweak */
2229+
xkb_mod_mask_t mask;
2230+
/** Target layouts targets */
2231+
xkb_layout_index_t *targets;
2232+
} shortcuts;
2233+
} config;
2234+
/** State */
2235+
struct {
2236+
/** The “real” base group, before shortcuts tweak */
2237+
int32_t base_group;
2238+
} state;
2239+
} extra;
22202240
};
22212241

22222242
struct xkb_state_machine_options {
2243+
/** Accessibility flags */
22232244
enum xkb_state_accessibility_flags a11y_affect;
22242245
enum xkb_state_accessibility_flags a11y_flags;
2246+
2247+
/** Shortcuts tweak */
2248+
struct xkb_shortcuts_config_options {
2249+
/** Modifier mask to trigger tweak */
2250+
xkb_mod_mask_t mask;
2251+
/** Target layouts targets */
2252+
darray(xkb_layout_index_t) targets;
2253+
} shortcuts;
2254+
22252255
struct xkb_context *ctx;
22262256
};
22272257

22282258
#define state_machine_options_new(context) {\
22292259
.a11y_affect = XKB_STATE_A11Y_NO_FLAGS, \
22302260
.a11y_flags = XKB_STATE_A11Y_NO_FLAGS, \
2231-
.ctx = (context) \
2261+
.shortcuts = { \
2262+
.mask = 0, \
2263+
.targets = darray_new(), \
2264+
}, \
2265+
.ctx = (context), \
22322266
}
22332267

22342268
/* Default state options */
@@ -2253,6 +2287,8 @@ xkb_state_machine_options_destroy(struct xkb_state_machine_options *options)
22532287
{
22542288
if (options == NULL)
22552289
return;
2290+
2291+
darray_free(options->shortcuts.targets);
22562292
xkb_context_unref(options->ctx);
22572293
free(options);
22582294
}
@@ -2282,6 +2318,110 @@ xkb_state_machine_options_update_a11y_flags(
22822318
return 0;
22832319
}
22842320

2321+
int
2322+
xkb_state_machine_options_shortcuts_update_mods(
2323+
struct xkb_state_machine_options* restrict options,
2324+
xkb_mod_mask_t affect, xkb_mod_mask_t mask
2325+
)
2326+
{
2327+
options->shortcuts.mask &= ~affect;
2328+
options->shortcuts.mask |= (mask & affect);
2329+
return 0;
2330+
}
2331+
2332+
static int
2333+
state_machine_set_shortcuts_reference_layout(
2334+
struct xkb_shortcuts_config_options* restrict options,
2335+
xkb_layout_index_t source, xkb_layout_index_t target
2336+
)
2337+
{
2338+
if (source >= XKB_MAX_GROUPS || target >= XKB_MAX_GROUPS)
2339+
return 1;
2340+
2341+
if (target == source) {
2342+
/* Skip default setting */
2343+
return 0;
2344+
}
2345+
2346+
/* Resize array & initialize new entries, if relevant */
2347+
if (source >= darray_size(options->targets)) {
2348+
xkb_layout_index_t new = darray_size(options->targets);
2349+
darray_resize(options->targets, source + 1);
2350+
for (; new < source; new++)
2351+
darray_item(options->targets, new) =
2352+
XKB_LAYOUT_INVALID;
2353+
}
2354+
2355+
darray_item(options->targets, source) = (source == target)
2356+
? XKB_LAYOUT_INVALID
2357+
: target;
2358+
return 0;
2359+
}
2360+
2361+
int
2362+
xkb_state_machine_options_shortcuts_set_mapping(
2363+
struct xkb_state_machine_options* restrict options,
2364+
xkb_layout_index_t source, xkb_layout_index_t target
2365+
)
2366+
{
2367+
return state_machine_set_shortcuts_reference_layout(&options->shortcuts,
2368+
source, target);
2369+
}
2370+
2371+
static bool
2372+
state_machine_set_shortcuts(struct xkb_state_machine *sm,
2373+
const struct xkb_shortcuts_config_options *options)
2374+
{
2375+
struct xkb_keymap * const restrict keymap = sm->state.keymap;
2376+
2377+
/* Consider only defined layouts */
2378+
xkb_layout_index_t count = MIN(
2379+
keymap->num_groups,
2380+
(xkb_layout_index_t) darray_size(options->targets)
2381+
);
2382+
/* Drop layout entries with default setting or invalid group */
2383+
static_assert(XKB_LAYOUT_INVALID > XKB_MAX_GROUPS, "");
2384+
while (count > 1) {
2385+
if (darray_item(options->targets, count - 1) <
2386+
keymap->num_groups)
2387+
break;
2388+
count--;
2389+
}
2390+
if (!count)
2391+
return true;
2392+
2393+
xkb_mod_mask_t mask = options->mask;
2394+
if (mask) {
2395+
/* Sanitize mask */
2396+
mask &= (UINT64_C(1) << xkb_keymap_num_mods(keymap)) - 1;
2397+
} else {
2398+
/* Default mask */
2399+
// TODO
2400+
}
2401+
if (!mask)
2402+
return true;
2403+
2404+
xkb_layout_index_t * const targets = calloc(keymap->num_groups,
2405+
sizeof(*targets));
2406+
if (!targets)
2407+
return false;
2408+
2409+
for (xkb_layout_index_t l = 0; l < count; l++) {
2410+
/* Sanitize layouts targets */
2411+
targets[l] = (darray_item(options->targets, l) <
2412+
keymap->num_groups)
2413+
? darray_item(options->targets, l)
2414+
: XKB_LAYOUT_INVALID;
2415+
}
2416+
for (xkb_layout_index_t l = count; l < keymap->num_groups; l++) {
2417+
targets[l] = XKB_LAYOUT_INVALID;
2418+
}
2419+
2420+
sm->extra.config.shortcuts.mask = mask;
2421+
sm->extra.config.shortcuts.targets = targets;
2422+
return true;
2423+
}
2424+
22852425
struct xkb_state_machine *
22862426
xkb_state_machine_new(struct xkb_keymap *keymap,
22872427
const struct xkb_state_machine_options *options)
@@ -2295,7 +2435,19 @@ xkb_state_machine_new(struct xkb_keymap *keymap,
22952435

22962436
xkb_state_init(&sm->state, keymap,
22972437
options->a11y_affect, options->a11y_flags);
2438+
sm->extra.state.base_group = (int32_t) XKB_LAYOUT_INVALID;
2439+
2440+
/* Shortcuts */
2441+
if (!darray_empty(options->shortcuts.targets)) {
2442+
if (!state_machine_set_shortcuts(sm, &options->shortcuts))
2443+
goto error;
2444+
}
2445+
22982446
return sm;
2447+
2448+
error:
2449+
xkb_state_machine_unref(sm);
2450+
return NULL;
22992451
}
23002452

23012453
struct xkb_state_machine *
@@ -2314,6 +2466,7 @@ xkb_state_machine_unref(struct xkb_state_machine *sm)
23142466
return;
23152467

23162468
xkb_state_destroy(&sm->state);
2469+
free(sm->extra.config.shortcuts.targets);
23172470
free(sm);
23182471
}
23192472

@@ -2337,6 +2490,8 @@ xkb_state_machine_update_controls(struct xkb_state_machine *sm,
23372490
&sm->state, events, affect, controls
23382491
);
23392492

2493+
// FIXME: shortcut tweak
2494+
23402495
if (changed) {
23412496
/* Create event only if some component actually changed */
23422497
darray_append(events->queue, (struct xkb_event) {
@@ -2379,6 +2534,8 @@ xkb_state_machine_update_latched_locked(struct xkb_state_machine *sm,
23792534
locked_layout
23802535
);
23812536

2537+
// FIXME: shortcut tweak
2538+
23822539
if (changed) {
23832540
/* Create event only if some component actually changed */
23842541
darray_append(events->queue, (struct xkb_event) {
@@ -2402,12 +2559,18 @@ xkb_state_machine_update_key(struct xkb_state_machine *sm,
24022559
events->next = 0;
24032560

24042561
struct xkb_state * restrict const state = &sm->state;
2562+
struct state_machine_data * restrict const extra = &sm->extra;
24052563
const struct xkb_key * const key = XkbKey(state->keymap, kc);
24062564
if (key == NULL)
24072565
return 0;
24082566

24092567
const struct state_components prev_components = state->components;
24102568

2569+
if (extra->state.base_group != (int32_t) XKB_LAYOUT_INVALID) {
2570+
/* Restore the real base group, but not the effective group */
2571+
state->components.base_group = extra->state.base_group;
2572+
}
2573+
24112574
state->set_mods = 0;
24122575
state->clear_mods = 0;
24132576

@@ -2439,6 +2602,25 @@ xkb_state_machine_update_key(struct xkb_state_machine *sm,
24392602

24402603
xkb_state_update_derived(state);
24412604

2605+
bool used_shortcuts_tweak = false;
2606+
if (extra->config.shortcuts.targets &&
2607+
(state->components.mods & extra->config.shortcuts.mask) &&
2608+
(extra->config.shortcuts.targets[state->components.group] !=
2609+
XKB_LAYOUT_INVALID)) {
2610+
/* Activate shortcuts tweak */
2611+
extra->state.base_group = state->components.base_group;
2612+
state->components.base_group
2613+
= (int32_t) extra->config.shortcuts.targets[state->components.group]
2614+
- state->components.latched_group
2615+
- state->components.locked_group;
2616+
// TODO: could we avoid recomputing everything?
2617+
xkb_state_update_derived(state);
2618+
used_shortcuts_tweak = true;
2619+
} else {
2620+
/* Deactivate shortcuts tweak */
2621+
extra->state.base_group = (int32_t) XKB_LAYOUT_INVALID;
2622+
}
2623+
24422624
const enum xkb_state_component changed =
24432625
get_state_component_changes(&prev_components, &state->components);
24442626

@@ -2454,6 +2636,8 @@ xkb_state_machine_update_key(struct xkb_state_machine *sm,
24542636
: XKB_EVENT_TYPE_KEY_UP,
24552637
.keycode = kc
24562638
});
2639+
} else if (used_shortcuts_tweak) {
2640+
// FIXME: we need to prepend the tweak
24572641
}
24582642

24592643
if (changed) {

0 commit comments

Comments
 (0)