diff --git a/quantum/keyboard.c b/quantum/keyboard.c index 6bb28009103e..a435fcc3da87 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -326,6 +326,9 @@ void quantum_init(void) { #if defined(BLUETOOTH_ENABLE) && defined(OUTPUT_AUTO_ENABLE) set_output(OUTPUT_AUTO); #endif +#ifdef COMBO_ENABLE + combo_enable(); +#endif } /** \brief keyboard_init diff --git a/quantum/process_keycode/process_combo.c b/quantum/process_keycode/process_combo.c index 8040ede5289a..25515525ad29 100644 --- a/quantum/process_keycode/process_combo.c +++ b/quantum/process_keycode/process_combo.c @@ -26,6 +26,173 @@ extern combo_t key_combos[]; extern uint16_t COMBO_LEN; #endif +/* Enabled at matrix initialization, when linked lists are initialized */ +static bool b_combo_enable = false; + +/* Combo guarantees: + * A combo will trigger if and only if + * - all of its keys are pressed within its combo term, and + * - none of the following occurs + * - One of the combo's keys was released before the final key was + * pressed + * - Two events (press and/or release) for the same keycode occur + * between the first and last triggering key presses + * - The combo has the "contiguous" requirement, and an unrelated key + * was pressed between the first and last triggering key presses + * - The combo has the "tap" requirement, but no triggering key is + * released within the hold term + * - The combo has the "hold" requirement, but a triggering key is + * released within the hold term + * - An overlapping combo with higher priority triggers instead + * - The key buffer overflows between the first and last key presses of + * the combo + * + * Combos, and key presses/releases not consumed by combos, will always be + * released in their event order. The event time of a combo is that of its + * FIRST trigger key press (so that the relevant keys are still in the + * buffer for combos requiring detailed event processing) + */ + +/* Combo statuses: + * Inactive: no triggering key presses are available for legal assignment to + * this combo + * - moves to partial status on the next triggering key press + * + * Partial: some, but not all, triggering key presses are available for + * legal assignment to this combo + * - moves to inactive status if any triggering key is released, if any + * triggering key press expires, or if any non-triggering key is pressed + * and the combo is not contiguous + * - moves to complete status if all triggering keys are pressed + * + * Complete: all triggering keys have been pressed, but either the combo + * term has not yet elapsed, or a tap is required has not yet occurred, or a + * hold is required but the hold term has not yet elapsed + * - moves to ripe status if combo/hold term elapses or tap occurs, + * according to combo requirements + * - moves to inactive status if tap occurs for "must hold" combos, or + * combo term elapses with no tap for "must tap" combos + * + * Ripe: all triggering keys have been pressed, the combo term or hold term + * has elapsed if required, and a tap has occurred if required + * - moves to ready status if this is the highest priority combo among all + * overlapping ripe combos, and if there is no higher-priority complete + * (but unripe) combo whose keys form a superset + * - moves to inactive status if a higher priority overlapping combo is + * ripened + * + * Ready: the combo is ready to be activated at the appropriate point in the + * key sequence. Trigger keys are committed to this combo + * - moves to active status when the first triggering key press is the + * oldest key press in the buffer + * + * Active: the combo's event has already been processed, and some its keys + * have not yet been released + * - moves to inactive status when the last of its keys is released + * + * Combo status is not recorded directly in the combo_t structure. Instead, + * all combos of each status are organized into linked lists. There are + * separate linked lists for inactive, active, and ripe combos. In addition, + * each queued key record in the key buffer gives the index of any ready + * combo that will consume that key. Alternatively, if the key record has + * not (yet) been assigned to a ready combo, then the record has a linked + * list of all the (partial and complete, but unripe) combos for which that + * record is the oldest relevant press event. + * + * For combos that are partial or complete, the low bits of the combo state + * give the number of remaining triggering key presses until the combo + * activates. + * + * We use linked lists to organize combos by state, instead of recording + * state directly in the combo_t structure, because we generally want to do + * something with all combos of a particular state, rather than determine + * what the state is of a particular combo. In particular, in order + * handle expiration and ripening of combos, it is convenient to already + * have each combo organized according to its oldest key press, rather than + * having to iterate over all combo/key pairs. It is also convenient to be + * able to sort ripe combos by priority. + * + * For the small number of active combos, more detailed state recording the + * identity of each still-pressed key is required; this is stored in a + * separate buffer. + */ + +#define COMBO_KEY_POS ((keypos_t){.col = 254, .row = 254}) +#define COMBO_NULL_INDEX (((combo_state_t)-1) >> COMBO_STATE_BITS) +#define COMBO_NULL_STATE ((COMBO_NULL_INDEX << COMBO_STATE_BITS)) + +/* Used by combo queue iterators */ +#define GET_NEXT_COMBO_DIRECT(state_ptr) ((*state_ptr) >> COMBO_STATE_BITS) +#define SET_NEXT_COMBO_DIRECT(state_ptr, index) *state_ptr = ((index << COMBO_STATE_BITS) | ((*state_ptr) & ((1 << COMBO_STATE_BITS) - 1))) + +/* Used when we have a combo_t or keyrecord_t */ +#define GET_STATE(key_or_combo) (key_or_combo->state & ((1 << COMBO_STATE_BITS) - 1)) +#define SET_STATE(key_or_combo, value) key_or_combo->state = ((key_or_combo->state & ~((1 << COMBO_STATE_BITS) - 1)) | value) +#define GET_NEXT_COMBO(key_or_combo) (key_or_combo->state >> COMBO_STATE_BITS) +#define SET_NEXT_COMBO(key_or_combo, index) key_or_combo->state = (GET_STATE(key_or_combo) | (index << COMBO_STATE_BITS)) +/* DANGER: no guard against underflow on the state bits */ +#define DECREMENT_STATE(key_or_combo) key_or_combo->state--; + +static combo_state_t inactive_head; +static combo_state_t ripe_head; +static combo_state_t active_head; + +/* Detailed state of active combos, i.e., which keys are still unpressed, is + * recorded here as a bit array for each active combo. + */ +typedef struct { + combo_state_t combo_index; + combo_active_state_t state; +} active_combo_t; + +static active_combo_t active_buffer[COMBO_BUFFER_LENGTH]; +static uint8_t active_combo_count = 0; + +/* Queued key record state is given by a combo_state_t. Like the + * combo_state_t that holds combo_t state, its high bits give a combo index, + * which can be used for a linked list of combos. Its low bits (the "state" + * bits) encode the following possible statuses: + * COMBO_KEY_BASE - any key release. Also, presses that might help + * activate a combo, but require additional presses or ripening + * conditions. If this is the oldest key press in the buffer, and + * the high bits encode COMBO_NULL_INDEX, then the key press is not + * used by any combo and can be emitted as an ordinary press + * COMBO_KEY_RIPE - presses that will be used to activate a combo, but + * for which the specific combo has not yet been determined (e.g., + * because we are waiting for an unripe high priority combo) + * COMBO_KEY_CONSUMED - presses that are being used to activate a + * combo, but will not trigger its event. The high bits indicate + * the combo being activated. + * COMBO_KEY_TRIGGER - the key press that actually triggers a combo + * event. The high bits indicate the combo being activated. + * + * Keys with status COMBO_KEY_CONSUMED or COMBO_KEY_TRIGGER can safely be + * used in the appropriate manner when they reach the front of the key + * buffer, as can keys with status COMBO_KEY_BASE whose high state bits + * encode COMBO_NULL_INDEX. + */ +enum queued_record_state { + COMBO_KEY_BASE, + COMBO_KEY_RIPE, + COMBO_KEY_CONSUMED, + COMBO_KEY_TRIGGER, +}; + +typedef struct { + keyrecord_t record; + combo_state_t state; +} queued_record_t; + +static queued_record_t key_buffer[COMBO_KEY_BUFFER_LENGTH]; +static uint8_t key_buffer_head = 0; +static uint8_t key_buffer_size = 0; +static uint16_t next_expiration = 0; + +#define GET_QUEUED_RECORD(i) &key_buffer[(key_buffer_head + i) % COMBO_KEY_BUFFER_LENGTH] + +/* Returns true if this key is ready to be dumped from the buffer */ +bool is_key_resolved(queued_record_t *qrecord) { return ((qrecord->state == COMBO_NULL_STATE) || GET_STATE(qrecord) >= COMBO_KEY_CONSUMED); } + __attribute__((weak)) void process_combo_event(uint16_t combo_index, bool pressed) {} #ifdef COMBO_MUST_HOLD_PER_COMBO @@ -40,111 +207,20 @@ __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; } +#ifdef COMBO_CONTIGUOUS_PER_COMBO +__attribute__((weak)) bool get_combo_interrupted(uint16_t index, combo_t *combo, keyrecord_t *record) { return true; } #endif -#ifndef COMBO_NO_TIMER -static uint16_t timer = 0; +#ifdef COMBO_DETAILED_EVENTS_PER_COMBO +__attribute__((weak)) bool get_combo_needs_details(uint16_t index, combo_t *combo) { return false; } +__attribute__((weak)) void process_combo_detailed_press(uint16_t index, combo_t *combo, keyrecord_t *triggers) {} +__attribute__((weak)) bool process_combo_detailed_release(uint16_t index, combo_t *combo, uint8_t key_index, uint16_t keycode, keyevent_t *event) { return false; } +static keyrecord_t triggers[MAX_COMBO_LENGTH]; #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); +#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 - } 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 @@ -157,20 +233,6 @@ static inline bool _get_combo_must_hold(uint16_t combo_index, combo_t *combo) { 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); @@ -179,331 +241,599 @@ static inline uint16_t _get_combo_term(uint16_t combo_index, combo_t *combo) { 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 uint16_t _get_hold_term(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 + ) { + return COMBO_HOLD_TERM; } + return _get_combo_term(combo_index, combo); } -static inline void dump_key_buffer(void) { - /* First call start from 0 index; recursive calls need to start from i+1 index */ - static uint8_t key_buffer_next = 0; - - if (key_buffer_size == 0) { - return; - } - - for (uint8_t key_buffer_i = key_buffer_next; key_buffer_i < key_buffer_size; key_buffer_i++) { - key_buffer_next = key_buffer_i + 1; - - queued_record_t *qrecord = &key_buffer[key_buffer_i]; - keyrecord_t * record = &qrecord->record; - - if (IS_NOEVENT(record->event)) { - continue; +bool _combo_has_key(combo_t *combo, uint16_t keycode) { + const uint16_t *keys = combo->keys; + for (uint8_t i = 0;; i++) { + uint16_t key = pgm_read_word(&keys[i]); + if (keycode == key) { + return true; + } else if (COMBO_END == key) { + return false; } + } +} - 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 +uint8_t _get_combo_keycode_index(combo_t *combo, uint16_t keycode) { + const uint16_t *keys = combo->keys; + for (uint8_t i = 0;; i++) { + uint16_t key = pgm_read_word(&keys[i]); + if (keycode == key) { + return i; + } else if (COMBO_END == key) { + return (uint8_t)-1; } - record->event.time = 0; } - - key_buffer_next = 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)++; +uint8_t _get_combo_length(combo_t *combo) { + const uint16_t *keys = combo->keys; + for (uint8_t i = 0;; i++) { + uint16_t key = pgm_read_word(&keys[i]); + if (COMBO_END == key) { + return i; + } } } -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); - } +uint8_t get_combo_overlap(combo_t *combo1, combo_t *combo2) { + const uint16_t *keys = combo1->keys; + uint8_t overlap = 0; + for (uint8_t i = 0;; i++) { + uint16_t key = pgm_read_word(&keys[i]); + if (COMBO_END == key) { break; } - INCREMENT_MOD(i); + if (_combo_has_key(combo2, key)) { + overlap++; + } } + return overlap; } -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. */ +/************************* + * COMBO QUEUE ITERATION * + *************************/ - if (COMBO_DISABLED(combo)) { +typedef struct { + combo_t *combo; + combo_state_t combo_index; + combo_state_t *prev_combo; + bool removed; +} combo_iterator_t; + +/* Have we reached the end of the iteration; usually used only through convenience macro below */ +inline bool combo_iter_is_complete(combo_iterator_t *iter) { return (iter->combo_index == COMBO_NULL_INDEX); } + +/* Move to next iterator; usually used only through convenience macro below */ +void combo_iter_next(combo_iterator_t *iter) { + if (combo_iter_is_complete(iter)) { return; } + if (!iter->removed) { + iter->prev_combo = &iter->combo->state; + } else { + iter->removed = false; + } + iter->combo_index = GET_NEXT_COMBO_DIRECT(iter->prev_combo); + if (iter->combo_index == COMBO_NULL_INDEX) { + iter->combo = NULL; + } else { + iter->combo = &key_combos[iter->combo_index]; + } +} - // 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 +/* Intialize new iterator; usually used only through convenience macro below */ +void combo_iter_init(combo_iterator_t *iter, combo_state_t *head) { + iter->prev_combo = head; + iter->combo_index = 0; + iter->removed = true; + combo_iter_next(iter); +} - 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; +#define ALL_COMBOS_IN_QUEUE(queue, iter) \ + combo_iter_init(iter, queue); \ + !combo_iter_is_complete(iter); \ + combo_iter_next(iter) +#define ALL_COMBOS_IN_KEY(qrecord, iter) ALL_COMBOS_IN_QUEUE(&qrecord->state, iter) + +/* Remove from its queue the combo currently accessed by the iterator */ +inline void combo_iter_remove(combo_iterator_t *iter) { + if (!iter->removed && !combo_iter_is_complete(iter)) { + SET_NEXT_COMBO_DIRECT(iter->prev_combo, GET_NEXT_COMBO(iter->combo)); + iter->removed = true; + } +} - uint8_t key_count = 0; - uint16_t key_index = -1; - _find_key_index_and_count(combo->keys, keycode, &key_index, &key_count); +/* Insert combo immediately before current entry in iterator */ +inline void combo_iter_insert(combo_iterator_t *iter, combo_state_t combo_index, combo_t *combo) { + SET_NEXT_COMBO(combo, iter->combo_index); + SET_NEXT_COMBO_DIRECT(iter->prev_combo, combo_index); +} - if (-1 == (int16_t)key_index) { - // key not part of this combo - continue; - } +/* Insert combo at head of queue */ +inline void combo_queue_insert(combo_state_t *head, combo_state_t combo_index, combo_t *combo) { + SET_NEXT_COMBO(combo, GET_NEXT_COMBO_DIRECT(head)); + SET_NEXT_COMBO_DIRECT(head, combo_index); +} - 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; +/**************************** + * ACTIVE BUFFER MANAGEMENT * + ****************************/ - qrecord->combo_index = combo_index; - ACTIVATE_COMBO(combo); +bool release_from_active(keyrecord_t *record); - 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; +/* Search for active combo state with given combo index and return pointer */ +combo_active_state_t *get_combo_active_state(combo_state_t combo_index) { + for (uint8_t i = 0; i < COMBO_BUFFER_LENGTH; i++) { + active_combo_t *buffer_entry = &active_buffer[i]; + if (buffer_entry->combo_index == combo_index) { + return &buffer_entry->state; } } - drop_combo_from_buffer(combo_index); + return NULL; } -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; +/* Free active state buffer space currently allocated to a particular combo */ +void free_combo_active_state(combo_state_t combo_index) { + for (uint8_t i = 0; i < COMBO_BUFFER_LENGTH; i++) { + active_combo_t *buffer_entry = &active_buffer[i]; + if (buffer_entry->combo_index == combo_index) { + buffer_entry->combo_index = COMBO_NULL_INDEX; + active_combo_count--; + return; } -#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; +void process_active_buffer_overflow(void) { + /* Get oldest active combo (tail of active queue) */ + combo_iterator_t iter; + for (ALL_COMBOS_IN_QUEUE(&active_head, &iter)) { + if (COMBO_NULL_INDEX == GET_NEXT_COMBO(iter.combo)) { + break; + } + } + uint16_t combo_index = iter.combo_index; + combo_t *combo = iter.combo; + combo_state_t *combo_state = NULL; + if (combo != NULL) { + combo_state = get_combo_active_state(iter.combo_index); + } + if (combo_state == NULL) { + /* Called overflow even though there aren't any active combos, or + oldest active combo doesn't have active state... + This should never happen. */ + combo_index = active_buffer[0].combo_index; + if (combo_index == COMBO_NULL_INDEX) { + return; } - idx1 += 1; + combo = &key_combos[combo_index]; + combo_state = &active_buffer[0].state; } - if (!overlaps) return NULL; - if (idx2 < idx1) return combo2; - return combo1; + /* Release the active combo, key by key */ + uint16_t keycode; + for (uint8_t combo_key = 0; (keycode = pgm_read_word(&combo->keys[combo_key])) != COMBO_END; combo_key++) { + if (*combo_state & (1 << combo_key)) { + /* Key is still present in the combo; simulate its release */ + keyrecord_t fake_release = { + .event = + { + .key = COMBO_KEY_POS, + .time = timer_read() | 1, + .pressed = false, + }, + .keycode = keycode, + }; + release_from_active(&fake_release); /*free_combo_active_state called on final release */ + } + } } -#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; +/* Search for unused active combo state buffer space, allocate to specified + * combo, and return pointer */ +combo_active_state_t *init_combo_active_state(combo_state_t combo_index) { + if (active_combo_count == COMBO_BUFFER_LENGTH) { + process_active_buffer_overflow(); } -# 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; + for (uint8_t i = 0; i < COMBO_BUFFER_LENGTH; i++) { + active_combo_t *buffer_entry = &active_buffer[i]; + if (buffer_entry->combo_index == COMBO_NULL_INDEX) { + buffer_entry->combo_index = combo_index; + buffer_entry->state = 0; + active_combo_count++; + return &buffer_entry->state; + } } - return false; + return NULL; } -#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; - _find_key_index_and_count(combo->keys, keycode, &key_index, &key_count); +/******************************** + * RIPENING AND READYING COMBOS * + ********************************/ - /* Continue processing if key isn't part of current combo. */ - if (-1 == (int16_t)key_index) { - return false; +/* Insert into ripe queue, maintaining sort in order of increasing combo index */ +void ripen_combo(combo_state_t combo_index, combo_t *combo) { + combo_iterator_t iter; + if (combo_index > GET_NEXT_COMBO_DIRECT(&ripe_head)) { + combo_queue_insert(&ripe_head, combo_index, combo); + return; } + for (ALL_COMBOS_IN_QUEUE(&ripe_head, &iter)) { + if (combo_index > iter.combo_index) { + break; + } + } + combo_iter_insert(&iter, combo_index, combo); +} - 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); - if (!COMBO_ACTIVE(combo)) { - KEY_STATE_DOWN(combo->state, key_index); - if (longest_term < time) { - longest_term = time; +/* Prepare to activate a ripened combo by transferring it to the ready queue + * and committing its triggering key records + * + * Should only be called by resolve_conflicts() */ +void ready_combo(combo_state_t combo_index, combo_t *combo) { + bool triggered = false; + SET_NEXT_COMBO(combo, COMBO_NULL_INDEX); + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *qrecord = GET_QUEUED_RECORD(i); + if ((COMBO_KEY_RIPE == GET_STATE(qrecord)) && _combo_has_key(combo, qrecord->record.keycode)) { + /* This function is only called by resolve_conflicts(), so the key queue should already + * be empty */ + if (!triggered) { + SET_STATE(qrecord, COMBO_KEY_TRIGGER); + SET_NEXT_COMBO(qrecord, combo_index); + triggered = true; + } else { + qrecord->state = COMBO_KEY_CONSUMED; + SET_NEXT_COMBO(qrecord, combo_index); } } - 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); - } + } +} + +/* Ensure that there are no overlapping combos in the ripe queue by + * inactivating lower-priority combos. + * + * For surviving ripe combos, mark every key including the combo as + * COMBO_KEY_RIPE + * + * Also ensure that any remaining ripe combos have no overlap with combos in + * the key queues, except in the case of higher-priority combos with a + * superset of triggers */ +void resolve_conflicts(void) { + combo_iterator_t ripe_iter; + for (ALL_COMBOS_IN_QUEUE(&ripe_head, &ripe_iter)) { + bool blocked = false; + uint8_t combo_length = _get_combo_length(ripe_iter.combo); + combo_iterator_t conflict_iter; + /* Overlapping ripe combos (after this one) are lower priority; inactivate them */ + for (ALL_COMBOS_IN_QUEUE(&ripe_iter.combo->state, &conflict_iter)) { + if (get_combo_overlap(ripe_iter.combo, conflict_iter.combo)) { + combo_iter_remove(&conflict_iter); + combo_queue_insert(&inactive_head, conflict_iter.combo_index, conflict_iter.combo); + } + } + /* Overlapping partial and unripe complete combos that have lower + * priority or don't fully contain this combo are inactivated */ + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *qrecord = GET_QUEUED_RECORD(i); + if (_combo_has_key(ripe_iter.combo, qrecord->record.keycode)) { + SET_STATE(qrecord, COMBO_KEY_RIPE); + } else if (is_key_resolved(qrecord)) { + /* This key might have temporarily appeared resolved until + * this call to resolve_conflicts(), if this combo was the + * only remaining combo for the key, so it's important to + * first check for presence of the key in the combo */ + continue; + } + for (ALL_COMBOS_IN_KEY(qrecord, &conflict_iter)) { + uint8_t overlap = get_combo_overlap(ripe_iter.combo, conflict_iter.combo); + if (overlap) { + if ((ripe_iter.combo_index > conflict_iter.combo_index) || (overlap < combo_length)) { + combo_iter_remove(&conflict_iter); + combo_queue_insert(&inactive_head, conflict_iter.combo_index, conflict_iter.combo); + } else { + blocked = true; } } + } + } + if (!blocked) { + combo_iter_remove(&ripe_iter); + ready_combo(ripe_iter.combo_index, ripe_iter.combo); + } + } +} - 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); +/********************************** + * COMBO AND HOLD TERM EXPIRATION * + **********************************/ - // get possible longer waiting time for tap-/hold-only combos. - longest_term = _get_wait_time(combo_index, combo); - } - } // if timer elapsed end +/* Process expiration of a queued key record until a particular limit time. + * May cause completed combos to ripen, ready, or inactivate, or cause + * partial combos to inactivate. All incomplete or unripe combos for which + * this key record was the first press are held in the combo queue of this + * queued key record, so the relevant expiration time is easy to compute. + * + * If until!=0, any combo whose hold or combo term expired before `until` is + * processed. If tap!=0, then additionally, any combo containing the key + * `tap` is processed. If until==0, all combos are processed. + * + * If tap==0, then hold requirements are satisfied and tap requirements are + * unsatisfied. If tap==0, the opposite holds only for combos including the + * key `tap`. */ +void expire_key(queued_record_t *qrecord, uint16_t until, uint16_t tap) { + if (is_key_resolved(qrecord)) { + return; + } + + combo_iterator_t iter; + for (ALL_COMBOS_IN_KEY(qrecord, &iter)) { + bool combo_complete = (0 == GET_STATE(iter.combo)); + bool combo_expired = true; + bool combo_tapped = false; + if (tap && _combo_has_key(iter.combo, tap)) { + combo_tapped = true; } - } 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; + if (!combo_tapped && until) { + uint16_t term = combo_complete ? _get_hold_term(iter.combo_index, iter.combo) : _get_combo_term(iter.combo_index, iter.combo); + if (!timer_expired(until, (qrecord->record.event.time + term))) { + combo_expired = false; } + } + if (combo_expired) { + combo_iter_remove(&iter); + if (!combo_complete #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); + || (!combo_tapped && get_combo_must_tap(iter.combo_index, iter.combo)) +#endif +#if defined(COMBO_MUST_HOLD_PER_COMBO) || defined(COMBO_MUST_HOLD_MODS) + || (combo_tapped && _get_combo_must_hold(iter.combo_index, iter.combo)) +#endif + ) { + combo_queue_insert(&inactive_head, iter.combo_index, iter.combo); + } else { + ripen_combo(iter.combo_index, iter.combo); + } + } + } + + resolve_conflicts(); +} + +/* Forces expiration (with specified keycode tap if tap!=0) of all combos + * associated with oldest `force` records in the key buffer. Additionally, + * if until != 0, handles expiration of all combos expiring before the limit + * `until` (using the specified keycode tap if tap!=0). */ +void process_expiration(uint16_t until, uint16_t tap, uint8_t force) { + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *qrecord = GET_QUEUED_RECORD(i); + if (i < force) { + expire_key(qrecord, 0, tap); + } else if (until) { + expire_key(qrecord, until, tap); + } else { + break; + } + } +} + +/* Compute when the next expiration should be performed. Should only be + * called after calling dump_safely() (since that calls resolve_conflicts(), + * which affects the result of this computation) + * + * If there are unripe complete combos, the next expiration is the soonest + * time when one will ripen. (The order of ripening is important for + * consistent triggering, since combos can inactivate higher priority + * completed combos when they ripen, as long as the higher priority combos + * do not have a superset of the combo triggers.) + * + * If there are no unripe complete combos, the next expiration is determined + * by the longest combo term among partial combos containing the oldest + * (unresolved) key in the buffer, since this determines the soonest time + * when we can safely dump a key. */ +void set_next_expiration(void) { + next_expiration = 0; + uint16_t partial_expiration = 0; + bool oldest = true; + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *qrecord = GET_QUEUED_RECORD(i); + if (is_key_resolved(qrecord)) { + continue; + } + combo_iterator_t iter; + for (ALL_COMBOS_IN_KEY(qrecord, &iter)) { + if (0 == GET_STATE(iter.combo)) { + uint16_t ripening = qrecord->record.event.time + _get_hold_term(iter.combo_index, iter.combo); + if (!next_expiration || ripening < next_expiration) { + next_expiration = ripening; + } + } else if (oldest && !next_expiration) { + uint16_t inactivation = qrecord->record.event.time + _get_combo_term(iter.combo_index, iter.combo); + if (inactivation > partial_expiration) { + partial_expiration = inactivation; } -# endif } + } + oldest = false; + } + if (!next_expiration) { + next_expiration = partial_expiration; + } +} + +/**************************** + * DUMPING KEYS FROM BUFFER * + ****************************/ + +/* Release a key record for processing by later steps of QMK pipeline */ +void dump_keyrecord(keyrecord_t *record) { +#ifndef NO_ACTION_TAPPING + action_tapping_process(*record); +#else + process_record(record); #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); +/* Insert combo into active queue, process the combo's outcome, and + * initialize detailed active state */ +void activate_combo(queued_record_t *qrecord) { + combo_state_t combo_index = GET_NEXT_COMBO(qrecord); + combo_t *combo = &key_combos[combo_index]; + combo_state_t *combo_state = init_combo_active_state(combo_index); + if (combo_state == NULL) { + /* This should never happen! */ + return; + } + uint8_t combo_length = _get_combo_length(combo); + *combo_state = (1 << combo_length) - 1; + combo_queue_insert(&active_head, combo_index, combo); + + if (!combo->keycode) { +#ifdef COMBO_DETAILED_EVENTS_PER_COMBO + if (get_combo_needs_details(combo_index, combo)) { + triggers[0] = qrecord->record; + uint8_t trigger_index = 1; + combo_state_t consumed_state = ((combo_index << COMBO_STATE_BITS) & COMBO_KEY_CONSUMED); + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *buffer_entry = GET_QUEUED_RECORD(i); + if (qrecord->state == consumed_state) { + triggers[trigger_index] = buffer_entry->record; + trigger_index++; + } + } + process_combo_detailed_press(combo_index, combo, triggers); + } else #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; + { + process_combo_event(combo_index, true); + } + } else { + keyrecord_t record = { + .event = + { + .key = COMBO_KEY_POS, + .time = qrecord->record.event.time, + .pressed = true, + }, + .keycode = combo->keycode, + }; + dump_keyrecord(&record); + } +} -#ifdef COMBO_PROCESS_KEY_RELEASE - if (process_combo_key_release(combo_index, combo, key_index, keycode)) { - release_combo(combo_index, combo); +/* Try releasing a key from any active combos in which it is present. + * Returns true if it was in fact released from an active combo, false + * otherwise. */ +bool release_from_active(keyrecord_t *record) { + bool released = false; + combo_iterator_t iter; + for (ALL_COMBOS_IN_QUEUE(&active_head, &iter)) { + uint8_t key_index = _get_combo_keycode_index(iter.combo, record->keycode); + if (key_index != (uint8_t)-1) { + combo_state_t *combo_state = get_combo_active_state(iter.combo_index); + if (combo_state == NULL) { + /* Shouldn't happen... */ + continue; } + if (*combo_state & (1 << key_index)) { + /* Key is still pressed for this combo */ + released = true; + *combo_state &= ~(1 << key_index); +#ifdef COMBO_PROCESS_KEY_RELEASE +# ifdef COMBO_DETAILED_EVENTS_PER_COMBO + if (get_combo_needs_details(iter.combo_index, iter.combo)) { + if (process_combo_detailed_release(iter.combo_index, iter.combo, key_index, record->keycode, &record->event)) { + *combo_state = 0; + } + } +# endif + if (process_combo_key_release(iter.combo_index, iter.combo, key_index, record->keycode)) { + *combo_state = 0; + } #endif - } else { - /* The released key was part of an incomplete combo */ - key_is_part_of_combo = false; + if (*combo_state == 0) { + if (!iter.combo->keycode) { + process_combo_event(iter.combo_index, false); + } else { + keyrecord_t release_record = { + .event = + { + .key = COMBO_KEY_POS, + .time = record->event.time, + .pressed = false, + }, + .keycode = iter.combo->keycode, + }; + dump_keyrecord(&release_record); + } + free_combo_active_state(iter.combo_index); + combo_iter_remove(&iter); + combo_queue_insert(&inactive_head, iter.combo_index, iter.combo); + } + } } + } + return released; +} - KEY_STATE_UP(combo->state, key_index); +/* Releases key records from the buffer and combos from their queues for as + * long as we have enough information to fully resolve the next item in the + * sequence */ +void dump_safely(void) { + resolve_conflicts(); + while (key_buffer_size > 0) { + queued_record_t *qrecord = GET_QUEUED_RECORD(0); + if (!is_key_resolved(qrecord)) { + break; + } + /* We advance the buffer here, in case dump_safely() gets called + * recursively as a side-effect of user code */ + key_buffer_head = (key_buffer_head + 1) % COMBO_KEY_BUFFER_LENGTH; + key_buffer_size--; + switch (GET_STATE(qrecord)) { + case COMBO_KEY_CONSUMED: + break; + case COMBO_KEY_TRIGGER: + activate_combo(qrecord); + break; + default: + if (!qrecord->record.event.pressed) { + if (release_from_active(&qrecord->record)) { + break; + } + } + /* Remove our cached keycode so that the proper keycode can + * be inferred from matrix position */ + qrecord->record.keycode = 0; + dump_keyrecord(&qrecord->record); + } } +} - return key_is_part_of_combo; +/* Force oldest key in the buffer to be released, as though all combos + * involving it had already been resolved */ +void process_key_buffer_overflow(void) { + process_expiration(0, 0, 1); + dump_safely(); } -bool process_combo(uint16_t keycode, keyrecord_t *record) { - bool is_combo_key = false; - bool no_combo_keys_pressed = true; +/*************************** + * MAIN COMBO ENTRY POINTS * + ***************************/ +bool process_combo(uint16_t keycode, keyrecord_t *record) { if (keycode == CMB_ON && record->event.pressed) { combo_enable(); return true; @@ -519,82 +849,155 @@ bool process_combo(uint16_t keycode, keyrecord_t *record) { return true; } + if (!is_combo_enabled()) { + return true; + } + + if (IS_NOEVENT(record->event)) { + 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)); + /* We first check the key buffer for duplicate keycodes. When we + * encounter a new event for a keycode already in the buffer, everything + * preceding the older event immediately resolves, allowing the older + * keyrecord to be dumped. + * + * Regardless of whether we encounter a duplicate keycode, we first + * expire everything until the event time of the new keyrecord, so that + * we don't end up, e.g., completing partial combos that should have + * already expired. If the new keyrecord is a key release, it is + * processed during this expiration to appropriately ripen or inactivate + * completed combos (depending on tap/hold requirements) and to + * inactivate overlapping partial combos. + * + * After expiration, we dump anything that has resolved from the + * keybuffer. */ + + uint8_t force = 0; + uint16_t tap = record->event.pressed ? 0 : keycode; + for (uint8_t i = 0; i < key_buffer_size; i++) { + queued_record_t *old_record = GET_QUEUED_RECORD(i); + if (keycode == old_record->record.keycode) { + force = i + 1; + break; + } } + process_expiration(record->event.time, tap, force); + dump_safely(); - 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) { + process_key_buffer_overflow(); + } - 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 - }; + queued_record_t *qrecord = GET_QUEUED_RECORD(key_buffer_size); + qrecord->record = *record; + qrecord->record.keycode = keycode; + qrecord->state = COMBO_NULL_STATE; + key_buffer_size++; + + if (record->event.pressed) { + /* Key presses can: + * - contribute to partial combos, possibly completing them; + * - contribute to inactive combos, moving them to partial state (or + * completing them, if user has defined a combo of length 1); + * - inactivate partial combos that are contiguous but lack this key + */ + + /* Add key to inactive combos */ + combo_iterator_t iter; + for (ALL_COMBOS_IN_QUEUE(&inactive_head, &iter)) { + if (_combo_has_key(iter.combo, keycode)) { + /* Remove from inactive queue and add to this key's queue */ + uint8_t combo_length = _get_combo_length(iter.combo); + SET_STATE(iter.combo, (combo_length - 1)); + combo_iter_remove(&iter); + combo_queue_insert(&qrecord->state, iter.combo_index, iter.combo); + } } - } 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; + + /* Add key to partial combos of other keys */ + for (uint8_t i = 0; i < key_buffer_size - 1; i++) { + queued_record_t *old_record = GET_QUEUED_RECORD(i); + if (is_key_resolved(old_record)) { + continue; + } + for (ALL_COMBOS_IN_KEY(old_record, &iter)) { + if (_combo_has_key(iter.combo, keycode)) { + DECREMENT_STATE(iter.combo); + } else +#ifdef COMBO_CONTIGUOUS_PER_COMBO + if (get_combo_interrupted(iter.combo_index, iter.combo, &qrecord->record)) #endif - clear_combos(); + { + /* Inactivate partial combos that don't include this key + * but are required to be contiguous */ + if (0 != GET_STATE(iter.combo)) { + combo_iter_remove(&iter); + combo_queue_insert(&inactive_head, iter.combo_index, iter.combo); + } + } + } } - } - return !is_combo_key; + } /* Key releases are handled by the process_expiration call above */ + + dump_safely(); + set_next_expiration(); + return false; /* Key events can only be released from buffer when safe */ } void combo_task(void) { - if (!b_combo_enable) { + if (!is_combo_enabled() || !next_expiration) { return; } + uint16_t time = timer_read(); + if (timer_expired(time, next_expiration)) { + process_expiration(time, 0, key_buffer_size); + dump_safely(); + set_next_expiration(); + } +} + +/************************ + * COMBO FEATURE SET-UP * + ************************/ + +/* Initializes combo state when keyboard matrix is initialized, and when + * combo feature is re-enabled after having been disabled */ +void reset_combos(void) { + key_buffer_head = key_buffer_size = 0; + ripe_head = active_head = COMBO_NULL_STATE; + + inactive_head = 0; + combo_t *combo = &key_combos[0]; + for (combo_state_t i = 0; i < COMBO_LEN; i++) { + combo = &key_combos[i]; + SET_NEXT_COMBO(combo, (i + 1)); + } + SET_NEXT_COMBO(combo, COMBO_NULL_INDEX); -#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(); - } + active_combo_count = 0; + for (uint8_t i = 0; i < COMBO_BUFFER_LENGTH - 1; i++) { + active_buffer[i].combo_index = COMBO_NULL_INDEX; + active_buffer[i].state = 0; } -#endif } -void combo_enable(void) { b_combo_enable = true; } +void combo_enable(void) { + if ((!b_combo_enable) && COMBO_LEN > 0) { + reset_combos(); + 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; - clear_combos(); - dump_key_buffer(); + b_combo_enable = false; + process_expiration(0, 0, key_buffer_size); + dump_safely(); } void combo_toggle(void) { diff --git a/quantum/process_keycode/process_combo.h b/quantum/process_keycode/process_combo.h index 4c4e574e34f2..28d3c5736011 100644 --- a/quantum/process_keycode/process_combo.h +++ b/quantum/process_keycode/process_combo.h @@ -20,39 +20,49 @@ #include "quantum.h" #include -#ifdef EXTRA_SHORT_COMBOS -# define MAX_COMBO_LENGTH 6 -#elif defined(EXTRA_EXTRA_LONG_COMBOS) +/* COMBO_BUFFER_LENGTH defines the maximum number of simulatenously active combos. */ +#ifndef COMBO_BUFFER_LENGTH +# define COMBO_BUFFER_LENGTH 4 +#endif + +#if defined(EXTRA_EXTRA_LONG_COMBOS) # define MAX_COMBO_LENGTH 32 +# define COMBO_STATE_BITS 5 +typedef uint32_t combo_active_state_t; #elif defined(EXTRA_LONG_COMBOS) # define MAX_COMBO_LENGTH 16 +# define COMBO_STATE_BITS 4 +typedef uint16_t combo_active_state_t; +#elif defined(EXTRA_SMALL_COMBOS) +# define MAX_COMBO_LENGTH 4 +# define COMBO_STATE_BITS 2 +typedef uint8_t combo_active_state_t; #else # define MAX_COMBO_LENGTH 8 +# define COMBO_STATE_BITS 3 +typedef uint8_t combo_active_state_t; #endif -#ifndef COMBO_KEY_BUFFER_LENGTH -# define COMBO_KEY_BUFFER_LENGTH MAX_COMBO_LENGTH +#if !defined(MANY_COMBOS) && defined(COMBO_COUNT) +# if (COMBO_COUNT + 1) * MAX_COMBO_LENGTH >= 256 +# define MANY_COMBOS +# endif #endif -#ifndef COMBO_BUFFER_LENGTH -# define COMBO_BUFFER_LENGTH 4 + +#ifdef MANY_COMBOS +typedef uint16_t combo_state_t; +#else +typedef uint8_t combo_state_t; +#endif + +#ifndef COMBO_KEY_BUFFER_LENGTH +# define COMBO_KEY_BUFFER_LENGTH (MAX_COMBO_LENGTH + 4) #endif typedef struct { const uint16_t *keys; uint16_t keycode; -#ifdef EXTRA_SHORT_COMBOS - uint8_t state; -#else - bool disabled; - bool active; -# if defined(EXTRA_EXTRA_LONG_COMBOS) - uint32_t state; -# elif defined(EXTRA_LONG_COMBOS) - uint16_t state; -# else - uint8_t state; -# endif -#endif + combo_state_t state; } combo_t; #define COMBO(ck, ca) \