mirror of
https://github.com/Keychron/qmk_firmware.git
synced 2024-12-12 21:25:33 +06:00
Add combo key repress feature (#22858)
Co-authored-by: jack <jack@pngu.org>
This commit is contained in:
parent
b5c807fb4a
commit
0fd9909657
|
@ -307,6 +307,50 @@ bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Customizable key repress
|
||||||
|
By defining `COMBO_PROCESS_KEY_REPRESS` and implementing `bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode)` you can run your custom code when you repress just released key of a combo. By combining it with custom `process_combo_event` we can for example make special handling for Alt+Tab to switch windows, which, on combo F+G activation, registers Alt and presses Tab - then we can switch windows forward by releasing G and pressing it again, or backwards with F key. Here's the full example:
|
||||||
|
|
||||||
|
```c
|
||||||
|
enum combos {
|
||||||
|
CMB_ALTTAB
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint16_t PROGMEM combo_alttab[] = {KC_F, KC_G, COMBO_END};
|
||||||
|
|
||||||
|
combo_t key_combos[COMBO_LENGTH] = {
|
||||||
|
[CMB_ALTTAB] = COMBO(combo_alttab, KC_NO), // KC_NO to leave processing for process_combo_event
|
||||||
|
};
|
||||||
|
|
||||||
|
void process_combo_event(uint16_t combo_index, bool pressed) {
|
||||||
|
switch (combo_index) {
|
||||||
|
case CMB_ALTTAB:
|
||||||
|
if (pressed) {
|
||||||
|
register_mods(MOD_LALT);
|
||||||
|
tap_code(KC_TAB);
|
||||||
|
} else {
|
||||||
|
unregister_mods(MOD_LALT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
|
||||||
|
switch (combo_index) {
|
||||||
|
case CMB_ALTTAB:
|
||||||
|
switch (keycode) {
|
||||||
|
case KC_F:
|
||||||
|
tap_code16(S(KC_TAB));
|
||||||
|
return true;
|
||||||
|
case KC_G:
|
||||||
|
tap_code(KC_TAB);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Layer independent combos
|
### Layer independent combos
|
||||||
|
|
||||||
If you, for example, use multiple base layers for different key layouts, one for QWERTY, and another one for Colemak, you might want your combos to work from the same key positions on all layers. Defining the same combos again for another layout is redundant and takes more memory. The solution is to just check the keycodes from one layer.
|
If you, for example, use multiple base layers for different key layouts, one for QWERTY, and another one for Colemak, you might want your combos to work from the same key positions on all layers. Defining the same combos again for another layout is redundant and takes more memory. The solution is to just check the keycodes from one layer.
|
||||||
|
|
|
@ -65,12 +65,20 @@ __attribute__((weak)) bool process_combo_key_release(uint16_t combo_index, combo
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef COMBO_PROCESS_KEY_REPRESS
|
||||||
|
__attribute__((weak)) bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef COMBO_SHOULD_TRIGGER
|
#ifdef COMBO_SHOULD_TRIGGER
|
||||||
__attribute__((weak)) bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
|
__attribute__((weak)) bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef enum { COMBO_KEY_NOT_PRESSED, COMBO_KEY_PRESSED, COMBO_KEY_REPRESSED } combo_key_action_t;
|
||||||
|
|
||||||
#ifndef COMBO_NO_TIMER
|
#ifndef COMBO_NO_TIMER
|
||||||
static uint16_t timer = 0;
|
static uint16_t timer = 0;
|
||||||
#endif
|
#endif
|
||||||
|
@ -414,14 +422,14 @@ static bool keys_pressed_in_order(uint16_t combo_index, combo_t *combo, uint16_t
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) {
|
static combo_key_action_t process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) {
|
||||||
uint8_t key_count = 0;
|
uint8_t key_count = 0;
|
||||||
uint16_t key_index = -1;
|
uint16_t key_index = -1;
|
||||||
_find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
|
_find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
|
||||||
|
|
||||||
/* Continue processing if key isn't part of current combo. */
|
/* Continue processing if key isn't part of current combo. */
|
||||||
if (-1 == (int16_t)key_index) {
|
if (-1 == (int16_t)key_index) {
|
||||||
return false;
|
return COMBO_KEY_NOT_PRESSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool key_is_part_of_combo = (!COMBO_DISABLED(combo) && is_combo_enabled()
|
bool key_is_part_of_combo = (!COMBO_DISABLED(combo) && is_combo_enabled()
|
||||||
|
@ -449,7 +457,7 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
|
||||||
/* Don't buffer this combo if its combo term has passed. */
|
/* Don't buffer this combo if its combo term has passed. */
|
||||||
if (timer && timer_elapsed(timer) > time) {
|
if (timer && timer_elapsed(timer) > time) {
|
||||||
DISABLE_COMBO(combo);
|
DISABLE_COMBO(combo);
|
||||||
return true;
|
return COMBO_KEY_PRESSED;
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
|
@ -485,6 +493,15 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
|
||||||
}
|
}
|
||||||
} // if timer elapsed end
|
} // if timer elapsed end
|
||||||
}
|
}
|
||||||
|
#ifdef COMBO_PROCESS_KEY_REPRESS
|
||||||
|
} else if (record->event.pressed) {
|
||||||
|
if (COMBO_ACTIVE(combo)) {
|
||||||
|
if (process_combo_key_repress(combo_index, combo, key_index, keycode)) {
|
||||||
|
KEY_STATE_DOWN(combo->state, key_index);
|
||||||
|
return COMBO_KEY_REPRESSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
// chord releases
|
// chord releases
|
||||||
if (!COMBO_ACTIVE(combo) && ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
|
if (!COMBO_ACTIVE(combo) && ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
|
||||||
|
@ -531,12 +548,12 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
|
||||||
KEY_STATE_UP(combo->state, key_index);
|
KEY_STATE_UP(combo->state, key_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
return key_is_part_of_combo;
|
return key_is_part_of_combo ? COMBO_KEY_PRESSED : COMBO_KEY_NOT_PRESSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool process_combo(uint16_t keycode, keyrecord_t *record) {
|
bool process_combo(uint16_t keycode, keyrecord_t *record) {
|
||||||
bool is_combo_key = false;
|
uint8_t is_combo_key = COMBO_KEY_NOT_PRESSED;
|
||||||
bool no_combo_keys_pressed = true;
|
bool no_combo_keys_pressed = true;
|
||||||
|
|
||||||
if (keycode == QK_COMBO_ON && record->event.pressed) {
|
if (keycode == QK_COMBO_ON && record->event.pressed) {
|
||||||
combo_enable();
|
combo_enable();
|
||||||
|
@ -582,12 +599,17 @@ bool process_combo(uint16_t keycode, keyrecord_t *record) {
|
||||||
# endif
|
# endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (key_buffer_size < COMBO_KEY_BUFFER_LENGTH) {
|
#ifdef COMBO_PROCESS_KEY_REPRESS
|
||||||
key_buffer[key_buffer_size++] = (queued_record_t){
|
if (is_combo_key == COMBO_KEY_PRESSED)
|
||||||
.record = *record,
|
#endif
|
||||||
.keycode = keycode,
|
{
|
||||||
.combo_index = -1, // this will be set when applying combos
|
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 {
|
} else {
|
||||||
if (combo_buffer_read != combo_buffer_write) {
|
if (combo_buffer_read != combo_buffer_write) {
|
||||||
|
|
10
tests/combo/combo_repress/config.h
Normal file
10
tests/combo/combo_repress/config.h
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2024 @Filios92
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
#define TAPPING_TERM 200
|
||||||
|
|
||||||
|
#define COMBO_PROCESS_KEY_REPRESS
|
6
tests/combo/combo_repress/test.mk
Normal file
6
tests/combo/combo_repress/test.mk
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Copyright 2024 @Filios92
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
COMBO_ENABLE = yes
|
||||||
|
|
||||||
|
INTROSPECTION_KEYMAP_C = test_combos_repress.c
|
158
tests/combo/combo_repress/test_combo.cpp
Normal file
158
tests/combo/combo_repress/test_combo.cpp
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright 2024 @Filios92
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "keyboard_report_util.hpp"
|
||||||
|
#include "quantum.h"
|
||||||
|
#include "keycode.h"
|
||||||
|
#include "test_common.h"
|
||||||
|
#include "test_driver.hpp"
|
||||||
|
#include "test_fixture.hpp"
|
||||||
|
#include "test_keymap_key.hpp"
|
||||||
|
|
||||||
|
using testing::_;
|
||||||
|
using testing::InSequence;
|
||||||
|
|
||||||
|
class ComboRepress : public TestFixture {};
|
||||||
|
|
||||||
|
TEST_F(ComboRepress, combo_repress_tapped) {
|
||||||
|
TestDriver driver;
|
||||||
|
KeymapKey key_f(0, 0, 0, KC_F);
|
||||||
|
KeymapKey key_g(0, 0, 1, KC_G);
|
||||||
|
set_keymap({key_f, key_g});
|
||||||
|
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT)).Times(2);
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
tap_combo({key_f, key_g}, 20);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ComboRepress, combo_repress_held_released_one_key_and_repressed) {
|
||||||
|
TestDriver driver;
|
||||||
|
KeymapKey key_f(0, 0, 0, KC_F);
|
||||||
|
KeymapKey key_g(0, 0, 1, KC_G);
|
||||||
|
KeymapKey key_h(0, 0, 2, KC_H);
|
||||||
|
KeymapKey key_j(0, 0, 3, KC_J);
|
||||||
|
set_keymap({key_f, key_g, key_h, key_j});
|
||||||
|
|
||||||
|
/* Press combo F+G */
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT)).Times(2);
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
|
||||||
|
key_f.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
key_g.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
idle_for(COMBO_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Release G */
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
key_g.release();
|
||||||
|
idle_for(80);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Tap G */
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT));
|
||||||
|
tap_key(key_g, TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Tap G, but hold for longer */
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT));
|
||||||
|
tap_key(key_g, TAPPING_TERM * 2);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
idle_for(500);
|
||||||
|
|
||||||
|
/* Tap other combo while holding F */
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE, KC_LEFT_ALT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT));
|
||||||
|
tap_combo({key_h, key_j}, TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* G press and hold */
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT));
|
||||||
|
key_g.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* F release and tap */
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT, KC_LEFT_SHIFT)).Times(2);
|
||||||
|
EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT, KC_LEFT_SHIFT));
|
||||||
|
EXPECT_REPORT(driver, (KC_LEFT_ALT));
|
||||||
|
key_f.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
tap_key(key_f);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Release G */
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
key_g.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ComboRepress, combo_repress_normal_combo) {
|
||||||
|
TestDriver driver;
|
||||||
|
KeymapKey key_f(0, 0, 0, KC_F);
|
||||||
|
KeymapKey key_g(0, 0, 1, KC_G);
|
||||||
|
KeymapKey key_h(0, 0, 2, KC_H);
|
||||||
|
KeymapKey key_j(0, 0, 3, KC_J);
|
||||||
|
set_keymap({key_f, key_g, key_h, key_j});
|
||||||
|
|
||||||
|
/* Press combo H+J */
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE));
|
||||||
|
key_h.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
key_j.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
idle_for(COMBO_TERM + 10);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Release H */
|
||||||
|
EXPECT_NO_REPORT(driver);
|
||||||
|
key_h.release();
|
||||||
|
idle_for(80);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Tap H */
|
||||||
|
EXPECT_REPORT(driver, (KC_H, KC_ESCAPE));
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE));
|
||||||
|
tap_key(key_h);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Tap H, but hold for longer */
|
||||||
|
EXPECT_REPORT(driver, (KC_H, KC_ESCAPE));
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE));
|
||||||
|
tap_key(key_h, TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
idle_for(500);
|
||||||
|
|
||||||
|
/* Tap other combo while holding K */
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE, KC_LEFT_ALT)).Times(2);
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE, KC_TAB, KC_LEFT_ALT));
|
||||||
|
EXPECT_REPORT(driver, (KC_ESCAPE));
|
||||||
|
tap_combo({key_f, key_g}, TAPPING_TERM + 1);
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* H press and hold */
|
||||||
|
EXPECT_REPORT(driver, (KC_H, KC_ESCAPE));
|
||||||
|
key_h.press();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* J release and tap */
|
||||||
|
EXPECT_REPORT(driver, (KC_H));
|
||||||
|
key_j.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
|
||||||
|
/* Release G */
|
||||||
|
EXPECT_EMPTY_REPORT(driver);
|
||||||
|
key_h.release();
|
||||||
|
run_one_scan_loop();
|
||||||
|
VERIFY_AND_CLEAR(driver);
|
||||||
|
}
|
43
tests/combo/combo_repress/test_combos_repress.c
Normal file
43
tests/combo/combo_repress/test_combos_repress.c
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2024 @Filios92
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include "quantum.h"
|
||||||
|
|
||||||
|
enum combos { alttab, esc };
|
||||||
|
|
||||||
|
uint16_t const alttab_combo[] = {KC_F, KC_G, COMBO_END};
|
||||||
|
uint16_t const esc_combo[] = {KC_H, KC_J, COMBO_END};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
combo_t key_combos[] = {
|
||||||
|
[alttab] = COMBO(alttab_combo, KC_NO),
|
||||||
|
[esc] = COMBO(esc_combo, KC_ESC)
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
void process_combo_event(uint16_t combo_index, bool pressed) {
|
||||||
|
switch (combo_index) {
|
||||||
|
case alttab:
|
||||||
|
if (pressed) {
|
||||||
|
register_mods(MOD_LALT);
|
||||||
|
tap_code(KC_TAB);
|
||||||
|
} else {
|
||||||
|
unregister_mods(MOD_LALT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
|
||||||
|
switch (combo_index) {
|
||||||
|
case alttab:
|
||||||
|
switch (keycode) {
|
||||||
|
case KC_F:
|
||||||
|
tap_code16(S(KC_TAB));
|
||||||
|
return true;
|
||||||
|
case KC_G:
|
||||||
|
tap_code(KC_TAB);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user