aboutsummaryrefslogtreecommitdiffstats
path: root/quantum/process_keycode/process_combo.c
blob: c4e299958af210b69ad58121a9af85a6c51e61c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51<
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
/* 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"

#ifndef COMBO_VARIABLE_LEN
__attribute__((weak)) combo_t key_combos[COMBO_COUNT] = {};
#else
extern combo_t  key_combos[];
extern int      COMBO_LEN;
#endif

__attribute__((weak)) void process_combo_event(uint8_t combo_index, bool pressed) {}

static uint16_t timer               = 0;
static uint8_t  current_combo_index = 0;
static bool     drop_buffer         = false;
static bool     is_active           = false;
static bool     b_combo_enable      = true;  // defaults to enabled

static uint8_t buffer_size = 0;
#ifdef COMBO_ALLOW_ACTION_KEYS
static keyrecord_t key_buffer[MAX_COMBO_LENGTH];
#else
static uint16_t key_buffer[MAX_COMBO_LENGTH];
#endif

static inline void send_combo(uint16_t action, bool pressed) {
    if (action) {
        if (pressed) {
            register_code16(action);
        } else {
            unregister_code16(action);
        }
    } else {
        process_combo_event(current_combo_index, pressed);
    }
}

static inline void dump_key_buffer(bool emit) {
    if (buffer_size == 0) {
        return;
    }

    if (emit) {
        for (uint8_t i = 0; i < buffer_size; i++) {
#ifdef COMBO_ALLOW_ACTION_KEYS
            const action_t action = store_or_get_action(key_buffer[i].event.pressed, key_buffer[i].event.key);
            process_action(&(key_buffer[i]), action);
#else
            register_code16(key_buffer[i]);
            send_keyboard_report();
#endif
        }
    }

    buffer_size = 0;
}

#define ALL_COMBO_KEYS_ARE_DOWN (((1 << count) - 1) == combo->state)
#define KEY_STATE_DOWN(key)         \
    do {                            \
        combo->state |= (1 << key); \
    } while (0)
#define KEY_STATE_UP(key)            \
    do {                             \
        combo->state &= ~(1 << key); \
    } while (0)

static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record) {
    uint8_t count = 0;
    uint8_t index = -1;
    /* Find index of keycode and number of combo keys */
    for (const uint16_t *keys = combo->keys;; ++count) {
        uint16_t key = pgm_read_word(&keys[count]);
        if (keycode == key) index = count;
        if (COMBO_END == key) break;
    }

    /* Continue processing if not a combo key */
    if (-1 == (int8_t)index) return false;

    bool is_combo_active = is_active;

    if (record->event.pressed) {
        KEY_STATE_DOWN(index);

        if (is_combo_active) {
            if (ALL_COMBO_KEYS_ARE_DOWN) { /* Combo was pressed */
                send_combo(combo->keycode, true);
                drop_buffer = true;
            }
        }
    } else {
        if (ALL_COMBO_KEYS_ARE_DOWN) { /* Combo was released */
            send_combo(combo->keycode, false);
        } else {
            /* continue processing without immediately returning */
            is_combo_active = false;
        }

        KEY_STATE_UP(index);
    }

    return is_combo_active;
}

#define NO_COMBO_KEYS_ARE_DOWN (0 == combo->state)

bool process_combo(uint16_t keycode, keyrecord_t *record) {
    bool is_combo_key          = false;
    drop_buffer                = 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;
    }
#ifndef COMBO_VARIABLE_LEN
    for (current_combo_index = 0; current_combo_index < COMBO_COUNT; ++current_combo_index) {
#else
    for (current_combo_index = 0; current_combo_index < COMBO_LEN; ++current_combo_index) {
#endif
        combo_t *combo = &key_combos[current_combo_index];
        is_combo_key |= process_single_combo(combo, keycode, record);
        no_combo_keys_pressed = no_combo_keys_pressed && NO_COMBO_KEYS_ARE_DOWN;
    }

    if (drop_buffer) {
        /* buffer is only dropped when we complete a combo, so we refresh the timer
         * here */
        timer = timer_read();
        dump_key_buffer(false);
    } else if (!is_combo_key) {
        /* if no combos claim the key we need to emit the keybuffer */
        dump_key_buffer(true);

        // reset state if there are no combo keys pressed at all
        if (no_combo_keys_pressed) {
            timer     = 0;
            is_active = true;
        }
    } else if (record->event.pressed && is_active) {
        /* otherwise the key is consumed and placed in the buffer */
        timer = timer_read();

        if (buffer_size < MAX_COMBO_LENGTH) {
#ifdef COMBO_ALLOW_ACTION_KEYS
            key_buffer[buffer_size++] = *record;
#else
            key_buffer[buffer_size++] = keycode;
#endif
        }
    }

    return !is_combo_key;
}

void matrix_scan_combo(void) {
    if (b_combo_enable && is_active && timer && timer_elapsed(timer) > COMBO_TERM) {
        /* This disables the combo, meaning key events for this
         * combo will be handled by the next processors in the chain
         */
        is_active = false;
        dump_key_buffer(true);
    }
}

void combo_enable(void) { b_combo_enable = true; }

void combo_disable(void) {
    b_combo_enable = is_active = false;
    timer                      = 0;
    dump_key_buffer(true);
}

void combo_toggle(void) {
    if (b_combo_enable) {
        combo_disable();
    } else {
        combo_enable();
    }
}

bool is_combo_enabled(void) { return b_combo_enable; }