Skip to content

Commit

Permalink
New combo configuration options
Browse files Browse the repository at this point in the history
`COMBO_MUST_PRESS_IN_ORDER` to force combos to only activate if the keys
are pressed in the same order as they are defined in the combo's key
array.

Per combo configuration can be modified by defining
`COMBO_MUST_PRESS_IN_ORDER_PER_COMBO` and defining
`bool get_combo_must_press_in_order(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)`
function in which you choose which combos require the correct key order.

`COMBO_SHOULD_TRIGGER` and its companying function
`bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record)`
enable disallowing combos to be activated when you choose so. For
example on certain layers, or when a timer is running.
  • Loading branch information
sevanteri committed Nov 6, 2021
1 parent 6c507af commit 7e1f9b3
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 4 deletions.
43 changes: 40 additions & 3 deletions docs/feature_combo.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,14 @@ 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
combo_index
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 needs to be pressed in order.
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 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.
Expand All @@ -153,6 +157,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
Expand Down Expand Up @@ -216,6 +221,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
Expand Down
39 changes: 38 additions & 1 deletion quantum/process_keycode/process_combo.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,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
Expand Down Expand Up @@ -334,6 +342,28 @@ combo_t* overlaps(combo_t *combo1, combo_t *combo2) {
return combo1;
}

#ifdef COMBO_MUST_PRESS_IN_ORDER
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;
Expand All @@ -344,7 +374,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()
#ifdef COMBO_MUST_PRESS_IN_ORDER
&& 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);
Expand Down

0 comments on commit 7e1f9b3

Please sign in to comment.