0fd99096577a57453f070f2dd94baefa09e369a4

Author
Filios92 <filios92@gmail.com>
Committer
GitHub <noreply@github.com>
Date

Message

Add combo key repress feature (#22858)

Co-authored-by: jack <jack@pngu.org>

Diff

This diff is truncated to protect this page.

  1diff --git a/docs/features/combo.md b/docs/features/combo.md
  2index bdb8c4b15fcb32ae6072084044fa82e118aa46a1..10e33b72a6389e4c933e67a246645856682c41eb 100644
  3--- a/docs/features/combo.md
  4+++ b/docs/features/combo.md
  5@@ -307,6 +307,50 @@ bool process_combo_key_release(uint16_t combo_index, combo_t *combo, uint8_t key
  6     return false;
  7 }
  8 ```
  9+
 10+### Customizable key repress
 11diff --git a/quantum/process_keycode/process_combo.c b/quantum/process_keycode/process_combo.c
 12index b0034d136a8f233229c5b1f9c85139f02192bf4e..38171c2f54b6d36a989daf55193fe2bb545e5fc7 100644
 13--- a/quantum/process_keycode/process_combo.c
 14+++ b/quantum/process_keycode/process_combo.c
 15@@ -65,12 +65,20 @@ __attribute__((weak)) bool process_combo_key_release(uint16_t combo_index, combo
 16 }
 17 #endif
 18 
 19+#ifdef COMBO_PROCESS_KEY_REPRESS
 20+__attribute__((weak)) bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
 21+    return false;
 22+}
 23+#endif
 24+
 25 #ifdef COMBO_SHOULD_TRIGGER
 26 __attribute__((weak)) bool combo_should_trigger(uint16_t combo_index, combo_t *combo, uint16_t keycode, keyrecord_t *record) {
 27     return true;
 28 }
 29 #endif
 30 
 31+typedef enum { COMBO_KEY_NOT_PRESSED, COMBO_KEY_PRESSED, COMBO_KEY_REPRESSED } combo_key_action_t;
 32+
 33 #ifndef COMBO_NO_TIMER
 34 static uint16_t timer = 0;
 35 #endif
 36@@ -414,14 +422,14 @@ static bool keys_pressed_in_order(uint16_t combo_index, combo_t *combo, uint16_t
 37 }
 38 #endif
 39 
 40-static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) {
 41+static combo_key_action_t process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *record, uint16_t combo_index) {
 42     uint8_t  key_count = 0;
 43     uint16_t key_index = -1;
 44     _find_key_index_and_count(combo->keys, keycode, &key_index, &key_count);
 45 
 46     /* Continue processing if key isn't part of current combo. */
 47     if (-1 == (int16_t)key_index) {
 48-        return false;
 49+        return COMBO_KEY_NOT_PRESSED;
 50     }
 51 
 52     bool key_is_part_of_combo = (!COMBO_DISABLED(combo) && is_combo_enabled()
 53@@ -449,7 +457,7 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
 54             /* Don't buffer this combo if its combo term has passed. */
 55             if (timer && timer_elapsed(timer) > time) {
 56                 DISABLE_COMBO(combo);
 57-                return true;
 58+                return COMBO_KEY_PRESSED;
 59             } else
 60 #endif
 61             {
 62@@ -485,6 +493,15 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
 63                 }
 64             } // if timer elapsed end
 65         }
 66+#ifdef COMBO_PROCESS_KEY_REPRESS
 67+    } else if (record->event.pressed) {
 68+        if (COMBO_ACTIVE(combo)) {
 69+            if (process_combo_key_repress(combo_index, combo, key_index, keycode)) {
 70+                KEY_STATE_DOWN(combo->state, key_index);
 71+                return COMBO_KEY_REPRESSED;
 72+            }
 73+        }
 74+#endif
 75     } else {
 76         // chord releases
 77         if (!COMBO_ACTIVE(combo) && ALL_COMBO_KEYS_ARE_DOWN(COMBO_STATE(combo), key_count)) {
 78@@ -531,12 +548,12 @@ static bool process_single_combo(combo_t *combo, uint16_t keycode, keyrecord_t *
 79         KEY_STATE_UP(combo->state, key_index);
 80     }
 81 
 82-    return key_is_part_of_combo;
 83+    return key_is_part_of_combo ? COMBO_KEY_PRESSED : COMBO_KEY_NOT_PRESSED;
 84 }
 85 
 86 bool process_combo(uint16_t keycode, keyrecord_t *record) {
 87-    bool is_combo_key          = false;
 88-    bool no_combo_keys_pressed = true;
 89+    uint8_t is_combo_key          = COMBO_KEY_NOT_PRESSED;
 90+    bool    no_combo_keys_pressed = true;
 91 
 92     if (keycode == QK_COMBO_ON && record->event.pressed) {
 93         combo_enable();
 94@@ -582,12 +599,17 @@ bool process_combo(uint16_t keycode, keyrecord_t *record) {
 95 #    endif
 96 #endif
 97 
 98-        if (key_buffer_size < COMBO_KEY_BUFFER_LENGTH) {
 99-            key_buffer[key_buffer_size++] = (queued_record_t){
100-                .record      = *record,
101-                .keycode     = keycode,
102-                .combo_index = -1, // this will be set when applying combos
103-            };
104+#ifdef COMBO_PROCESS_KEY_REPRESS
105+        if (is_combo_key == COMBO_KEY_PRESSED)
106+#endif
107+        {
108+            if (key_buffer_size < COMBO_KEY_BUFFER_LENGTH) {
109+                key_buffer[key_buffer_size++] = (queued_record_t){
110+                    .record      = *record,
111+                    .keycode     = keycode,
112+                    .combo_index = -1, // this will be set when applying combos
113+                };
114+            }
115diff --git a/tests/combo/combo_repress/config.h b/tests/combo/combo_repress/config.h
116new file mode 100644
117index 0000000000000000000000000000000000000000..61ba58177eb57a6b6a3e6a4db1771cb1824aca50
118--- /dev/null
119+++ b/tests/combo/combo_repress/config.h
120@@ -0,0 +1,10 @@
121+// Copyright 2024 @Filios92
122+// SPDX-License-Identifier: GPL-2.0-or-later
123+
124+#pragma once
125+
126+#include "test_common.h"
127+
128+#define TAPPING_TERM 200
129+
130+#define COMBO_PROCESS_KEY_REPRESS
131diff --git a/tests/combo/combo_repress/test.mk b/tests/combo/combo_repress/test.mk
132new file mode 100644
133index 0000000000000000000000000000000000000000..cc3f482f4bbc99b9e6a0463727e3e2f9a7675d34
134--- /dev/null
135+++ b/tests/combo/combo_repress/test.mk
136@@ -0,0 +1,6 @@
137+# Copyright 2024 @Filios92
138+# SPDX-License-Identifier: GPL-2.0-or-later
139+
140+COMBO_ENABLE = yes
141+
142+INTROSPECTION_KEYMAP_C = test_combos_repress.c
143diff --git a/tests/combo/combo_repress/test_combo.cpp b/tests/combo/combo_repress/test_combo.cpp
144new file mode 100644
145index 0000000000000000000000000000000000000000..1488d5c1bdc0e6a2e10a28b3840d679a82138cca
146--- /dev/null
147+++ b/tests/combo/combo_repress/test_combo.cpp
148@@ -0,0 +1,158 @@
149+// Copyright 2024 @Filios92
150+// SPDX-License-Identifier: GPL-2.0-or-later
151+
152+#include "keyboard_report_util.hpp"
153+#include "quantum.h"
154+#include "keycode.h"
155+#include "test_common.h"
156+#include "test_driver.hpp"
157+#include "test_fixture.hpp"
158+#include "test_keymap_key.hpp"
159+
160+using testing::_;
161+using testing::InSequence;
162+
163+class ComboRepress : public TestFixture {};
164+
165+TEST_F(ComboRepress, combo_repress_tapped) {
166+    TestDriver driver;
167+    KeymapKey  key_f(0, 0, 0, KC_F);
168+    KeymapKey  key_g(0, 0, 1, KC_G);
169+    set_keymap({key_f, key_g});
170+
171+    EXPECT_REPORT(driver, (KC_LEFT_ALT)).Times(2);
172+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
173+    EXPECT_EMPTY_REPORT(driver);
174+    tap_combo({key_f, key_g}, 20);
175+    VERIFY_AND_CLEAR(driver);
176+}
177+
178+TEST_F(ComboRepress, combo_repress_held_released_one_key_and_repressed) {
179+    TestDriver driver;
180+    KeymapKey  key_f(0, 0, 0, KC_F);
181+    KeymapKey  key_g(0, 0, 1, KC_G);
182+    KeymapKey  key_h(0, 0, 2, KC_H);
183+    KeymapKey  key_j(0, 0, 3, KC_J);
184+    set_keymap({key_f, key_g, key_h, key_j});
185+
186+    /* Press combo F+G */
187+    EXPECT_REPORT(driver, (KC_LEFT_ALT)).Times(2);
188+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
189+    key_f.press();
190+    run_one_scan_loop();
191+    key_g.press();
192+    run_one_scan_loop();
193+    idle_for(COMBO_TERM + 1);
194+    VERIFY_AND_CLEAR(driver);
195+
196+    /* Release G */
197+    EXPECT_NO_REPORT(driver);
198+    key_g.release();
199+    idle_for(80);
200+    VERIFY_AND_CLEAR(driver);
201+
202+    /* Tap G */
203+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
204+    EXPECT_REPORT(driver, (KC_LEFT_ALT));
205+    tap_key(key_g, TAPPING_TERM + 1);
206+    VERIFY_AND_CLEAR(driver);
207+
208+    /* Tap G, but hold for longer */
209+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
210+    EXPECT_REPORT(driver, (KC_LEFT_ALT));
211+    tap_key(key_g, TAPPING_TERM * 2);
212+    VERIFY_AND_CLEAR(driver);
213+
214+    idle_for(500);
215+
216+    /* Tap other combo while holding F */
217+    EXPECT_REPORT(driver, (KC_ESCAPE, KC_LEFT_ALT));
218+    EXPECT_REPORT(driver, (KC_LEFT_ALT));
219+    tap_combo({key_h, key_j}, TAPPING_TERM + 1);
220+    VERIFY_AND_CLEAR(driver);
221+
222+    /* G press and hold */
223+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT));
224+    EXPECT_REPORT(driver, (KC_LEFT_ALT));
225+    key_g.press();
226+    run_one_scan_loop();
227+    VERIFY_AND_CLEAR(driver);
228+
229+    /* F release and tap */
230+    EXPECT_REPORT(driver, (KC_LEFT_ALT, KC_LEFT_SHIFT)).Times(2);
231+    EXPECT_REPORT(driver, (KC_TAB, KC_LEFT_ALT, KC_LEFT_SHIFT));
232+    EXPECT_REPORT(driver, (KC_LEFT_ALT));
233+    key_f.release();
234+    run_one_scan_loop();
235+    tap_key(key_f);
236+    VERIFY_AND_CLEAR(driver);
237+
238+    /* Release G */
239+    EXPECT_EMPTY_REPORT(driver);
240+    key_g.release();
241+    run_one_scan_loop();
242+    VERIFY_AND_CLEAR(driver);
243+}
244+
245+TEST_F(ComboRepress, combo_repress_normal_combo) {
246+    TestDriver driver;
247+    KeymapKey  key_f(0, 0, 0, KC_F);
248diff --git a/tests/combo/combo_repress/test_combos_repress.c b/tests/combo/combo_repress/test_combos_repress.c
249new file mode 100644
250index 0000000000000000000000000000000000000000..73fa77b0aabe2c6121ef367ef6b0e9c179d6b43d
251--- /dev/null
252+++ b/tests/combo/combo_repress/test_combos_repress.c
253@@ -0,0 +1,43 @@
254+// Copyright 2024 @Filios92
255+// SPDX-License-Identifier: GPL-2.0-or-later
256+#include "quantum.h"
257+
258+enum combos { alttab, esc };
259+
260+uint16_t const alttab_combo[] = {KC_F, KC_G, COMBO_END};
261+uint16_t const esc_combo[]    = {KC_H, KC_J, COMBO_END};
262+
263+// clang-format off
264+combo_t key_combos[] = {
265+    [alttab]  = COMBO(alttab_combo, KC_NO),
266+    [esc]     = COMBO(esc_combo, KC_ESC)
267+};
268+// clang-format on
269+
270+void process_combo_event(uint16_t combo_index, bool pressed) {
271+    switch (combo_index) {
272+        case alttab:
273+            if (pressed) {
274+                register_mods(MOD_LALT);
275+                tap_code(KC_TAB);
276+            } else {
277+                unregister_mods(MOD_LALT);
278+            }
279+            break;
280+    }
281+}
282+
283+bool process_combo_key_repress(uint16_t combo_index, combo_t *combo, uint8_t key_index, uint16_t keycode) {
284+    switch (combo_index) {
285+        case alttab:
286+            switch (keycode) {
287+                case KC_F:
288+                    tap_code16(S(KC_TAB));
289+                    return true;
290+                case KC_G:
291+                    tap_code(KC_TAB);
292+                    return true;
293+            }
294+    }
295+    return false;
296+}