From 21958a93434e40db904eb731bc75b65661bcb9ae Mon Sep 17 00:00:00 2001 From: Pete Sevander Date: Sun, 9 Jan 2022 22:02:25 +0200 Subject: [PATCH] New combo configuration options (#15083) Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> --- docs/feature_combo.md | 42 +++++++++++++++++++++++-- quantum/process_keycode/process_combo.c | 39 ++++++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/docs/feature_combo.md b/docs/feature_combo.md index 47128c431bf..c0e10f09d5a 100644 --- a/docs/feature_combo.md +++ b/docs/feature_combo.md @@ -141,10 +141,13 @@ Processing combos has two buffers, one for the key presses, another for the comb ## Modifier Combos If a combo resolves to a Modifier, the window for processing the combo can be extended independently from normal combos. By default, this is disabled but can be enabled with `#define COMBO_MUST_HOLD_MODS`, and the time window can be configured with `#define COMBO_HOLD_TERM 150` (default: `TAPPING_TERM`). With `COMBO_MUST_HOLD_MODS`, you cannot tap the combo any more which makes the combo less prone to misfires. -## Per Combo Timing, Holding and Tapping -For each combo, it is possible to configure the time window it has to pressed in, if it needs to be held down, or if it needs to be tapped. +## Strict key press order +By defining `COMBO_MUST_PRESS_IN_ORDER` combos only activate when the keys are pressed in the same order as they are defined in the key array. -For example, tap-only combos are useful if any (or all) of the underlying keys is a Mod-Tap or a Layer-Tap key. When you tap the combo, you get the combo result. When you press the combo and hold it down, the combo doesn't actually activate. Instead the keys are processed separately as if the combo wasn't even there. +## Per Combo Timing, Holding, Tapping and Key Press Order +For each combo, it is possible to configure the time window it has to pressed in, if it needs to be held down, if it needs to be tapped, or if its keys need to be pressed in order. + +For example, tap-only combos are useful if any (or all) of the underlying keys are mod-tap or layer-tap keys. When you tap the combo, you get the combo result. When you press the combo and hold it down, the combo doesn't activate. Instead the keys are processed separately as if the combo wasn't even there. In order to use these features, the following configuration options and functions need to be defined. Coming up with useful timings and configuration is left as an exercise for the reader. @@ -153,6 +156,7 @@ In order to use these features, the following configuration options and function | `COMBO_TERM_PER_COMBO` | uint16_t get_combo_term(uint16_t index, combo_t \*combo) | Optional per-combo timeout window. (default: `COMBO_TERM`) | | `COMBO_MUST_HOLD_PER_COMBO` | bool get_combo_must_hold(uint16_t index, combo_t \*combo) | Controls if a given combo should fire immediately on tap or if it needs to be held. (default: `false`) | | `COMBO_MUST_TAP_PER_COMBO` | bool get_combo_must_tap(uint16_t index, combo_t \*combo) | Controls if a given combo should fire only if tapped within `COMBO_HOLD_TERM`. (default: `false`) | +| `COMBO_MUST_PRESS_IN_ORDER_PER_COMBO` | bool get_combo_must_press_in_order(uint16_t index, combo_t \*combo) | Controls if a given combo should fire only if its keys are pressed in order. (default: `true`) | Examples: ```c @@ -216,6 +220,38 @@ bool get_combo_must_tap(uint16_t index, combo_t *combo) { return false; } + +bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) { + switch (combo_index) { + /* List combos here that you want to only activate if their keys + * are pressed in the same order as they are defined in the combo's key + * array. */ + case COMBO_NAME_HERE: + return true; + default: + return false; + } +} +``` + +## Generic hook to (dis)allow a combo activation + +By defining `COMBO_SHOULD_TRIGGER` and its companying function `bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)` you can block or allow combos to activate on the conditions of your choice. +For example, you could disallow some combos on the base layer and allow them on another. Or disable combos on the home row when a timer is running. + +Examples: +```c +bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) { + /* Disable combo `SOME_COMBO` on layer `_LAYER_A` */ + switch (combo_index) { + case SOME_COMBO: + if (layer_state_is(_LAYER_A)) { + return false; + } + } + + return true; +} ``` ## Variable Length Combos diff --git a/quantum/process_keycode/process_combo.c b/quantum/process_keycode/process_combo.c index a050161edf8..8040ede5289 100644 --- a/quantum/process_keycode/process_combo.c +++ b/quantum/process_keycode/process_combo.c @@ -40,10 +40,18 @@ __attribute__((weak)) bool get_combo_must_tap(uint16_t index, combo_t *combo) { __attribute__((weak)) uint16_t get_combo_term(uint16_t index, combo_t *combo) { return COMBO_TERM; } #endif +#ifdef COMBO_MUST_PRESS_IN_ORDER_PER_COMBO +__attribute__((weak)) bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo) { return true; } +#endif + #ifdef COMBO_PROCESS_KEY_RELEASE __attribute__((weak)) bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) { return false; } #endif +#ifdef COMBO_SHOULD_TRIGGER +__attribute__((weak)) bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) { return true; } +#endif + #ifndef COMBO_NO_TIMER static uint16_t timer = 0; #endif @@ -350,6 +358,28 @@ combo_t *overlaps(combo_t *combo1, combo_t *combo2) { return combo1; } +#if defined(COMBO_MUST_PRESS_IN_ORDER) || defined(COMBO_MUST_PRESS_IN_ORDER_PER_COMBO) +static bool keys_pressed_in_order(uint16_t combo_index, combo_t *combo, uint16_t key_index, uint16_t keycode, keyrecord_t *record) { +# ifdef COMBO_MUST_PRESS_IN_ORDER_PER_COMBO + if (!get_combo_must_press_in_order(combo_index, combo)) { + return true; + } +# endif + if ( + // The `state` bit for the key being pressed. + (1 << key_index) == + // The *next* combo key's bit. + (COMBO_STATE(combo) + 1) + // E.g. two keys already pressed: `state == 11`. + // Next possible `state` is `111`. + // So the needed bit is `100` which we get with `11 + 1`. + ) { + return true; + } + return false; +} +#endif + static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) { uint8_t key_count = 0; uint16_t key_index = -1; @@ -360,7 +390,14 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t * return false; } - bool key_is_part_of_combo = !COMBO_DISABLED(combo) && is_combo_enabled(); + bool key_is_part_of_combo = (!COMBO_DISABLED(combo) && is_combo_enabled() +#if defined(COMBO_MUST_PRESS_IN_ORDER) || defined(COMBO_MUST_PRESS_IN_ORDER_PER_COMBO) + && keys_pressed_in_order(combo_index, combo, key_index, keycode, record) +#endif +#ifdef COMBO_SHOULD_TRIGGER + && combo_should_trigger(combo_index, combo, keycode, record) +#endif + ); if (record->event.pressed && key_is_part_of_combo) { uint16_t time = _get_combo_term(combo_index, combo);