forked from forks/qmk_firmware
7e983796e1
* Combo processing improvements. Now it is possible to use ModTap and LayerTap keys as part of combos. Overlapping combos also don't trigger all the combos, just exactly the one that you press. New settings: - COMBO_MUST_HOLD_MODS - COMBO_MOD_TERM - COMBO_TERM_PER_COMBO - COMBO_MUST_HOLD_PER_COMBO - COMBO_STRICT_TIMER - COMBO_NO_TIMER * Remove the size flags from combo_t struct boolean members. This in the end actually saves space as the members are accessed so many times. The amount of operations needed to access the bits uses more memory than setting the size saves. * Fix `process_combo_key_release` not called correctly with tap-only combos * Fix not passing a pointer when NO_ACTION_TAPPING is defined. * Docs for `COMBO_ONLY_FROM_LAYER` * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update quantum/process_keycode/process_combo.c Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Add `EXTRA_SHORT_COMBOS` option. Stuff combo's `disabled` and `active` flags into `state`. Possibly can save some space. * Add more examples and clarify things with dict management system. - Simple examples now has a combo that has modifiers included. - The slightly more advanced examples now are actually more advanced instead of just `tap_code16(<modded-keycode>)`. - Added a note that `COMBO_ACTION`s are not needed anymore as you can just use custom keycodes. - Added a note that the `g/keymap_combo.h` macros use the `process_combo_event` function and that it is not usable in one's keymap afterwards. * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Change "the" combo action example to "email" example. * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Fix sneaky infinite loop with `combo_disable()` No need to call `dump_key_buffer` when disabling combos because the buffer is either being dumped if a combo-key was pressed, or the buffer is empty if a non-combo-key is pressed. * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> * Update docs/feature_combo.md Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> Co-authored-by: Drashna Jaelre <drashna@live.com>
564 lines
18 KiB
C
564 lines
18 KiB
C
/* Copyright 2016 Jack Humbert
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "print.h"
|
|
#include "process_combo.h"
|
|
#include "action_tapping.h"
|
|
|
|
|
|
#ifdef COMBO_COUNT
|
|
__attribute__((weak)) combo_t key_combos[COMBO_COUNT];
|
|
uint16_t COMBO_LEN = COMBO_COUNT;
|
|
#else
|
|
extern combo_t key_combos[];
|
|
extern uint16_t COMBO_LEN;
|
|
#endif
|
|
|
|
__attribute__((weak)) void process_combo_event(uint16_t combo_index, bool pressed) {}
|
|
|
|
#ifdef COMBO_MUST_HOLD_PER_COMBO
|
|
__attribute__((weak)) bool get_combo_must_hold(uint16_t index, combo_t *combo) { return false; }
|
|
#endif
|
|
|
|
#ifdef COMBO_MUST_TAP_PER_COMBO
|
|
__attribute__((weak)) bool get_combo_must_tap(uint16_t index, combo_t *combo) { return false; }
|
|
#endif
|
|
|
|
#ifdef COMBO_TERM_PER_COMBO
|
|
__attribute__((weak)) uint16_t get_combo_term(uint16_t index, combo_t *combo) { return COMBO_TERM; }
|
|
#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
|
|
|
|
#ifndef COMBO_NO_TIMER
|
|
static uint16_t timer = 0;
|
|
#endif
|
|
static bool b_combo_enable = true; // defaults to enabled
|
|
static uint16_t longest_term = 0;
|
|
|
|
typedef struct {
|
|
keyrecord_t record;
|
|
uint16_t combo_index;
|
|
uint16_t keycode;
|
|
} queued_record_t;
|
|
static uint8_t key_buffer_size = 0;
|
|
static queued_record_t key_buffer[COMBO_KEY_BUFFER_LENGTH];
|
|
|
|
typedef struct {
|
|
uint16_t combo_index;
|
|
} queued_combo_t;
|
|
static uint8_t combo_buffer_write= 0;
|
|
static uint8_t combo_buffer_read = 0;
|
|
static queued_combo_t combo_buffer[COMBO_BUFFER_LENGTH];
|
|
|
|
#define INCREMENT_MOD(i) i = (i + 1) % COMBO_BUFFER_LENGTH
|
|
|
|
#define COMBO_KEY_POS ((keypos_t){.col=254, .row=254})
|
|
|
|
|
|
#ifndef EXTRA_SHORT_COMBOS
|
|
/* flags are their own elements in combo_t struct. */
|
|
# define COMBO_ACTIVE(combo) (combo->active)
|
|
# define COMBO_DISABLED(combo) (combo->disabled)
|
|
# define COMBO_STATE(combo) (combo->state)
|
|
|
|
# define ACTIVATE_COMBO(combo) do {combo->active = true;}while(0)
|
|
# define DEACTIVATE_COMBO(combo) do {combo->active = false;}while(0)
|
|
# define DISABLE_COMBO(combo) do {combo->disabled = true;}while(0)
|
|
# define RESET_COMBO_STATE(combo) do { \
|
|
combo->disabled = false; \
|
|
combo->state = 0; \
|
|
}while(0)
|
|
#else
|
|
/* flags are at the two high bits of state. */
|
|
# define COMBO_ACTIVE(combo) (combo->state & 0x80)
|
|
# define COMBO_DISABLED(combo) (combo->state & 0x40)
|
|
# define COMBO_STATE(combo) (combo->state & 0x3F)
|
|
|
|
# define ACTIVATE_COMBO(combo) do {combo->state |= 0x80;}while(0)
|
|
# define DEACTIVATE_COMBO(combo) do {combo->state &= ~0x80;}while(0)
|
|
# define DISABLE_COMBO(combo) do {combo->state |= 0x40;}while(0)
|
|
# define RESET_COMBO_STATE(combo) do {combo->state &= ~0x7F;}while(0)
|
|
#endif
|
|
|
|
static inline void release_combo(uint16_t combo_index, combo_t *combo) {
|
|
if (combo->keycode) {
|
|
keyrecord_t record = {
|
|
.event = {
|
|
.key = COMBO_KEY_POS,
|
|
.time = timer_read()|1,
|
|
.pressed = false,
|
|
},
|
|
.keycode = combo->keycode,
|
|
};
|
|
#ifndef NO_ACTION_TAPPING
|
|
action_tapping_process(record);
|
|
#else
|
|
process_record(&record);
|
|
#endif
|
|
} else {
|
|
process_combo_event(combo_index, false);
|
|
}
|
|
DEACTIVATE_COMBO(combo);
|
|
}
|
|
|
|
static inline bool _get_combo_must_hold(uint16_t combo_index, combo_t *combo) {
|
|
#ifdef COMBO_NO_TIMER
|
|
return false;
|
|
#elif defined(COMBO_MUST_HOLD_PER_COMBO)
|
|
return get_combo_must_hold(combo_index, combo);
|
|
#elif defined(COMBO_MUST_HOLD_MODS)
|
|
return (KEYCODE_IS_MOD(combo->keycode) ||
|
|
(combo->keycode >= QK_MOMENTARY && combo->keycode <= QK_MOMENTARY_MAX));
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
static inline uint16_t _get_wait_time(uint16_t combo_index, combo_t *combo ) {
|
|
if (_get_combo_must_hold(combo_index, combo)
|
|
#ifdef COMBO_MUST_TAP_PER_COMBO
|
|
|| get_combo_must_tap(combo_index, combo)
|
|
#endif
|
|
) {
|
|
if (longest_term < COMBO_HOLD_TERM) {
|
|
return COMBO_HOLD_TERM;
|
|
}
|
|
}
|
|
|
|
return longest_term;
|
|
}
|
|
|
|
static inline uint16_t _get_combo_term(uint16_t combo_index, combo_t *combo) {
|
|
|
|
#if defined(COMBO_TERM_PER_COMBO)
|
|
return get_combo_term(combo_index, combo);
|
|
#endif
|
|
|
|
return COMBO_TERM;
|
|
}
|
|
|
|
void clear_combos(void) {
|
|
uint16_t index = 0;
|
|
longest_term = 0;
|
|
for (index = 0; index < COMBO_LEN; ++index) {
|
|
combo_t *combo = &key_combos[index];
|
|
if (!COMBO_ACTIVE(combo)) {
|
|
RESET_COMBO_STATE(combo);
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline void dump_key_buffer(void) {
|
|
if (key_buffer_size == 0) {
|
|
return;
|
|
}
|
|
|
|
for (uint8_t key_buffer_i = 0; key_buffer_i < key_buffer_size; key_buffer_i++) {
|
|
|
|
queued_record_t *qrecord = &key_buffer[key_buffer_i];
|
|
keyrecord_t *record = &qrecord->record;
|
|
|
|
if (IS_NOEVENT(record->event)) {
|
|
continue;
|
|
}
|
|
|
|
if (!record->keycode && qrecord->combo_index != (uint16_t)-1) {
|
|
process_combo_event(qrecord->combo_index, true);
|
|
} else {
|
|
#ifndef NO_ACTION_TAPPING
|
|
action_tapping_process(*record);
|
|
#else
|
|
process_record(record);
|
|
#endif
|
|
}
|
|
record->event.time = 0;
|
|
}
|
|
|
|
key_buffer_size = 0;
|
|
}
|
|
|
|
#define NO_COMBO_KEYS_ARE_DOWN (0 == COMBO_STATE(combo))
|
|
#define ALL_COMBO_KEYS_ARE_DOWN(state, key_count) (((1 << key_count) - 1) == state)
|
|
#define ONLY_ONE_KEY_IS_DOWN(state) !(state & (state - 1))
|
|
#define KEY_NOT_YET_RELEASED(state, key_index) ((1 << key_index) & state)
|
|
#define KEY_STATE_DOWN(state, key_index) \
|
|
do { \
|
|
state |= (1 << key_index); \
|
|
} while (0)
|
|
#define KEY_STATE_UP(state, key_index) \
|
|
do { \
|
|
state &= ~(1 << key_index); \
|
|
} while (0)
|
|
|
|
static inline void _find_key_index_and_count(const uint16_t *keys, uint16_t keycode, uint16_t *key_index, uint8_t *key_count) {
|
|
while (true) {
|
|
uint16_t key = pgm_read_word(&keys[*key_count]);
|
|
if (keycode == key) *key_index = *key_count;
|
|
if (COMBO_END == key) break;
|
|
(*key_count)++;
|
|
}
|
|
}
|
|
|
|
void drop_combo_from_buffer(uint16_t combo_index) {
|
|
/* Mark a combo as processed from the buffer. If the buffer is in the
|
|
* beginning of the buffer, drop it. */
|
|
uint8_t i = combo_buffer_read;
|
|
while (i != combo_buffer_write) {
|
|
queued_combo_t *qcombo = &combo_buffer[i];
|
|
|
|
if (qcombo->combo_index == combo_index) {
|
|
combo_t *combo = &key_combos[combo_index];
|
|
DISABLE_COMBO(combo);
|
|
|
|
if (i == combo_buffer_read) {
|
|
INCREMENT_MOD(combo_buffer_read);
|
|
}
|
|
break;
|
|
}
|
|
INCREMENT_MOD(i);
|
|
}
|
|
}
|
|
|
|
void apply_combo(uint16_t combo_index, combo_t *combo) {
|
|
/* Apply combo's result keycode to the last chord key of the combo and
|
|
* disable the other keys. */
|
|
|
|
if (COMBO_DISABLED(combo)) { return; }
|
|
|
|
// state to check against so we find the last key of the combo from the buffer
|
|
#if defined(EXTRA_EXTRA_LONG_COMBOS)
|
|
uint32_t state = 0;
|
|
#elif defined(EXTRA_LONG_COMBOS)
|
|
uint16_t state = 0;
|
|
#else
|
|
uint8_t state = 0;
|
|
#endif
|
|
|
|
for (uint8_t key_buffer_i = 0; key_buffer_i < key_buffer_size; key_buffer_i++) {
|
|
|
|
queued_record_t *qrecord = &key_buffer[key_buffer_i];
|
|
keyrecord_t *record = &qrecord->record;
|
|
uint16_t keycode = qrecord->keycode;
|
|
|
|
uint8_t key_count = 0;
|
|
uint16_t key_index = -1;
|
|
_find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
|
|
|
|
if (-1 == (int16_t)key_index) {
|
|
// key not part of this combo
|
|
continue;
|
|
}
|
|
|
|
KEY_STATE_DOWN(state, key_index);
|
|
if (ALL_COMBO_KEYS_ARE_DOWN(state, key_count)) {
|
|
// this in the end executes the combo when the key_buffer is dumped.
|
|
record->keycode = combo->keycode;
|
|
record->event.key = COMBO_KEY_POS;
|
|
|
|
qrecord->combo_index = combo_index;
|
|
ACTIVATE_COMBO(combo);
|
|
|
|
break;
|
|
} else {
|
|
// key was part of the combo but not the last one, "disable" it
|
|
// by making it a TICK event.
|
|
record->event.time = 0;
|
|
}
|
|
|
|
}
|
|
drop_combo_from_buffer(combo_index);
|
|
}
|
|
|
|
static inline void apply_combos(void) {
|
|
// Apply all buffered normal combos.
|
|
for (uint8_t i = combo_buffer_read;
|
|
i != combo_buffer_write;
|
|
INCREMENT_MOD(i)) {
|
|
|
|
queued_combo_t *buffered_combo = &combo_buffer[i];
|
|
combo_t *combo = &key_combos[buffered_combo->combo_index];
|
|
|
|
#ifdef COMBO_MUST_TAP_PER_COMBO
|
|
if (get_combo_must_tap(buffered_combo->combo_index, combo)) {
|
|
// Tap-only combos are applied on key release only, so let's drop 'em here.
|
|
drop_combo_from_buffer(buffered_combo->combo_index);
|
|
continue;
|
|
}
|
|
#endif
|
|
apply_combo(buffered_combo->combo_index, combo);
|
|
}
|
|
dump_key_buffer();
|
|
clear_combos();
|
|
}
|
|
|
|
combo_t* overlaps(combo_t *combo1, combo_t *combo2) {
|
|
/* Checks if the combos overlap and returns the combo that should be
|
|
* dropped from the combo buffer.
|
|
* The combo that has less keys will be dropped. If they have the same
|
|
* amount of keys, drop combo1. */
|
|
|
|
uint8_t idx1 = 0, idx2 = 0;
|
|
uint16_t key1, key2;
|
|
bool overlaps = false;
|
|
|
|
while ((key1 = pgm_read_word(&combo1->keys[idx1])) != COMBO_END) {
|
|
idx2 = 0;
|
|
while ((key2 = pgm_read_word(&combo2->keys[idx2])) != COMBO_END) {
|
|
if (key1 == key2) overlaps = true;
|
|
idx2 += 1;
|
|
}
|
|
idx1 += 1;
|
|
}
|
|
|
|
if (!overlaps) return NULL;
|
|
if (idx2 < idx1) return combo2;
|
|
return combo1;
|
|
}
|
|
|
|
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;
|
|
_find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
|
|
|
|
/* Continue processing if key isn't part of current combo. */
|
|
if (-1 == (int16_t)key_index) {
|
|
return false;
|
|
}
|
|
|
|
bool key_is_part_of_combo = !COMBO_DISABLED(combo);
|
|
|
|
if (record->event.pressed && !COMBO_DISABLED(combo)) {
|
|
uint16_t time = _get_combo_term(combo_index, combo);
|
|
if (!COMBO_ACTIVE(combo)) {
|
|
KEY_STATE_DOWN(combo->state, key_index);
|
|
if (longest_term < time) {
|
|
longest_term = time;
|
|
}
|
|
}
|
|
if (ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
|
|
/* Combo was fully pressed */
|
|
/* Buffer the combo so we can fire it after COMBO_TERM */
|
|
|
|
#ifndef COMBO_NO_TIMER
|
|
/* Don't buffer this combo if its combo term has passed. */
|
|
if (timer && timer_elapsed(timer) > time) {
|
|
DISABLE_COMBO(combo);
|
|
return true;
|
|
} else
|
|
#endif
|
|
{
|
|
|
|
// disable readied combos that overlap with this combo
|
|
combo_t *drop = NULL;
|
|
for (uint8_t combo_buffer_i = combo_buffer_read;
|
|
combo_buffer_i != combo_buffer_write;
|
|
INCREMENT_MOD(combo_buffer_i)) {
|
|
|
|
queued_combo_t *qcombo = &combo_buffer[combo_buffer_i];
|
|
combo_t *buffered_combo = &key_combos[qcombo->combo_index];
|
|
|
|
if ((drop = overlaps(buffered_combo, combo))) {
|
|
DISABLE_COMBO(drop);
|
|
if (drop == combo) {
|
|
// stop checking for overlaps if dropped combo was current combo.
|
|
break;
|
|
} else if (combo_buffer_i == combo_buffer_read && drop == buffered_combo) {
|
|
/* Drop the disabled buffered combo from the buffer if
|
|
* it is in the beginning of the buffer. */
|
|
INCREMENT_MOD(combo_buffer_read);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (drop != combo) {
|
|
// save this combo to buffer
|
|
combo_buffer[combo_buffer_write] = (queued_combo_t){
|
|
.combo_index=combo_index,
|
|
};
|
|
INCREMENT_MOD(combo_buffer_write);
|
|
|
|
// get possible longer waiting time for tap-/hold-only combos.
|
|
longest_term = _get_wait_time(combo_index, combo);
|
|
}
|
|
} // if timer elapsed end
|
|
|
|
}
|
|
} else {
|
|
// chord releases
|
|
if (!COMBO_ACTIVE(combo) && ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
|
|
/* First key quickly released */
|
|
if (COMBO_DISABLED(combo) || _get_combo_must_hold(combo_index, combo)) {
|
|
// combo wasn't tappable, disable it and drop it from buffer.
|
|
drop_combo_from_buffer(combo_index);
|
|
key_is_part_of_combo = false;
|
|
}
|
|
#ifdef COMBO_MUST_TAP_PER_COMBO
|
|
else if (get_combo_must_tap(combo_index, combo)) {
|
|
// immediately apply tap-only combo
|
|
apply_combo(combo_index, combo);
|
|
apply_combos(); // also apply other prepared combos and dump key buffer
|
|
# ifdef COMBO_PROCESS_KEY_RELEASE
|
|
if (process_combo_key_release(combo_index, combo, key_index, keycode)) {
|
|
release_combo(combo_index, combo);
|
|
}
|
|
# endif
|
|
}
|
|
#endif
|
|
} else if (COMBO_ACTIVE(combo)
|
|
&& ONLY_ONE_KEY_IS_DOWN(COMBO_STATE(combo))
|
|
&& KEY_NOT_YET_RELEASED(COMBO_STATE(combo), key_index)
|
|
) {
|
|
/* last key released */
|
|
release_combo(combo_index, combo);
|
|
key_is_part_of_combo = true;
|
|
|
|
#ifdef COMBO_PROCESS_KEY_RELEASE
|
|
process_combo_key_release(combo_index, combo, key_index, keycode);
|
|
#endif
|
|
} else if (COMBO_ACTIVE(combo)
|
|
&& KEY_NOT_YET_RELEASED(COMBO_STATE(combo), key_index)
|
|
) {
|
|
/* first or middle key released */
|
|
key_is_part_of_combo = true;
|
|
|
|
#ifdef COMBO_PROCESS_KEY_RELEASE
|
|
if (process_combo_key_release(combo_index, combo, key_index, keycode)) {
|
|
release_combo(combo_index, combo);
|
|
}
|
|
#endif
|
|
} else {
|
|
/* The released key was part of an incomplete combo */
|
|
key_is_part_of_combo = false;
|
|
}
|
|
|
|
KEY_STATE_UP(combo->state, key_index);
|
|
}
|
|
|
|
return key_is_part_of_combo;
|
|
}
|
|
|
|
bool process_combo(uint16_t keycode, keyrecord_t *record) {
|
|
bool is_combo_key = false;
|
|
bool no_combo_keys_pressed = true;
|
|
|
|
if (keycode == CMB_ON && record->event.pressed) {
|
|
combo_enable();
|
|
return true;
|
|
}
|
|
|
|
if (keycode == CMB_OFF && record->event.pressed) {
|
|
combo_disable();
|
|
return true;
|
|
}
|
|
|
|
if (keycode == CMB_TOG && record->event.pressed) {
|
|
combo_toggle();
|
|
return true;
|
|
}
|
|
|
|
if (!is_combo_enabled()) {
|
|
return true;
|
|
}
|
|
|
|
#ifdef COMBO_ONLY_FROM_LAYER
|
|
/* Only check keycodes from one layer. */
|
|
keycode = keymap_key_to_keycode(COMBO_ONLY_FROM_LAYER, record->event.key);
|
|
#endif
|
|
|
|
for (uint16_t idx = 0; idx < COMBO_LEN; ++idx) {
|
|
combo_t *combo = &key_combos[idx];
|
|
is_combo_key |= process_single_combo(combo, keycode, record, idx);
|
|
no_combo_keys_pressed = no_combo_keys_pressed && (NO_COMBO_KEYS_ARE_DOWN || COMBO_ACTIVE(combo) || COMBO_DISABLED(combo));
|
|
}
|
|
|
|
if (record->event.pressed && is_combo_key) {
|
|
#ifndef COMBO_NO_TIMER
|
|
# ifdef COMBO_STRICT_TIMER
|
|
if (!timer) {
|
|
// timer is set only on the first key
|
|
timer = timer_read();
|
|
}
|
|
# else
|
|
timer = timer_read();
|
|
# endif
|
|
#endif
|
|
|
|
if (key_buffer_size < COMBO_KEY_BUFFER_LENGTH) {
|
|
key_buffer[key_buffer_size++] = (queued_record_t){
|
|
.record = *record,
|
|
.keycode = keycode,
|
|
.combo_index = -1, // this will be set when applying combos
|
|
};
|
|
}
|
|
} else {
|
|
if (combo_buffer_read != combo_buffer_write) {
|
|
// some combo is prepared
|
|
apply_combos();
|
|
} else {
|
|
// reset state if there are no combo keys pressed at all
|
|
dump_key_buffer();
|
|
#ifndef COMBO_NO_TIMER
|
|
timer = 0;
|
|
#endif
|
|
clear_combos();
|
|
}
|
|
}
|
|
return !is_combo_key;
|
|
}
|
|
|
|
void combo_task(void) {
|
|
if (!b_combo_enable) {
|
|
return;
|
|
}
|
|
|
|
#ifndef COMBO_NO_TIMER
|
|
if (timer && timer_elapsed(timer) > longest_term) {
|
|
if (combo_buffer_read != combo_buffer_write) {
|
|
apply_combos();
|
|
longest_term = 0;
|
|
timer = 0;
|
|
} else {
|
|
dump_key_buffer();
|
|
timer = 0;
|
|
clear_combos();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void combo_enable(void) { b_combo_enable = true; }
|
|
|
|
void combo_disable(void) {
|
|
#ifndef COMBO_NO_TIMER
|
|
timer = 0;
|
|
#endif
|
|
b_combo_enable = false;
|
|
combo_buffer_read = combo_buffer_write;
|
|
}
|
|
|
|
void combo_toggle(void) {
|
|
if (b_combo_enable) {
|
|
combo_disable();
|
|
} else {
|
|
combo_enable();
|
|
}
|
|
}
|
|
|
|
bool is_combo_enabled(void) { return b_combo_enable; }
|