[Core] Add Repeat Key ("repeat last key") as a core feature. (#19700)
Co-authored-by: casuanoob <96005765+casuanoob@users.noreply.github.com> Co-authored-by: Sergey Vlasov <sigprof@gmail.com>
This commit is contained in:
		
							parent
							
								
									e1766df185
								
							
						
					
					
						commit
						3993b15f05
					
				| @ -32,6 +32,7 @@ GENERIC_FEATURES = \ | |||||||
|     KEY_OVERRIDE \
 |     KEY_OVERRIDE \
 | ||||||
|     LEADER \
 |     LEADER \
 | ||||||
|     PROGRAMMABLE_BUTTON \
 |     PROGRAMMABLE_BUTTON \
 | ||||||
|  |     REPEAT_KEY \
 | ||||||
|     SECURE \
 |     SECURE \
 | ||||||
|     SPACE_CADET \
 |     SPACE_CADET \
 | ||||||
|     SWAP_HANDS \
 |     SWAP_HANDS \
 | ||||||
|  | |||||||
| @ -85,7 +85,8 @@ OTHER_OPTION_NAMES = \ | |||||||
|   SECURE_ENABLE \
 |   SECURE_ENABLE \
 | ||||||
|   CAPS_WORD_ENABLE \
 |   CAPS_WORD_ENABLE \
 | ||||||
|   AUTOCORRECT_ENABLE \
 |   AUTOCORRECT_ENABLE \
 | ||||||
|   TRI_LAYER_ENABLE |   TRI_LAYER_ENABLE \
 | ||||||
|  |   REPEAT_KEY_ENABLE | ||||||
| 
 | 
 | ||||||
| define NAME_ECHO | define NAME_ECHO | ||||||
|        @printf "  %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)" |        @printf "  %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)" | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								data/constants/keycodes/keycodes_0.0.3.hjson
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								data/constants/keycodes/keycodes_0.0.3.hjson
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										18
									
								
								data/constants/keycodes/keycodes_0.0.3_quantum.hjson
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								data/constants/keycodes/keycodes_0.0.3_quantum.hjson
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | { | ||||||
|  |     "keycodes": { | ||||||
|  |        "0x7C79": { | ||||||
|  |             "group": "quantum", | ||||||
|  |             "key": "QK_REPEAT_KEY", | ||||||
|  |             "aliases": [ | ||||||
|  |                 "QK_REP" | ||||||
|  |             ] | ||||||
|  |         }, | ||||||
|  |         "0x7C7A": { | ||||||
|  |             "group": "quantum", | ||||||
|  |             "key": "QK_ALT_REPEAT_KEY", | ||||||
|  |             "aliases": [ | ||||||
|  |                 "QK_AREP" | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -70,6 +70,7 @@ | |||||||
|     * [Macros](feature_macros.md) |     * [Macros](feature_macros.md) | ||||||
|     * [Mouse Keys](feature_mouse_keys.md) |     * [Mouse Keys](feature_mouse_keys.md) | ||||||
|     * [Programmable Button](feature_programmable_button.md) |     * [Programmable Button](feature_programmable_button.md) | ||||||
|  |     * [Repeat Key](feature_repeat_key.md) | ||||||
|     * [Space Cadet Shift](feature_space_cadet.md) |     * [Space Cadet Shift](feature_space_cadet.md) | ||||||
|     * [US ANSI Shifted Keys](keycodes_us_ansi_shifted.md) |     * [US ANSI Shifted Keys](keycodes_us_ansi_shifted.md) | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										457
									
								
								docs/feature_repeat_key.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								docs/feature_repeat_key.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,457 @@ | |||||||
|  | # Repeat Key | ||||||
|  | 
 | ||||||
|  | The Repeat Key performs the action of the last pressed key. Tapping the Repeat | ||||||
|  | Key after tapping the <kbd>Z</kbd> key types another "`z`." This is useful for | ||||||
|  | typing doubled letters, like the `z` in "`dazzle`": a double tap on <kbd>Z</kbd> | ||||||
|  | can instead be a roll from <kbd>Z</kbd> to <kbd>Repeat</kbd>, which is | ||||||
|  | potentially faster and more comfortable. The Repeat Key is also useful for | ||||||
|  | hotkeys, like repeating Ctrl + Shift + Right Arrow to select by word.  | ||||||
|  | 
 | ||||||
|  | Repeat Key remembers mods that were active with the last key press. These mods | ||||||
|  | are combined with any additional mods while pressing the Repeat Key. If the last | ||||||
|  | press key was <kbd>Ctrl</kbd> + <kbd>Z</kbd>, then <kbd>Shift</kbd> + | ||||||
|  | <kbd>Repeat</kbd> performs Ctrl + Shift + `Z`. | ||||||
|  | 
 | ||||||
|  | ## How do I enable Repeat Key | ||||||
|  | 
 | ||||||
|  | In your `rules.mk`, add: | ||||||
|  | 
 | ||||||
|  | ```make | ||||||
|  | REPEAT_KEY_ENABLE = yes | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Then pick a key in your keymap and assign it the keycode `QK_REPEAT_KEY` (short | ||||||
|  | alias `QK_REP`). Optionally, use the keycode `QK_ALT_REPEAT_KEY` (short alias | ||||||
|  | `QK_AREP`) on another key. | ||||||
|  | 
 | ||||||
|  | ## Keycodes | ||||||
|  | 
 | ||||||
|  | |Keycode                |Aliases  |Description                          | | ||||||
|  | |-----------------------|---------|-------------------------------------| | ||||||
|  | |`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          | | ||||||
|  | |`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    | | ||||||
|  | 
 | ||||||
|  | ## Alternate Repeating | ||||||
|  | 
 | ||||||
|  | The Alternate Repeat Key performs the "alternate" action of the last pressed key | ||||||
|  | if it is defined. By default, Alternate Repeat is defined for navigation keys to | ||||||
|  | act in the reverse direction. When the last key is the common "select by word" | ||||||
|  | hotkey Ctrl + Shift + Right Arrow, the Alternate Repeat Key performs Ctrl + | ||||||
|  | Shift + Left Arrow, which together with the Repeat Key enables convenient | ||||||
|  | selection by words in either direction. | ||||||
|  | 
 | ||||||
|  | Alternate Repeat is enabled with the Repeat Key by default. Optionally, to | ||||||
|  | reduce firmware size, Alternate Repeat may be disabled by adding in config.h: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | #define NO_ALT_REPEAT_KEY | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The following alternate keys are defined by default. See | ||||||
|  | `get_alt_repeat_key_keycode_user()` below for how to change or add to these | ||||||
|  | definitions. Where it makes sense, these definitions also include combinations  | ||||||
|  | with mods, like Ctrl + Left ↔ Ctrl + Right Arrow. | ||||||
|  | 
 | ||||||
|  | **Navigation**  | ||||||
|  | 
 | ||||||
|  | |Keycodes                           |Description                        | | ||||||
|  | |-----------------------------------|-----------------------------------| | ||||||
|  | |`KC_LEFT` ↔ `KC_RGHT`         | Left ↔ Right Arrow           | | ||||||
|  | |`KC_UP` ↔ `KC_DOWN`           | Up ↔ Down Arrow              | | ||||||
|  | |`KC_HOME` ↔ `KC_END`          | Home ↔ End                   | | ||||||
|  | |`KC_PGUP` ↔ `KC_PGDN`         | Page Up ↔ Page Down          | | ||||||
|  | |`KC_MS_L` ↔ `KC_MS_R`         | Mouse Cursor Left ↔ Right    | | ||||||
|  | |`KC_MS_U` ↔ `KC_MS_D`         | Mouse Cursor Up ↔ Down       | | ||||||
|  | |`KC_WH_L` ↔ `KC_WH_R`         | Mouse Wheel Left ↔ Right     | | ||||||
|  | |`KC_WH_U` ↔ `KC_WH_D`         | Mouse Wheel Up ↔ Down        | | ||||||
|  | 
 | ||||||
|  | **Misc**  | ||||||
|  | 
 | ||||||
|  | |Keycodes                           |Description                        | | ||||||
|  | |-----------------------------------|-----------------------------------| | ||||||
|  | |`KC_BSPC` ↔ `KC_DEL`          | Backspace ↔ Delete           | | ||||||
|  | |`KC_LBRC` ↔ `KC_RBRC`         | `[` ↔ `]`                    | | ||||||
|  | |`KC_LCBR` ↔ `KC_RCBR`         | `{` ↔ `}`                    | | ||||||
|  | 
 | ||||||
|  | **Media**  | ||||||
|  | 
 | ||||||
|  | |Keycodes                           |Description                        | | ||||||
|  | |-----------------------------------|-----------------------------------| | ||||||
|  | |`KC_WBAK` ↔ `KC_WFWD`         | Browser Back ↔ Forward       | | ||||||
|  | |`KC_MNXT` ↔ `KC_MPRV`         | Next ↔ Previous Media Track  | | ||||||
|  | |`KC_MFFD` ↔ `KC_MRWD`         | Fast Forward ↔ Rewind Media  | | ||||||
|  | |`KC_VOLU` ↔ `KC_VOLD`         | Volume Up ↔ Down             | | ||||||
|  | |`KC_BRIU` ↔ `KC_BRID`         | Brightness Up ↔ Down         | | ||||||
|  | 
 | ||||||
|  | **Hotkeys in Vim, Emacs, and other programs** | ||||||
|  | 
 | ||||||
|  | |Keycodes                           |Description                        | | ||||||
|  | |-----------------------------------|-----------------------------------| | ||||||
|  | |mod + `KC_F` ↔ mod + `KC_B`   | Forward ↔ Backward           | | ||||||
|  | |mod + `KC_D` ↔ mod + `KC_U`   | Down ↔ Up                    | | ||||||
|  | |mod + `KC_N` ↔ mod + `KC_P`   | Next ↔ Previous              | | ||||||
|  | |mod + `KC_A` ↔ mod + `KC_E`   | Home ↔ End                   | | ||||||
|  | |mod + `KC_O` ↔ mod + `KC_I`   | Vim jump list Older ↔ Newer  | | ||||||
|  | |`KC_J` ↔ `KC_K`               | Down ↔ Up                    | | ||||||
|  | |`KC_H` ↔ `KC_L`               | Left ↔ Right                 | | ||||||
|  | |`KC_W` ↔ `KC_B`               | Forward ↔ Backward by Word   | | ||||||
|  | 
 | ||||||
|  | (where above, "mod" is Ctrl, Alt, or GUI) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Defining alternate keys | ||||||
|  | 
 | ||||||
|  | Use the `get_alt_repeat_key_keycode_user()` callback to define the "alternate" | ||||||
|  | for additional keys or override the default definitions. For example, to define | ||||||
|  | Ctrl + Y as the alternate of Ctrl + Z, and vice versa, add the following in | ||||||
|  | keymap.c: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     if ((mods & MOD_MASK_CTRL)) {  // Was Ctrl held? | ||||||
|  |         switch (keycode) { | ||||||
|  |             case KC_Y: return C(KC_Z);  // Ctrl + Y reverses to Ctrl + Z. | ||||||
|  |             case KC_Z: return C(KC_Y);  // Ctrl + Z reverses to Ctrl + Y. | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return KC_TRNS;  // Defer to default definitions. | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The `keycode` and `mods` args are the keycode and mods that were active with the | ||||||
|  | last pressed key. The meaning of the return value from this function is: | ||||||
|  | 
 | ||||||
|  | * `KC_NO` – do nothing (any predefined alternate key is not used); | ||||||
|  | * `KC_TRNS` – use the default alternate key if it exists; | ||||||
|  | * anything else – use the specified keycode. Any keycode may be returned | ||||||
|  |   as an alternate key, including custom keycodes. | ||||||
|  | 
 | ||||||
|  | Another example, defining Shift + Tab as the alternate of Tab, and vice versa: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     bool shifted = (mods & MOD_MASK_SHIFT);  // Was Shift held? | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_TAB: | ||||||
|  |             if (shifted) {        // If the last key was Shift + Tab, | ||||||
|  |                 return KC_TAB;    // ... the reverse is Tab. | ||||||
|  |             } else {              // Otherwise, the last key was Tab, | ||||||
|  |                 return S(KC_TAB); // ... and the reverse is Shift + Tab. | ||||||
|  |             } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return KC_TRNS; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Eliminating SFBs | ||||||
|  | 
 | ||||||
|  | Alternate Repeat can be configured more generally to perform an action that | ||||||
|  | "complements" the last key. Alternate Repeat is not limited to reverse | ||||||
|  | repeating, and it need not be symmetric. You can use it to eliminate cases of | ||||||
|  | same-finger bigrams in your layout, that is, pairs of letters typed by the same | ||||||
|  | finger. The following addresses the top 5 same-finger bigrams in English on | ||||||
|  | QWERTY, so that for instance "`ed`" may be typed as <kbd>E</kbd>, <kbd>Alt | ||||||
|  | Repeat</kbd>. | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_E: return KC_D;  // For "ED" bigram. | ||||||
|  |         case KC_D: return KC_E;  // For "DE" bigram. | ||||||
|  |         case KC_C: return KC_E;  // For "CE" bigram. | ||||||
|  |         case KC_L: return KC_O;  // For "LO" bigram. | ||||||
|  |         case KC_U: return KC_N;  // For "UN" bigram. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return KC_TRNS; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Typing shortcuts | ||||||
|  | 
 | ||||||
|  | A useful possibility is having Alternate Repeat press [a | ||||||
|  | macro](feature_macros.md). This way macros can be used without having to | ||||||
|  | dedicate keys to them. The following defines a couple shortcuts. | ||||||
|  | 
 | ||||||
|  | * Typing <kbd>K</kbd>, <kbd>Alt Repeat</kbd> produces "`keyboard`," with the | ||||||
|  |   initial "`k`" typed as usual and the "`eybord`" produced by the macro.  | ||||||
|  | * Typing <kbd>.</kbd>, <kbd>Alt Repeat</kbd> produces "`../`," handy for "up | ||||||
|  |   directory" on the shell. Similary, <kbd>.</kbd> types the initial "`.`" and  | ||||||
|  |   "`./`" is produced by the macro. | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | enum custom_keycodes { | ||||||
|  |     M_KEYBOARD = SAFE_RANGE, | ||||||
|  |     M_UPDIR, | ||||||
|  |     // Other custom keys... | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_K: return M_KEYBOARD; | ||||||
|  |         case KC_DOT: return M_UPDIR; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return KC_TRNS; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case M_KEYBOARD: SEND_STRING(/*k*/"eyboard"); break; | ||||||
|  |         case M_UPDIR: SEND_STRING(/*.*/"./"); break; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Ignoring certain keys and mods | ||||||
|  | 
 | ||||||
|  | In tracking what is "the last key" to be repeated or alternate repeated, | ||||||
|  | modifier and layer switch keys are always ignored. This makes it possible to set | ||||||
|  | some mods and change layers between pressing a key and repeating it. By default, | ||||||
|  | all other (non-modifier, non-layer switch) keys are remembered so that they are | ||||||
|  | eligible for repeating. To configure additional keys to be ignored, define | ||||||
|  | `remember_last_key_user()` in your keymap.c. | ||||||
|  | 
 | ||||||
|  | #### Ignoring a key | ||||||
|  | 
 | ||||||
|  | The following ignores the Backspace key: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, | ||||||
|  |                             uint8_t* remembered_mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_BSPC: | ||||||
|  |             return false;  // Ignore backspace. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true;  // Other keys can be repeated. | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Then for instance, the Repeat key in <kbd>Left Arrow</kbd>, | ||||||
|  | <kbd>Backspace</kbd>, <kbd>Repeat</kbd> sends Left Arrow again instead of | ||||||
|  | repeating Backspace. | ||||||
|  | 
 | ||||||
|  | The `remember_last_key_user()` callback is called on every key press excluding | ||||||
|  | modifiers and layer switches. Returning true indicates the key is remembered, | ||||||
|  | while false means it is ignored. | ||||||
|  | 
 | ||||||
|  | #### Filtering remembered mods | ||||||
|  | 
 | ||||||
|  | The `remembered_mods` arg represents the mods that will be remembered with | ||||||
|  | this key. It can be modified to forget certain mods. This may be | ||||||
|  | useful to forget capitalization when repeating shifted letters, so that "Aaron" | ||||||
|  | does not becom "AAron": | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, | ||||||
|  |                             uint8_t* remembered_mods) { | ||||||
|  |     // Forget Shift on letter keys when Shift or AltGr are the only mods. | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_A ... KC_Z: | ||||||
|  |             if ((*remembered_mods & ~(MOD_MASK_SHIFT | MOD_BIT(KC_RALT))) == 0) { | ||||||
|  |                 *remembered_mods &= ~MOD_MASK_SHIFT; | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### Further conditions | ||||||
|  | 
 | ||||||
|  | Besides checking the keycode, this callback could also make conditions based on | ||||||
|  | the current layer state (with `IS_LAYER_ON(layer)`) or mods (`get_mods()`). For | ||||||
|  | example, the following ignores keys on layer 2 as well as key combinations | ||||||
|  | involving GUI: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, | ||||||
|  |                             uint8_t* remembered_mods) { | ||||||
|  |     if (IS_LAYER_ON(2) || (get_mods() & MOD_MASK_GUI)) { | ||||||
|  |         return false;  // Ignore layer 2 keys and GUI chords. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true;  // Other keys can be repeated. | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ?> See [Layer Functions](feature_layers.md#functions) and [Checking Modifier | ||||||
|  | State](feature_advanced_keycodes.md#checking-modifier-state) for further | ||||||
|  | details. | ||||||
|  |   | ||||||
|  | 
 | ||||||
|  | ## Handle how a key is repeated | ||||||
|  | 
 | ||||||
|  | By default, pressing the Repeat Key will simply behave as if the last key | ||||||
|  | were pressed again. This also works with macro keys with custom handlers, | ||||||
|  | invoking the macro again. In case fine-tuning is needed for sensible repetition, | ||||||
|  | you can handle how a key is repeated with `get_repeat_key_count()` within | ||||||
|  | `process_record_user()`.  | ||||||
|  | 
 | ||||||
|  | The `get_repeat_key_count()` function returns a signed count of times the key | ||||||
|  | has been repeated or alternate repeated. When a key is pressed as usual, | ||||||
|  | `get_repeat_key_count()` is 0. On the first repeat, it is 1, then the second | ||||||
|  | repeat, 2, and so on. Negative counts are used similarly for alternate | ||||||
|  | repeating. For instance supposing `MY_MACRO` is a custom keycode used in the | ||||||
|  | layout: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case MY_MACRO: | ||||||
|  |             if (get_repeat_key_count() > 0) { | ||||||
|  |                 // MY_MACRO is being repeated! | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     SEND_STRING("repeat!");     | ||||||
|  |                 } | ||||||
|  |             } else {                           | ||||||
|  |                 // MY_MACRO is being used normally. | ||||||
|  |                 if (record->event.pressed) {   | ||||||
|  |                     SEND_STRING("macro"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |       | ||||||
|  |         // Other macros... | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Handle how a key is alternate repeated | ||||||
|  | 
 | ||||||
|  | Pressing the Alternate Repeat Key behaves as if the "alternate" of the last | ||||||
|  | pressed key were pressed, if an alternate is defined. To define how a particular | ||||||
|  | key is alternate repeated, use the `get_alt_repeat_key_keycode_user()` callback | ||||||
|  | as described above to define which keycode to use as its alternate. Beyond this, | ||||||
|  | `get_repeat_key_count()` may be used in custom handlers to fine-tune behavior | ||||||
|  | when alternate repeating. | ||||||
|  | 
 | ||||||
|  | The following example defines `MY_MACRO` as its own alternate, and specially | ||||||
|  | handles repeating and alternate repeating: | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case MY_MACRO: return MY_MACRO;  // MY_MACRO is its own alternate. | ||||||
|  |     } | ||||||
|  |     return KC_TRNS; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case MY_MACRO: | ||||||
|  |             if (get_repeat_key_count() > 0) {        // Repeating. | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     SEND_STRING("repeat!");     | ||||||
|  |                 } | ||||||
|  |             } else if (get_repeat_key_count() < 0) { // Alternate repeating. | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     SEND_STRING("alt repeat!"); | ||||||
|  |                 } | ||||||
|  |             } else {                                 // Used normally. | ||||||
|  |                 if (record->event.pressed) {   | ||||||
|  |                     SEND_STRING("macro"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |       | ||||||
|  |         // Other macros... | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Functions | ||||||
|  | 
 | ||||||
|  | | Function                       | Description                                                            | | ||||||
|  | |--------------------------------|------------------------------------------------------------------------| | ||||||
|  | | `get_last_keycode()`           | The last key's keycode, the key to be repeated.                        | | ||||||
|  | | `get_last_mods()`              | Mods to apply when repeating.                                          | | ||||||
|  | | `set_last_keycode(kc)`         | Set the keycode to be repeated.                                        | | ||||||
|  | | `set_last_mods(mods)`          | Set the mods to apply when repeating.                                  | | ||||||
|  | | `get_repeat_key_count()`       | Signed count of times the key has been repeated or alternate repeated. | | ||||||
|  | | `get_alt_repeat_key_keycode()` | Keycode to be used for alternate repeating.                            | | ||||||
|  |   | ||||||
|  | 
 | ||||||
|  | ## Additional "Alternate" keys | ||||||
|  | 
 | ||||||
|  | By leveraging `get_last_keycode()` in macros, it is possible to define | ||||||
|  | additional, distinct "Alternate Repeat"-like keys. The following defines two | ||||||
|  | keys `ALTREP2` and `ALTREP3` and implements ten shortcuts with them for common | ||||||
|  | English 5-gram letter patterns, taking inspiration from | ||||||
|  | [Stenotype](feature_stenography.md): | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Typing                           | Produces | Typing                           | Produces | | ||||||
|  | |----------------------------------|----------|----------------------------------|----------| | ||||||
|  | | <kbd>A</kbd>, <kbd>ALTREP2</kbd> | `ation`  | <kbd>A</kbd>, <kbd>ALTREP3</kbd> | `about`   | | ||||||
|  | | <kbd>I</kbd>, <kbd>ALTREP2</kbd> | `ition`  | <kbd>I</kbd>, <kbd>ALTREP3</kbd> | `inter`   | | ||||||
|  | | <kbd>S</kbd>, <kbd>ALTREP2</kbd> | `ssion`  | <kbd>S</kbd>, <kbd>ALTREP3</kbd> | `state`   | | ||||||
|  | | <kbd>T</kbd>, <kbd>ALTREP2</kbd> | `their`  | <kbd>T</kbd>, <kbd>ALTREP3</kbd> | `there`   | | ||||||
|  | | <kbd>W</kbd>, <kbd>ALTREP2</kbd> | `which`  | <kbd>W</kbd>, <kbd>ALTREP3</kbd> | `would`   | | ||||||
|  | 
 | ||||||
|  | ```c | ||||||
|  | enum custom_keycodes { | ||||||
|  |     ALTREP2 = SAFE_RANGE, | ||||||
|  |     ALTREP3, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Use ALTREP2 and ALTREP3 in your layout... | ||||||
|  | 
 | ||||||
|  | bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, | ||||||
|  |                             uint8_t* remembered_mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case ALTREP2: | ||||||
|  |         case ALTREP3: | ||||||
|  |             return false;  // Ignore ALTREP keys. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true;  // Other keys can be repeated. | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void process_altrep2(uint16_t keycode, uint8_t mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_A: SEND_STRING(/*a*/"tion"); break; | ||||||
|  |         case KC_I: SEND_STRING(/*i*/"tion"); break; | ||||||
|  |         case KC_S: SEND_STRING(/*s*/"sion"); break; | ||||||
|  |         case KC_T: SEND_STRING(/*t*/"heir"); break; | ||||||
|  |         case KC_W: SEND_STRING(/*w*/"hich"); break; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void process_altrep3(uint16_t keycode, uint8_t mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case KC_A: SEND_STRING(/*a*/"bout"); break; | ||||||
|  |         case KC_I: SEND_STRING(/*i*/"nter"); break; | ||||||
|  |         case KC_S: SEND_STRING(/*s*/"tate"); break; | ||||||
|  |         case KC_T: SEND_STRING(/*t*/"here"); break; | ||||||
|  |         case KC_W: SEND_STRING(/*w*/"ould"); break; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         case ALTREP2:  | ||||||
|  |             if (record->event.pressed) { | ||||||
|  |                 process_altrep2(get_last_keycode(), get_last_mods()); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         case ALTREP3: | ||||||
|  |             if (record->event.pressed) { | ||||||
|  |                 process_altrep3(get_last_keycode(), get_last_mods()); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| @ -68,6 +68,7 @@ | |||||||
|     * [モッドタップ](ja/mod_tap.md) |     * [モッドタップ](ja/mod_tap.md) | ||||||
|     * [マクロ](ja/feature_macros.md) |     * [マクロ](ja/feature_macros.md) | ||||||
|     * [マウスキー](ja/feature_mouse_keys.md) |     * [マウスキー](ja/feature_mouse_keys.md) | ||||||
|  |     * [Repeat Key](ja/feature_repeat_key.md) | ||||||
|     * [Space Cadet Shift](ja/feature_space_cadet.md) |     * [Space Cadet Shift](ja/feature_space_cadet.md) | ||||||
|     * [US ANSI シフトキー](ja/keycodes_us_ansi_shifted.md) |     * [US ANSI シフトキー](ja/keycodes_us_ansi_shifted.md) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -803,6 +803,15 @@ See also: [Programmable Button](feature_programmable_button.md) | |||||||
| |`QK_PROGRAMMABLE_BUTTON_31`|`PB_31`|Programmable button 31| | |`QK_PROGRAMMABLE_BUTTON_31`|`PB_31`|Programmable button 31| | ||||||
| |`QK_PROGRAMMABLE_BUTTON_32`|`PB_32`|Programmable button 32| | |`QK_PROGRAMMABLE_BUTTON_32`|`PB_32`|Programmable button 32| | ||||||
| 
 | 
 | ||||||
|  | ## Repeat Key :id=repeat-key | ||||||
|  | 
 | ||||||
|  | See also: [Repeat Key](feature_repeat_key.md) | ||||||
|  | 
 | ||||||
|  | |Keycode                |Aliases  |Description                          | | ||||||
|  | |-----------------------|---------|-------------------------------------| | ||||||
|  | |`QK_REPEAT_KEY`        |`QK_REP` |Repeat the last pressed key          | | ||||||
|  | |`QK_ALT_REPEAT_KEY`    |`QK_AREP`|Perform alternate of the last key    | | ||||||
|  | 
 | ||||||
| ## Space Cadet :id=space-cadet | ## Space Cadet :id=space-cadet | ||||||
| 
 | 
 | ||||||
| See also: [Space Cadet](feature_space_cadet.md) | See also: [Space Cadet](feature_space_cadet.md) | ||||||
|  | |||||||
| @ -73,6 +73,7 @@ | |||||||
|     * [Mod-Tap](zh-cn/mod_tap.md) |     * [Mod-Tap](zh-cn/mod_tap.md) | ||||||
|     * [宏](zh-cn/feature_macros.md) |     * [宏](zh-cn/feature_macros.md) | ||||||
|     * [鼠标键](zh-cn/feature_mouse_keys.md) |     * [鼠标键](zh-cn/feature_mouse_keys.md) | ||||||
|  |     * [Repeat Key](zh-cn/feature_repeat_key.md) | ||||||
|     * [Space Cadet Shift](zh-cn/feature_space_cadet.md) |     * [Space Cadet Shift](zh-cn/feature_space_cadet.md) | ||||||
|     * [US ANSI上档键值](zh-cn/keycodes_us_ansi_shifted.md) |     * [US ANSI上档键值](zh-cn/keycodes_us_ansi_shifted.md) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -285,7 +285,7 @@ void process_record(keyrecord_t *record) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void process_record_handler(keyrecord_t *record) { | void process_record_handler(keyrecord_t *record) { | ||||||
| #ifdef COMBO_ENABLE | #if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE) | ||||||
|     action_t action; |     action_t action; | ||||||
|     if (record->keycode) { |     if (record->keycode) { | ||||||
|         action = action_for_keycode(record->keycode); |         action = action_for_keycode(record->keycode); | ||||||
| @ -1109,7 +1109,7 @@ bool is_tap_record(keyrecord_t *record) { | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| #ifdef COMBO_ENABLE | #if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE) | ||||||
|     action_t action; |     action_t action; | ||||||
|     if (record->keycode) { |     if (record->keycode) { | ||||||
|         action = action_for_keycode(record->keycode); |         action = action_for_keycode(record->keycode); | ||||||
|  | |||||||
| @ -50,7 +50,7 @@ typedef struct { | |||||||
| #ifndef NO_ACTION_TAPPING | #ifndef NO_ACTION_TAPPING | ||||||
|     tap_t tap; |     tap_t tap; | ||||||
| #endif | #endif | ||||||
| #ifdef COMBO_ENABLE | #if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE) | ||||||
|     uint16_t keycode; |     uint16_t keycode; | ||||||
| #endif | #endif | ||||||
| } keyrecord_t; | } keyrecord_t; | ||||||
|  | |||||||
| @ -721,6 +721,8 @@ enum qk_keycode_defines { | |||||||
|     QK_AUTOCORRECT_TOGGLE = 0x7C76, |     QK_AUTOCORRECT_TOGGLE = 0x7C76, | ||||||
|     QK_TRI_LAYER_LOWER = 0x7C77, |     QK_TRI_LAYER_LOWER = 0x7C77, | ||||||
|     QK_TRI_LAYER_UPPER = 0x7C78, |     QK_TRI_LAYER_UPPER = 0x7C78, | ||||||
|  |     QK_REPEAT_KEY = 0x7C79, | ||||||
|  |     QK_ALT_REPEAT_KEY = 0x7C7A, | ||||||
|     QK_KB_0 = 0x7E00, |     QK_KB_0 = 0x7E00, | ||||||
|     QK_KB_1 = 0x7E01, |     QK_KB_1 = 0x7E01, | ||||||
|     QK_KB_2 = 0x7E02, |     QK_KB_2 = 0x7E02, | ||||||
| @ -1362,6 +1364,8 @@ enum qk_keycode_defines { | |||||||
|     AC_TOGG    = QK_AUTOCORRECT_TOGGLE, |     AC_TOGG    = QK_AUTOCORRECT_TOGGLE, | ||||||
|     TL_LOWR    = QK_TRI_LAYER_LOWER, |     TL_LOWR    = QK_TRI_LAYER_LOWER, | ||||||
|     TL_UPPR    = QK_TRI_LAYER_UPPER, |     TL_UPPR    = QK_TRI_LAYER_UPPER, | ||||||
|  |     QK_REP     = QK_REPEAT_KEY, | ||||||
|  |     QK_AREP    = QK_ALT_REPEAT_KEY, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // Range Helpers
 | // Range Helpers
 | ||||||
| @ -1413,6 +1417,6 @@ enum qk_keycode_defines { | |||||||
| #define IS_MACRO_KEYCODE(code) ((code) >= QK_MACRO_0 && (code) <= QK_MACRO_31) | #define IS_MACRO_KEYCODE(code) ((code) >= QK_MACRO_0 && (code) <= QK_MACRO_31) | ||||||
| #define IS_BACKLIGHT_KEYCODE(code) ((code) >= QK_BACKLIGHT_ON && (code) <= QK_BACKLIGHT_TOGGLE_BREATHING) | #define IS_BACKLIGHT_KEYCODE(code) ((code) >= QK_BACKLIGHT_ON && (code) <= QK_BACKLIGHT_TOGGLE_BREATHING) | ||||||
| #define IS_RGB_KEYCODE(code) ((code) >= RGB_TOG && (code) <= RGB_MODE_TWINKLE) | #define IS_RGB_KEYCODE(code) ((code) >= RGB_TOG && (code) <= RGB_MODE_TWINKLE) | ||||||
| #define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_TRI_LAYER_UPPER) | #define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY) | ||||||
| #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31) | #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31) | ||||||
| #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31) | #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31) | ||||||
|  | |||||||
							
								
								
									
										109
									
								
								quantum/process_keycode/process_repeat_key.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								quantum/process_keycode/process_repeat_key.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | |||||||
|  | // Copyright 2022-2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     https://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | #include "process_repeat_key.h" | ||||||
|  | 
 | ||||||
|  | // Default implementation of remember_last_key_user().
 | ||||||
|  | __attribute__((weak)) bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool remember_last_key(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     switch (keycode) { | ||||||
|  |         // Ignore MO, TO, TG, TT, and TL layer switch keys.
 | ||||||
|  |         case QK_MOMENTARY ... QK_MOMENTARY_MAX: | ||||||
|  |         case QK_TO ... QK_TO_MAX: | ||||||
|  |         case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX: | ||||||
|  |         case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: | ||||||
|  |         // Ignore mod keys.
 | ||||||
|  |         case KC_LCTL ... KC_RGUI: | ||||||
|  |         case KC_HYPR: | ||||||
|  |         case KC_MEH: | ||||||
|  | #ifndef NO_ACTION_ONESHOT // Ignore one-shot keys.
 | ||||||
|  |         case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX: | ||||||
|  |         case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX: | ||||||
|  | #endif                  // NO_ACTION_ONESHOT
 | ||||||
|  | #ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
 | ||||||
|  |         case QK_TRI_LAYER_LOWER: | ||||||
|  |         case QK_TRI_LAYER_UPPER: | ||||||
|  | #endif // TRI_LAYER_ENABLE
 | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |             // Ignore hold events on tap-hold keys.
 | ||||||
|  | #ifndef NO_ACTION_TAPPING | ||||||
|  |         case QK_MOD_TAP ... QK_MOD_TAP_MAX: | ||||||
|  | #    ifndef NO_ACTION_LAYER | ||||||
|  |         case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: | ||||||
|  | #    endif // NO_ACTION_LAYER
 | ||||||
|  |             if (record->tap.count == 0) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | #endif // NO_ACTION_TAPPING
 | ||||||
|  | 
 | ||||||
|  | #ifdef SWAP_HANDS_ENABLE | ||||||
|  |         case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: | ||||||
|  |             if (IS_SWAP_HANDS_KEYCODE(keycode) || record->tap.count == 0) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | #endif // SWAP_HANDS_ENABLE
 | ||||||
|  | 
 | ||||||
|  |         case QK_REPEAT_KEY: | ||||||
|  | #ifndef NO_ALT_REPEAT_KEY | ||||||
|  |         case QK_ALT_REPEAT_KEY: | ||||||
|  | #endif // NO_ALT_REPEAT_KEY
 | ||||||
|  |             return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return remember_last_key_user(keycode, record, remembered_mods); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool process_last_key(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     if (get_repeat_key_count()) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (record->event.pressed) { | ||||||
|  |         uint8_t remembered_mods = get_mods() | get_weak_mods(); | ||||||
|  | #ifndef NO_ACTION_ONESHOT | ||||||
|  |         remembered_mods |= get_oneshot_mods(); | ||||||
|  | #endif // NO_ACTION_ONESHOT
 | ||||||
|  | 
 | ||||||
|  |         if (remember_last_key(keycode, record, &remembered_mods)) { | ||||||
|  |             set_last_record(keycode, record); | ||||||
|  |             set_last_mods(remembered_mods); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool process_repeat_key(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     if (get_repeat_key_count()) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (keycode == QK_REPEAT_KEY) { | ||||||
|  |         repeat_key_invoke(&record->event); | ||||||
|  |         return false; | ||||||
|  | #ifndef NO_ALT_REPEAT_KEY | ||||||
|  |     } else if (keycode == QK_ALT_REPEAT_KEY) { | ||||||
|  |         alt_repeat_key_invoke(&record->event); | ||||||
|  |         return false; | ||||||
|  | #endif // NO_ALT_REPEAT_KEY
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								quantum/process_keycode/process_repeat_key.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								quantum/process_keycode/process_repeat_key.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | // Copyright 2022-2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     https://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "quantum.h" | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Process handler for remembering the last key. | ||||||
|  |  * | ||||||
|  |  * @param keycode  Keycode registered by matrix press, per keymap | ||||||
|  |  * @param record   keyrecord_t structure | ||||||
|  |  * @return true    Continue processing keycodes, and send to host | ||||||
|  |  * @return false   Stop processing keycodes, and don't send to host | ||||||
|  |  */ | ||||||
|  | bool process_last_key(uint16_t keycode, keyrecord_t* record); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Optional callback defining which keys are remembered. | ||||||
|  |  * | ||||||
|  |  * @param keycode          Keycode that was just pressed | ||||||
|  |  * @param record           keyrecord_t structure | ||||||
|  |  * @param remembered_mods  Mods that will be remembered with this key | ||||||
|  |  * @return true            Key is remembered | ||||||
|  |  * @return false           Key is ignored | ||||||
|  |  * | ||||||
|  |  * Modifier and layer switch keys are always ignored. For all other keys, this | ||||||
|  |  * callback is called on every key press. Returning true means that the key is | ||||||
|  |  * remembered, false means it is ignored. By default, all non-modifier, | ||||||
|  |  * non-layer switch keys are remembered. | ||||||
|  |  * | ||||||
|  |  * The `remembered_mods` arg represents the mods that will be remembered with | ||||||
|  |  * this key. It can be modified to forget certain mods, for instance to forget | ||||||
|  |  * capitalization when repeating shifted letters: | ||||||
|  |  * | ||||||
|  |  *     // Forget Shift on letter keys.
 | ||||||
|  |  *     if (KC_A <= keycode && keycode <= KC_Z && (*remembered_mods & ~MOD_MASK_SHIFT) == 0) { | ||||||
|  |  *         *remembered_mods = 0; | ||||||
|  |  *     } | ||||||
|  |  */ | ||||||
|  | bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Process handler for Repeat Key feature. | ||||||
|  |  * | ||||||
|  |  * @param keycode  Keycode registered by matrix press, per keymap | ||||||
|  |  * @param record   keyrecord_t structure | ||||||
|  |  * @return true    Continue processing keycodes, and send to host | ||||||
|  |  * @return false   Stop processing keycodes, and don't send to host | ||||||
|  |  */ | ||||||
|  | bool process_repeat_key(uint16_t keycode, keyrecord_t* record); | ||||||
| @ -176,7 +176,7 @@ void soft_reset_keyboard(void) { | |||||||
| 
 | 
 | ||||||
| /* Convert record into usable keycode via the contained event. */ | /* Convert record into usable keycode via the contained event. */ | ||||||
| uint16_t get_record_keycode(keyrecord_t *record, bool update_layer_cache) { | uint16_t get_record_keycode(keyrecord_t *record, bool update_layer_cache) { | ||||||
| #ifdef COMBO_ENABLE | #if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE) | ||||||
|     if (record->keycode) { |     if (record->keycode) { | ||||||
|         return record->keycode; |         return record->keycode; | ||||||
|     } |     } | ||||||
| @ -273,6 +273,9 @@ bool process_record_quantum(keyrecord_t *record) { | |||||||
|             // Must run asap to ensure all keypresses are recorded.
 |             // Must run asap to ensure all keypresses are recorded.
 | ||||||
|             process_dynamic_macro(keycode, record) && |             process_dynamic_macro(keycode, record) && | ||||||
| #endif | #endif | ||||||
|  | #ifdef REPEAT_KEY_ENABLE | ||||||
|  |             process_last_key(keycode, record) && process_repeat_key(keycode, record) && | ||||||
|  | #endif | ||||||
| #if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY) | #if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY) | ||||||
|             process_clicky(keycode, record) && |             process_clicky(keycode, record) && | ||||||
| #endif | #endif | ||||||
|  | |||||||
| @ -251,6 +251,11 @@ extern layer_state_t layer_state; | |||||||
| #    include "process_tri_layer.h" | #    include "process_tri_layer.h" | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | #ifdef REPEAT_KEY_ENABLE | ||||||
|  | #    include "repeat_key.h" | ||||||
|  | #    include "process_repeat_key.h" | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
| void set_single_persistent_default_layer(uint8_t default_layer); | void set_single_persistent_default_layer(uint8_t default_layer); | ||||||
| 
 | 
 | ||||||
| #define IS_LAYER_ON(layer) layer_state_is(layer) | #define IS_LAYER_ON(layer) layer_state_is(layer) | ||||||
|  | |||||||
							
								
								
									
										282
									
								
								quantum/repeat_key.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								quantum/repeat_key.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,282 @@ | |||||||
|  | // Copyright 2022-2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     https://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | #include "repeat_key.h" | ||||||
|  | 
 | ||||||
|  | // Variables saving the state of the last key press.
 | ||||||
|  | static keyrecord_t last_record = {0}; | ||||||
|  | static uint8_t     last_mods   = 0; | ||||||
|  | // Signed count of the number of times the last key has been repeated or
 | ||||||
|  | // alternate repeated: it is 0 when a key is pressed normally, positive when
 | ||||||
|  | // repeated, and negative when alternate repeated.
 | ||||||
|  | static int8_t last_repeat_count = 0; | ||||||
|  | // The repeat_count, but set to 0 outside of repeat_key_invoke() so that it is
 | ||||||
|  | // nonzero only while a repeated key is being processed.
 | ||||||
|  | static int8_t processing_repeat_count = 0; | ||||||
|  | 
 | ||||||
|  | uint16_t get_last_keycode(void) { | ||||||
|  |     return last_record.keycode; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | uint8_t get_last_mods(void) { | ||||||
|  |     return last_mods; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void set_last_keycode(uint16_t keycode) { | ||||||
|  |     set_last_record(keycode, &(keyrecord_t){ | ||||||
|  | #ifndef NO_ACTION_TAPPING | ||||||
|  |                                  .tap.interrupted = false, | ||||||
|  |                                  .tap.count       = 1, | ||||||
|  | #endif | ||||||
|  |                              }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void set_last_mods(uint8_t mods) { | ||||||
|  |     last_mods = mods; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void set_last_record(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     last_record         = *record; | ||||||
|  |     last_record.keycode = keycode; | ||||||
|  |     last_repeat_count   = 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** @brief Updates `last_repeat_count` in direction `dir`. */ | ||||||
|  | static void update_last_repeat_count(int8_t dir) { | ||||||
|  |     if (dir * last_repeat_count < 0) { | ||||||
|  |         last_repeat_count = dir; | ||||||
|  |     } else if (dir * last_repeat_count < 127) { | ||||||
|  |         last_repeat_count += dir; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | int8_t get_repeat_key_count(void) { | ||||||
|  |     return processing_repeat_count; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void repeat_key_invoke(const keyevent_t* event) { | ||||||
|  |     // It is possible (e.g. in rolled presses) that the last key changes while
 | ||||||
|  |     // the Repeat Key is pressed. To prevent stuck keys, it is important to
 | ||||||
|  |     // remember separately what key record was processed on press so that the
 | ||||||
|  |     // the corresponding record is generated on release.
 | ||||||
|  |     static keyrecord_t registered_record       = {0}; | ||||||
|  |     static int8_t      registered_repeat_count = 0; | ||||||
|  |     // Since this function calls process_record(), it may recursively call
 | ||||||
|  |     // itself. We return early if `processing_repeat_count` is nonzero to
 | ||||||
|  |     // prevent infinite recursion.
 | ||||||
|  |     if (processing_repeat_count || !last_record.keycode) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (event->pressed) { | ||||||
|  |         update_last_repeat_count(1); | ||||||
|  |         // On press, apply the last mods state, stacking on top of current mods.
 | ||||||
|  |         register_weak_mods(last_mods); | ||||||
|  |         registered_record       = last_record; | ||||||
|  |         registered_repeat_count = last_repeat_count; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Generate a keyrecord and plumb it into the event pipeline.
 | ||||||
|  |     registered_record.event = *event; | ||||||
|  |     processing_repeat_count = registered_repeat_count; | ||||||
|  |     process_record(®istered_record); | ||||||
|  |     processing_repeat_count = 0; | ||||||
|  | 
 | ||||||
|  |     // On release, restore the mods state.
 | ||||||
|  |     if (!event->pressed) { | ||||||
|  |         unregister_weak_mods(last_mods); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #ifndef NO_ALT_REPEAT_KEY | ||||||
|  | /**
 | ||||||
|  |  * @brief Find alternate keycode from a table of opposing keycode pairs. | ||||||
|  |  * @param table Array of pairs of basic keycodes, declared as PROGMEM. | ||||||
|  |  * @param table_size_bytes The size of the table in bytes. | ||||||
|  |  * @param target The basic keycode to find. | ||||||
|  |  * @return The alternate basic keycode, or KC_NO if none was found. | ||||||
|  |  * | ||||||
|  |  * @note The table keycodes and target must be basic keycodes. | ||||||
|  |  * | ||||||
|  |  * This helper is used several times below to define alternate keys. Given a | ||||||
|  |  * table of pairs of basic keycodes, the function finds the pair containing | ||||||
|  |  * `target` and returns the other keycode in the pair. | ||||||
|  |  */ | ||||||
|  | static uint8_t find_alt_keycode(const uint8_t (*table)[2], uint8_t table_size_bytes, uint8_t target) { | ||||||
|  |     const uint8_t* keycodes = (const uint8_t*)table; | ||||||
|  |     for (uint8_t i = 0; i < table_size_bytes; ++i) { | ||||||
|  |         if (target == pgm_read_byte(keycodes + i)) { | ||||||
|  |             // Xor (i ^ 1) the index to get the other element in the pair.
 | ||||||
|  |             return pgm_read_byte(keycodes + (i ^ 1)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return KC_NO; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | uint16_t get_alt_repeat_key_keycode(void) { | ||||||
|  |     uint16_t keycode = last_record.keycode; | ||||||
|  |     uint8_t  mods    = last_mods; | ||||||
|  | 
 | ||||||
|  |     // Call the user callback first to give it a chance to override the default
 | ||||||
|  |     // alternate key definitions that follow.
 | ||||||
|  |     uint16_t alt_keycode = get_alt_repeat_key_keycode_user(keycode, mods); | ||||||
|  | 
 | ||||||
|  |     if (alt_keycode != KC_TRANSPARENT) { | ||||||
|  |         return alt_keycode; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Convert 8-bit mods to the 5-bit format used in keycodes. This is lossy:
 | ||||||
|  |     // if left and right handed mods were mixed, they all become right handed.
 | ||||||
|  |     mods = ((mods & 0xf0) ? /* set right hand bit */ 0x10 : 0) | ||||||
|  |            // Combine right and left hand mods.
 | ||||||
|  |            | (((mods >> 4) | mods) & 0xf); | ||||||
|  | 
 | ||||||
|  |     switch (keycode) { | ||||||
|  |         case QK_MODS ... QK_MODS_MAX: // Unpack modifier + basic key.
 | ||||||
|  |             mods |= QK_MODS_GET_MODS(keycode); | ||||||
|  |             keycode = QK_MODS_GET_BASIC_KEYCODE(keycode); | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  | #    ifndef NO_ACTION_TAPPING | ||||||
|  |         case QK_MOD_TAP ... QK_MOD_TAP_MAX: | ||||||
|  |             keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode); | ||||||
|  |             break; | ||||||
|  | #        ifndef NO_ACTION_LAYER | ||||||
|  |         case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: | ||||||
|  |             keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode); | ||||||
|  |             break; | ||||||
|  | #        endif // NO_ACTION_LAYER
 | ||||||
|  | #    endif     // NO_ACTION_TAPPING
 | ||||||
|  | 
 | ||||||
|  | #    ifdef SWAP_HANDS_ENABLE | ||||||
|  |         case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: | ||||||
|  |             if (IS_SWAP_HANDS_KEYCODE(keycode)) { | ||||||
|  |                 return KC_NO; | ||||||
|  |             } | ||||||
|  |             keycode = QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode); | ||||||
|  |             break; | ||||||
|  | #    endif // SWAP_HANDS_ENABLE
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (IS_QK_BASIC(keycode)) { | ||||||
|  |         if ((mods & (MOD_LCTL | MOD_LALT | MOD_LGUI))) { | ||||||
|  |             // The last key was pressed with a modifier other than Shift.
 | ||||||
|  |             // The following maps
 | ||||||
|  |             //   mod + F <-> mod + B
 | ||||||
|  |             // and a few others, supporting several core hotkeys used in
 | ||||||
|  |             // Emacs, Vim, less, and other programs.
 | ||||||
|  |             // clang-format off
 | ||||||
|  |             static const uint8_t pairs[][2] PROGMEM = { | ||||||
|  |                 {KC_F   , KC_B   },  // Forward / Backward.
 | ||||||
|  |                 {KC_D   , KC_U   },  // Down / Up.
 | ||||||
|  |                 {KC_N   , KC_P   },  // Next / Previous.
 | ||||||
|  |                 {KC_A   , KC_E   },  // Home / End.
 | ||||||
|  |                 {KC_O   , KC_I   },  // Older / Newer in Vim jump list.
 | ||||||
|  |             }; | ||||||
|  |             // clang-format on
 | ||||||
|  |             alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode); | ||||||
|  |         } else { | ||||||
|  |             // The last key was pressed with no mods or only Shift. The
 | ||||||
|  |             // following map a few more Vim hotkeys.
 | ||||||
|  |             // clang-format off
 | ||||||
|  |             static const uint8_t pairs[][2] PROGMEM = { | ||||||
|  |                 {KC_J   , KC_K   },  // Down / Up.
 | ||||||
|  |                 {KC_H   , KC_L   },  // Left / Right.
 | ||||||
|  |                 // These two lines map W and E to B, and B to W.
 | ||||||
|  |                 {KC_W   , KC_B   },  // Forward / Backward by word.
 | ||||||
|  |                 {KC_E   , KC_B   },  // Forward / Backward by word.
 | ||||||
|  |             }; | ||||||
|  |             // clang-format on
 | ||||||
|  |             alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!alt_keycode) { | ||||||
|  |             // The following key pairs are considered with any mods.
 | ||||||
|  |             // clang-format off
 | ||||||
|  |             static const uint8_t pairs[][2] PROGMEM = { | ||||||
|  |                 {KC_LEFT, KC_RGHT},  // Left / Right Arrow.
 | ||||||
|  |                 {KC_UP  , KC_DOWN},  // Up / Down Arrow.
 | ||||||
|  |                 {KC_HOME, KC_END },  // Home / End.
 | ||||||
|  |                 {KC_PGUP, KC_PGDN},  // Page Up / Page Down.
 | ||||||
|  |                 {KC_BSPC, KC_DEL },  // Backspace / Delete.
 | ||||||
|  |                 {KC_LBRC, KC_RBRC},  // Brackets [ ] and { }.
 | ||||||
|  | #ifdef EXTRAKEY_ENABLE | ||||||
|  |                 {KC_WBAK, KC_WFWD},  // Browser Back / Forward.
 | ||||||
|  |                 {KC_MNXT, KC_MPRV},  // Next / Previous Media Track.
 | ||||||
|  |                 {KC_MFFD, KC_MRWD},  // Fast Forward / Rewind Media.
 | ||||||
|  |                 {KC_VOLU, KC_VOLD},  // Volume Up / Down.
 | ||||||
|  |                 {KC_BRIU, KC_BRID},  // Brightness Up / Down.
 | ||||||
|  | #endif  // EXTRAKEY_ENABLE
 | ||||||
|  | #ifdef MOUSEKEY_ENABLE | ||||||
|  |                 {KC_MS_L, KC_MS_R},  // Mouse Cursor Left / Right.
 | ||||||
|  |                 {KC_MS_U, KC_MS_D},  // Mouse Cursor Up / Down.
 | ||||||
|  |                 {KC_WH_L, KC_WH_R},  // Mouse Wheel Left / Right.
 | ||||||
|  |                 {KC_WH_U, KC_WH_D},  // Mouse Wheel Up / Down.
 | ||||||
|  | #endif  // MOUSEKEY_ENABLE
 | ||||||
|  |             }; | ||||||
|  |             // clang-format on
 | ||||||
|  |             alt_keycode = find_alt_keycode(pairs, sizeof(pairs), keycode); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (alt_keycode) { | ||||||
|  |             // Combine basic keycode with mods.
 | ||||||
|  |             return (mods << 8) | alt_keycode; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return KC_NO; // No alternate key found.
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void alt_repeat_key_invoke(const keyevent_t* event) { | ||||||
|  |     static keyrecord_t registered_record       = {0}; | ||||||
|  |     static int8_t      registered_repeat_count = 0; | ||||||
|  |     // Since this function calls process_record(), it may recursively call
 | ||||||
|  |     // itself. We return early if `processing_repeat_count` is nonzero to
 | ||||||
|  |     // prevent infinite recursion.
 | ||||||
|  |     if (processing_repeat_count) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (event->pressed) { | ||||||
|  |         registered_record = (keyrecord_t){ | ||||||
|  | #    ifndef NO_ACTION_TAPPING | ||||||
|  |             .tap.interrupted = false, | ||||||
|  |             .tap.count       = 0, | ||||||
|  | #    endif | ||||||
|  |             .keycode = get_alt_repeat_key_keycode(), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Early return if there is no alternate key defined.
 | ||||||
|  |     if (!registered_record.keycode) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (event->pressed) { | ||||||
|  |         update_last_repeat_count(-1); | ||||||
|  |         registered_repeat_count = last_repeat_count; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Generate a keyrecord and plumb it into the event pipeline.
 | ||||||
|  |     registered_record.event = *event; | ||||||
|  |     processing_repeat_count = registered_repeat_count; | ||||||
|  |     process_record(®istered_record); | ||||||
|  |     processing_repeat_count = 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Default implementation of get_alt_repeat_key_keycode_user().
 | ||||||
|  | __attribute__((weak)) uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     return KC_TRANSPARENT; | ||||||
|  | } | ||||||
|  | #endif // NO_ALT_REPEAT_KEY
 | ||||||
							
								
								
									
										80
									
								
								quantum/repeat_key.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								quantum/repeat_key.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | // Copyright 2022-2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     https://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "quantum.h" | ||||||
|  | 
 | ||||||
|  | uint16_t get_last_keycode(void);             /**< Keycode of the last key. */ | ||||||
|  | uint8_t  get_last_mods(void);                /**< Mods active with the last key. */ | ||||||
|  | void     set_last_keycode(uint16_t keycode); /**< Sets the last key. */ | ||||||
|  | void     set_last_mods(uint8_t mods);        /**< Sets the last mods. */ | ||||||
|  | 
 | ||||||
|  | /** @brief Gets the record for the last key. */ | ||||||
|  | keyrecord_t* get_last_record(void); | ||||||
|  | 
 | ||||||
|  | /** @brief Sets keycode and record info for the last key. */ | ||||||
|  | void set_last_record(uint16_t keycode, keyrecord_t* record); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Signed count of times the key has been repeated or alternate repeated. | ||||||
|  |  * | ||||||
|  |  * @note The count is nonzero only while a repeated or alternate-repeated key is | ||||||
|  |  *       being processed. | ||||||
|  |  * | ||||||
|  |  * When a key is pressed normally, the count is 0. When the Repeat Key is used | ||||||
|  |  * to repeat a key, the count is 1 on the first repeat, 2 on the second repeat, | ||||||
|  |  * and continuing up to 127. | ||||||
|  |  * | ||||||
|  |  * Negative counts are used similarly for alternate repeating. When the | ||||||
|  |  * Alternate Repeat Key is used, the count is -1 on the first alternate repeat, | ||||||
|  |  * -2 on the second, continuing down to -127. | ||||||
|  |  */ | ||||||
|  | int8_t get_repeat_key_count(void); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Calls `process_record()` on a generated record repeating the last key. | ||||||
|  |  * @param event Event information in the generated record. | ||||||
|  |  */ | ||||||
|  | void repeat_key_invoke(const keyevent_t* event); | ||||||
|  | 
 | ||||||
|  | #ifndef NO_ALT_REPEAT_KEY | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Keycode to be used for alternate repeating. | ||||||
|  |  * | ||||||
|  |  * Alternate Repeat performs this keycode based on the last eligible pressed key | ||||||
|  |  * and mods, get_last_keycode() and get_last_mods(). For example, when the last | ||||||
|  |  * key was KC_UP, this function returns KC_DOWN. The function returns KC_NO if | ||||||
|  |  * the last key doesn't have a defined alternate. | ||||||
|  |  */ | ||||||
|  | uint16_t get_alt_repeat_key_keycode(void); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Calls `process_record()` to alternate repeat the last key. | ||||||
|  |  * @param event Event information in the generated record. | ||||||
|  |  */ | ||||||
|  | void alt_repeat_key_invoke(const keyevent_t* event); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Optional user callback to define additional alternate keys. | ||||||
|  |  * | ||||||
|  |  * When `get_alt_repeat_key_keycode()` is called, it first calls this callback. | ||||||
|  |  * It should return a keycode representing the "alternate" of the given keycode | ||||||
|  |  * and mods. Returning KC_NO defers to the default definitions in | ||||||
|  |  * `get_alt_repeat_key_keycode()`. | ||||||
|  |  */ | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods); | ||||||
|  | 
 | ||||||
|  | #endif // NO_ALT_REPEAT_KEY
 | ||||||
							
								
								
									
										18
									
								
								tests/repeat_key/alt_repeat_key/config.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/repeat_key/alt_repeat_key/config.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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/>.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "test_common.h" | ||||||
							
								
								
									
										18
									
								
								tests/repeat_key/alt_repeat_key/test.mk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/repeat_key/alt_repeat_key/test.mk
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | # Copyright 2023 Google LLC
 | ||||||
|  | #
 | ||||||
|  | # 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/>.
 | ||||||
|  | 
 | ||||||
|  | REPEAT_KEY_ENABLE = yes | ||||||
|  | 
 | ||||||
|  | EXTRAKEY_ENABLE = yes | ||||||
							
								
								
									
										523
									
								
								tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										523
									
								
								tests/repeat_key/alt_repeat_key/test_alt_repeat_key.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,523 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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 <functional> | ||||||
|  | 
 | ||||||
|  | #include "keyboard_report_util.hpp" | ||||||
|  | #include "keycode.h" | ||||||
|  | #include "test_common.hpp" | ||||||
|  | #include "test_fixture.hpp" | ||||||
|  | #include "test_keymap_key.hpp" | ||||||
|  | 
 | ||||||
|  | using ::testing::AnyNumber; | ||||||
|  | using ::testing::InSequence; | ||||||
|  | 
 | ||||||
|  | namespace { | ||||||
|  | 
 | ||||||
|  | bool process_record_user_default(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | uint16_t get_alt_repeat_key_keycode_user_default(uint16_t keycode, uint8_t mods) { | ||||||
|  |     return KC_TRNS; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Indirections so that process_record_user() can be replaced with other
 | ||||||
|  | // functions in the test cases below.
 | ||||||
|  | std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun             = process_record_user_default; | ||||||
|  | std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun          = remember_last_key_user_default; | ||||||
|  | std::function<uint16_t(uint16_t, uint8_t)>            get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default; | ||||||
|  | 
 | ||||||
|  | extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     return process_record_user_fun(keycode, record); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     return remember_last_key_user_fun(keycode, record, remembered_mods); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | extern "C" uint16_t get_alt_repeat_key_keycode_user(uint16_t keycode, uint8_t mods) { | ||||||
|  |     return get_alt_repeat_key_keycode_user_fun(keycode, mods); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class AltRepeatKey : public TestFixture { | ||||||
|  |    public: | ||||||
|  |     bool process_record_user_was_called_; | ||||||
|  | 
 | ||||||
|  |     void SetUp() override { | ||||||
|  |         process_record_user_fun             = process_record_user_default; | ||||||
|  |         remember_last_key_user_fun          = remember_last_key_user_default; | ||||||
|  |         get_alt_repeat_key_keycode_user_fun = get_alt_repeat_key_keycode_user_default; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) { | ||||||
|  |         process_record_user_was_called_ = false; | ||||||
|  |         process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |             EXPECT_EQ(record->event.pressed, expected_press); | ||||||
|  |             EXPECT_KEYCODE_EQ(keycode, expected_keycode); | ||||||
|  |             EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count); | ||||||
|  |             // Tests below use this to verify process_record_user() was called.
 | ||||||
|  |             process_record_user_was_called_ = true; | ||||||
|  |             return true; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Expects that the characters of `s` are sent.
 | ||||||
|  |     // NOTE: This implementation is limited to chars a-z, A-Z.
 | ||||||
|  |     void ExpectString(TestDriver& driver, const std::string& s) { | ||||||
|  |         InSequence seq; | ||||||
|  |         for (int c : s) { | ||||||
|  |             switch (c) { | ||||||
|  |                 case 'a' ... 'z': { // Lowercase letter.
 | ||||||
|  |                     uint16_t keycode = c - ('a' - KC_A); | ||||||
|  |                     EXPECT_REPORT(driver, (keycode)); | ||||||
|  |                 } break; | ||||||
|  | 
 | ||||||
|  |                 case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
 | ||||||
|  |                     uint16_t keycode = c - ('A' - KC_A); | ||||||
|  |                     EXPECT_REPORT(driver, (KC_LSFT, keycode)); | ||||||
|  |                 } break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | TEST_F(AltRepeatKey, AlternateBasic) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_bspc(0, 0, 0, KC_BSPC); | ||||||
|  |     KeymapKey  key_pgdn(0, 1, 0, KC_PGDN); | ||||||
|  |     KeymapKey  key_pgup(0, 2, 0, KC_PGUP); | ||||||
|  |     KeymapKey  key_repeat(0, 4, 0, QK_REP); | ||||||
|  |     KeymapKey  key_alt_repeat(0, 5, 0, QK_AREP); | ||||||
|  |     set_keymap({key_bspc, key_pgdn, key_pgup, key_repeat, key_alt_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_BSPC)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_DEL)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_DEL)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_BSPC)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_DEL)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_PGDN)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_PGUP)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_PGUP)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_PGDN)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tap_key(key_bspc); | ||||||
|  | 
 | ||||||
|  |     for (int n = 1; n <= 2; ++n) { // Tap the Alternate Repeat Key twice.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(true, KC_DEL, -n); | ||||||
|  |         key_alt_repeat.press(); // Press the Alternate Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |         // Expect the corresponding release event.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(false, KC_DEL, -n); | ||||||
|  |         key_alt_repeat.release(); // Release the Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_keys(key_repeat, key_alt_repeat); | ||||||
|  |     tap_keys(key_pgdn, key_alt_repeat); | ||||||
|  |     tap_keys(key_pgup, key_alt_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct TestParamsAlternateKeyCodes { | ||||||
|  |     uint16_t keycode; | ||||||
|  |     uint8_t  mods; | ||||||
|  |     uint16_t expected_alt_keycode; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Tests `get_alt_repeat_key_keycode()` for various keycodes.
 | ||||||
|  | TEST_F(AltRepeatKey, GetAltRepeatKeyKeycode) { | ||||||
|  |     for (const auto& params : std::vector<TestParamsAlternateKeyCodes>({ | ||||||
|  |              // clang-format off
 | ||||||
|  |           // Each line tests one call to `get_alt_repeat_key_keycode()`:
 | ||||||
|  |           // {keycode, mods, expected_alt_keycode}.
 | ||||||
|  |           // Arrows.
 | ||||||
|  |           {KC_LEFT, 0, KC_RGHT}, | ||||||
|  |           {KC_RGHT, 0, KC_LEFT}, | ||||||
|  |           {KC_LEFT, MOD_BIT(KC_LSFT), LSFT(KC_RGHT)}, | ||||||
|  |           {KC_LEFT, MOD_BIT(KC_RSFT), RSFT(KC_RGHT)}, | ||||||
|  |           {KC_LEFT, MOD_BIT(KC_LCTL) | MOD_BIT(KC_LSFT), C(S(KC_RGHT))}, | ||||||
|  |           {KC_LEFT, MOD_BIT(KC_LGUI), LGUI(KC_RGHT)}, | ||||||
|  |           {C(KC_LEFT), MOD_BIT(KC_LSFT), C(S(KC_RGHT))}, | ||||||
|  |           {KC_UP, 0, KC_DOWN}, | ||||||
|  |           // Navigation keys.
 | ||||||
|  |           {KC_PGUP, 0, KC_PGDN}, | ||||||
|  |           {KC_HOME, 0, KC_END }, | ||||||
|  |           // Media keys.
 | ||||||
|  |           {KC_WBAK, 0, KC_WFWD}, | ||||||
|  |           {KC_MNXT, 0, KC_MPRV}, | ||||||
|  |           {KC_MRWD, 0, KC_MFFD}, | ||||||
|  |           {KC_VOLU, 0, KC_VOLD}, | ||||||
|  |           {KC_BRIU, 0, KC_BRID}, | ||||||
|  |           // Emacs navigation.
 | ||||||
|  |           {KC_N, MOD_BIT(KC_LCTL), C(KC_P)}, | ||||||
|  |           {KC_B, MOD_BIT(KC_LCTL), LCTL(KC_F)}, | ||||||
|  |           {KC_B, MOD_BIT(KC_RCTL), RCTL(KC_F)}, | ||||||
|  |           {KC_B, MOD_BIT(KC_LALT), LALT(KC_F)}, | ||||||
|  |           {KC_F, MOD_BIT(KC_LCTL), C(KC_B)}, | ||||||
|  |           {KC_A, MOD_BIT(KC_LCTL), C(KC_E)}, | ||||||
|  |           {KC_D, MOD_BIT(KC_LCTL), C(KC_U)}, | ||||||
|  |           // Vim navigation.
 | ||||||
|  |           {KC_J, 0, KC_K}, | ||||||
|  |           {KC_K, 0, KC_J}, | ||||||
|  |           {KC_H, 0, KC_L}, | ||||||
|  |           {KC_B, 0, KC_W}, | ||||||
|  |           {KC_W, 0, KC_B}, | ||||||
|  |           {KC_E, 0, KC_B}, | ||||||
|  |           {KC_B, MOD_BIT(KC_LSFT), S(KC_W)}, | ||||||
|  |           {KC_W, MOD_BIT(KC_LSFT), S(KC_B)}, | ||||||
|  |           {KC_E, MOD_BIT(KC_LSFT), S(KC_B)}, | ||||||
|  |           {KC_O, MOD_BIT(KC_LCTL), C(KC_I)}, | ||||||
|  |           {KC_I, MOD_BIT(KC_LCTL), C(KC_O)}, | ||||||
|  |           // Other.
 | ||||||
|  |           {KC_DEL, 0, KC_BSPC}, | ||||||
|  |           {KC_LBRC, 0, KC_RBRC}, | ||||||
|  |           {KC_LCBR, 0, KC_RCBR}, | ||||||
|  |           // Some keys where the last key is a tap-hold key.
 | ||||||
|  |           {LSFT_T(KC_F), MOD_BIT(KC_RCTL), RCTL(KC_B)}, | ||||||
|  |           {LT(1, KC_A), MOD_BIT(KC_RGUI), RGUI(KC_E)}, | ||||||
|  |           {RALT_T(KC_J), 0, KC_K}, | ||||||
|  |           // Some keys where no alternate is defined.
 | ||||||
|  |           {KC_A, 0, KC_NO}, | ||||||
|  |           {KC_F1, 0, KC_NO}, | ||||||
|  |           {QK_LEAD, 0, KC_NO}, | ||||||
|  |           {MO(1), 0, KC_NO}, | ||||||
|  |              // clang-format on
 | ||||||
|  |          })) { | ||||||
|  |         SCOPED_TRACE(std::string("Input keycode: ") + get_keycode_identifier_or_default(params.keycode)); | ||||||
|  |         set_last_keycode(params.keycode); | ||||||
|  |         set_last_mods(params.mods); | ||||||
|  | 
 | ||||||
|  |         const uint16_t actual = get_alt_repeat_key_keycode(); | ||||||
|  | 
 | ||||||
|  |         EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), params.expected_alt_keycode); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Test adding to and overriding the above through the
 | ||||||
|  | // `get_alt_repeat_key_keycode_user()` callback.
 | ||||||
|  | TEST_F(AltRepeatKey, GetAltRepeatKeyKeycodeUser) { | ||||||
|  |     get_alt_repeat_key_keycode_user_fun = [](uint16_t keycode, uint8_t mods) -> uint16_t { | ||||||
|  |         bool shifted = (mods & MOD_MASK_SHIFT); | ||||||
|  |         switch (keycode) { | ||||||
|  |             case KC_LEFT: | ||||||
|  |                 return KC_ENT; | ||||||
|  |             case MO(1): | ||||||
|  |                 return TG(1); | ||||||
|  |             case KC_TAB: // Tab <-> Shift + Tab example.
 | ||||||
|  |                 if (shifted) { | ||||||
|  |                     return KC_TAB; | ||||||
|  |                 } else { | ||||||
|  |                     return S(KC_TAB); | ||||||
|  |                 } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Ctrl + Y <-> Ctrl + Z example.
 | ||||||
|  |         if ((mods & MOD_MASK_CTRL)) { | ||||||
|  |             switch (keycode) { | ||||||
|  |                 case KC_Y: | ||||||
|  |                     return C(KC_Z); | ||||||
|  |                 case KC_Z: | ||||||
|  |                     return C(KC_Y); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return KC_NO; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_LEFT); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_ENT); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(MO(1)); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), TG(1)); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_TAB); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), S(KC_TAB)); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_TAB); | ||||||
|  |     set_last_mods(MOD_BIT(KC_LSFT)); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_TAB); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_Z); | ||||||
|  |     set_last_mods(MOD_BIT(KC_LCTL)); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), C(KC_Y)); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_Y); | ||||||
|  |     set_last_mods(MOD_BIT(KC_LCTL)); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), C(KC_Z)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests rolling from a key to Alternate Repeat.
 | ||||||
|  | TEST_F(AltRepeatKey, RollingToAltRepeat) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_left(0, 0, 0, KC_LEFT); | ||||||
|  |     KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP); | ||||||
|  |     set_keymap({key_left, key_alt_repeat}); | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LEFT, KC_RGHT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RGHT)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RGHT)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Perform a rolled press from Left to Alternate Repeat.
 | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_LEFT, 0); | ||||||
|  |     key_left.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_RGHT, -1); | ||||||
|  |     key_alt_repeat.press(); // Press the Alternate Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_LEFT, 0); | ||||||
|  |     key_left.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_RGHT, -1); | ||||||
|  |     key_alt_repeat.release(); // Release the Alternate Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_key(key_alt_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests rolling from Alternate Repeat to another key.
 | ||||||
|  | TEST_F(AltRepeatKey, RollingFromAltRepeat) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_left(0, 0, 0, KC_LEFT); | ||||||
|  |     KeymapKey  key_up(0, 1, 0, KC_UP); | ||||||
|  |     KeymapKey  key_alt_repeat(0, 2, 0, QK_AREP); | ||||||
|  |     set_keymap({key_left, key_up, key_alt_repeat}); | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_LEFT)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RGHT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RGHT, KC_UP)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_UP)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_DOWN)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tap_key(key_left); | ||||||
|  | 
 | ||||||
|  |     // Perform a rolled press from Alternate Repeat to Up.
 | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_RGHT, -1); | ||||||
|  |     key_alt_repeat.press(); // Press the Alternate Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_UP, 0); | ||||||
|  |     key_up.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_UP); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_RGHT, -1); | ||||||
|  |     key_alt_repeat.release(); // Release the Alternate Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_UP, 0); | ||||||
|  |     key_up.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_key(key_alt_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests using the Alternate Repeat Key on a macro that doesn't have an
 | ||||||
|  | // alternate keycode defined.
 | ||||||
|  | TEST_F(AltRepeatKey, AlternateUnsupportedMacro) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_foo(0, 0, 0, QK_USER_0); | ||||||
|  |     KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP); | ||||||
|  |     set_keymap({key_foo, key_alt_repeat}); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |         process_record_user_was_called_ = true; | ||||||
|  |         switch (keycode) { | ||||||
|  |             case QK_USER_0: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     SEND_STRING("foo"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "foofoo"); | ||||||
|  | 
 | ||||||
|  |     process_record_user_was_called_ = false; | ||||||
|  |     tap_key(key_foo); | ||||||
|  | 
 | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), QK_USER_0); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_alt_repeat_key_keycode(), KC_NO); | ||||||
|  | 
 | ||||||
|  |     process_record_user_was_called_ = false; | ||||||
|  |     key_alt_repeat.press(); // Press Alternate Repeat.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     EXPECT_FALSE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_was_called_ = false; | ||||||
|  |     key_alt_repeat.release(); // Release Alternate Repeat.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     EXPECT_FALSE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_was_called_ = false; | ||||||
|  |     tap_key(key_foo); | ||||||
|  | 
 | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests a macro with custom alternate behavior.
 | ||||||
|  | TEST_F(AltRepeatKey, MacroCustomAlternate) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_foo(0, 0, 0, QK_USER_0); | ||||||
|  |     KeymapKey  key_alt_repeat(0, 1, 0, QK_AREP); | ||||||
|  |     set_keymap({key_foo, key_alt_repeat}); | ||||||
|  | 
 | ||||||
|  |     get_alt_repeat_key_keycode_user_fun = [](uint16_t keycode, uint8_t mods) -> uint16_t { | ||||||
|  |         switch (keycode) { | ||||||
|  |             case QK_USER_0: | ||||||
|  |                 return QK_USER_0; // QK_USER_0 handles its own alternate.
 | ||||||
|  |             default: | ||||||
|  |                 return KC_NO; // No key by default.
 | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |         process_record_user_was_called_ = true; | ||||||
|  |         switch (keycode) { | ||||||
|  |             case QK_USER_0: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     if (get_repeat_key_count() >= 0) { | ||||||
|  |                         SEND_STRING("foo"); | ||||||
|  |                     } else { // Key is being alternate repeated.
 | ||||||
|  |                         SEND_STRING("bar"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "foobarbar"); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_foo, key_alt_repeat, key_alt_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests the Additional "Alternate" keys example from the documentation page.
 | ||||||
|  | TEST_F(AltRepeatKey, AdditionalAlternateKeysExample) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_w(0, 1, 0, KC_W); | ||||||
|  |     KeymapKey  key_altrep2(0, 2, 0, QK_USER_0); | ||||||
|  |     KeymapKey  key_altrep3(0, 3, 0, QK_USER_1); | ||||||
|  |     set_keymap({key_a, key_w, key_altrep2, key_altrep3}); | ||||||
|  | 
 | ||||||
|  |     remember_last_key_user_fun = [](uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |         switch (keycode) { | ||||||
|  |             case QK_USER_0: | ||||||
|  |             case QK_USER_1: | ||||||
|  |                 return false; // Ignore ALTREP keys.
 | ||||||
|  |         } | ||||||
|  |         return true; // Other keys can be repeated.
 | ||||||
|  |     }; | ||||||
|  |     process_record_user_fun = [=](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |         switch (keycode) { | ||||||
|  |             case QK_USER_0: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     const uint16_t last_key = get_last_keycode(); | ||||||
|  |                     switch (last_key) { | ||||||
|  |                         case KC_A: | ||||||
|  |                             SEND_STRING(/*a*/ "tion"); | ||||||
|  |                             break; | ||||||
|  |                         case KC_W: | ||||||
|  |                             SEND_STRING(/*w*/ "hich"); | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 return false; | ||||||
|  |             case QK_USER_1: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     const uint16_t last_key = get_last_keycode(); | ||||||
|  |                     switch (last_key) { | ||||||
|  |                         case KC_A: | ||||||
|  |                             SEND_STRING(/*a*/ "bout"); | ||||||
|  |                             break; | ||||||
|  |                         case KC_W: | ||||||
|  |                             SEND_STRING(/*w*/ "ould"); | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 return false; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "ationwhichaboutwould"); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_a, key_altrep2, key_w, key_altrep2); | ||||||
|  |     tap_keys(key_a, key_altrep3, key_w, key_altrep3); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace
 | ||||||
							
								
								
									
										20
									
								
								tests/repeat_key/config.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tests/repeat_key/config.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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/>.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "test_common.h" | ||||||
|  | 
 | ||||||
|  | #define NO_ALT_REPEAT_KEY | ||||||
							
								
								
									
										18
									
								
								tests/repeat_key/repeat_key_combo/config.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/repeat_key/repeat_key_combo/config.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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/>.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "test_common.h" | ||||||
							
								
								
									
										18
									
								
								tests/repeat_key/repeat_key_combo/test.mk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/repeat_key/repeat_key_combo/test.mk
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | # Copyright 2023 Google LLC
 | ||||||
|  | #
 | ||||||
|  | # 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/>.
 | ||||||
|  | 
 | ||||||
|  | REPEAT_KEY_ENABLE = yes | ||||||
|  | 
 | ||||||
|  | COMBO_ENABLE = yes | ||||||
							
								
								
									
										67
									
								
								tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								tests/repeat_key/repeat_key_combo/test_repeat_key_combo.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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 "keyboard_report_util.hpp" | ||||||
|  | #include "keycode.h" | ||||||
|  | #include "test_common.hpp" | ||||||
|  | #include "test_fixture.hpp" | ||||||
|  | #include "test_keymap_key.hpp" | ||||||
|  | 
 | ||||||
|  | using ::testing::AnyNumber; | ||||||
|  | using ::testing::InSequence; | ||||||
|  | 
 | ||||||
|  | namespace { | ||||||
|  | 
 | ||||||
|  | extern "C" { | ||||||
|  | // Define a combo: KC_X + KC_Y = KC_Q.
 | ||||||
|  | const uint16_t xy_combo[] PROGMEM = {KC_X, KC_Y, COMBO_END}; | ||||||
|  | combo_t        key_combos[]       = {COMBO(xy_combo, KC_Q)}; | ||||||
|  | uint16_t       COMBO_LEN          = sizeof(key_combos) / sizeof(*key_combos); | ||||||
|  | } // extern "C"
 | ||||||
|  | 
 | ||||||
|  | class RepeatKey : public TestFixture {}; | ||||||
|  | 
 | ||||||
|  | // Tests repeating a combo, KC_X + KC_Y = KC_Q, by typing
 | ||||||
|  | // "X, Repeat, Repeat, {X Y}, Repeat, Repeat". This produces "xxxqqq".
 | ||||||
|  | TEST_F(RepeatKey, Combo) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_x(0, 0, 0, KC_X); | ||||||
|  |     KeymapKey  key_y(0, 1, 0, KC_Y); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_x, key_y, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_X)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_X)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_X)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_Q)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_Q)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_Q)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_x, key_repeat, key_repeat); | ||||||
|  |     tap_combo({key_x, key_y}); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_Q); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace
 | ||||||
							
								
								
									
										18
									
								
								tests/repeat_key/test.mk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/repeat_key/test.mk
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | # Copyright 2023 Google LLC
 | ||||||
|  | #
 | ||||||
|  | # 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/>.
 | ||||||
|  | 
 | ||||||
|  | REPEAT_KEY_ENABLE = yes | ||||||
|  | 
 | ||||||
|  | AUTO_SHIFT_ENABLE = yes | ||||||
							
								
								
									
										754
									
								
								tests/repeat_key/test_repeat_key.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										754
									
								
								tests/repeat_key/test_repeat_key.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,754 @@ | |||||||
|  | // Copyright 2023 Google LLC
 | ||||||
|  | //
 | ||||||
|  | // 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 <functional> | ||||||
|  | 
 | ||||||
|  | #include "keyboard_report_util.hpp" | ||||||
|  | #include "keycode.h" | ||||||
|  | #include "test_common.hpp" | ||||||
|  | #include "test_fixture.hpp" | ||||||
|  | #include "test_keymap_key.hpp" | ||||||
|  | 
 | ||||||
|  | using ::testing::AnyNumber; | ||||||
|  | using ::testing::AnyOf; | ||||||
|  | using ::testing::InSequence; | ||||||
|  | 
 | ||||||
|  | #define FOO_MACRO SAFE_RANGE | ||||||
|  | 
 | ||||||
|  | namespace { | ||||||
|  | 
 | ||||||
|  | bool process_record_user_default(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool remember_last_key_user_default(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Indirection so that process_record_user() and remember_last_key_user()
 | ||||||
|  | // can be replaced with other functions in the test cases below.
 | ||||||
|  | std::function<bool(uint16_t, keyrecord_t*)>           process_record_user_fun    = process_record_user_default; | ||||||
|  | std::function<bool(uint16_t, keyrecord_t*, uint8_t*)> remember_last_key_user_fun = remember_last_key_user_default; | ||||||
|  | 
 | ||||||
|  | extern "C" bool process_record_user(uint16_t keycode, keyrecord_t* record) { | ||||||
|  |     return process_record_user_fun(keycode, record); | ||||||
|  | } | ||||||
|  | extern "C" bool remember_last_key_user(uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |     return remember_last_key_user_fun(keycode, record, remembered_mods); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class RepeatKey : public TestFixture { | ||||||
|  |    public: | ||||||
|  |     bool process_record_user_was_called_; | ||||||
|  | 
 | ||||||
|  |     void SetUp() override { | ||||||
|  |         autoshift_disable(); | ||||||
|  |         process_record_user_fun    = process_record_user_default; | ||||||
|  |         remember_last_key_user_fun = remember_last_key_user_default; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void ExpectProcessRecordUserCalledWith(bool expected_press, uint16_t expected_keycode, int8_t expected_repeat_key_count) { | ||||||
|  |         process_record_user_was_called_ = false; | ||||||
|  |         process_record_user_fun         = [=](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |             EXPECT_EQ(record->event.pressed, expected_press); | ||||||
|  |             EXPECT_KEYCODE_EQ(keycode, expected_keycode); | ||||||
|  |             EXPECT_EQ(get_repeat_key_count(), expected_repeat_key_count); | ||||||
|  |             // Tests below use this to verify process_record_user() was called.
 | ||||||
|  |             process_record_user_was_called_ = true; | ||||||
|  |             return true; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Expects that the characters of `s` are sent.
 | ||||||
|  |     // NOTE: This implementation is limited to chars a-z, A-Z.
 | ||||||
|  |     void ExpectString(TestDriver& driver, const std::string& s) { | ||||||
|  |         InSequence seq; | ||||||
|  |         for (int c : s) { | ||||||
|  |             switch (c) { | ||||||
|  |                 case 'a' ... 'z': { // Lowercase letter.
 | ||||||
|  |                     uint16_t keycode = c - ('a' - KC_A); | ||||||
|  |                     EXPECT_REPORT(driver, (keycode)); | ||||||
|  |                 } break; | ||||||
|  | 
 | ||||||
|  |                 case 'A' ... 'Z': { // Capital letter = KC_LSFT + letter key.
 | ||||||
|  |                     uint16_t keycode = c - ('A' - KC_A); | ||||||
|  |                     EXPECT_REPORT(driver, (KC_LSFT, keycode)); | ||||||
|  |                 } break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Tests that "A, Repeat, Repeat, B, Repeat" produces "aaabb".
 | ||||||
|  | TEST_F(RepeatKey, Basic) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_b(0, 1, 0, KC_B); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_b, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "aaabb"); | ||||||
|  | 
 | ||||||
|  |     // When KC_A is pressed, process_record_user() should be called
 | ||||||
|  |     // with a press event with keycode == KC_A and repeat_key_count() == 0.
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_A, 0); | ||||||
|  |     key_a.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     // After pressing A, the keycode of the key to be repeated is KC_A.
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A); | ||||||
|  |     EXPECT_EQ(get_last_mods(), 0); | ||||||
|  | 
 | ||||||
|  |     // Expect the corresponding release event when A is released.
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_A, 0); | ||||||
|  |     key_a.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     for (int n = 1; n <= 2; ++n) { // Tap the Repeat Key twice.
 | ||||||
|  |         // When Repeat is pressed, process_record_user() should be called with a
 | ||||||
|  |         // press event with keycode == KC_A and repeat_key_count() == n.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(true, KC_A, n); | ||||||
|  |         key_repeat.press(); // Press the Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |         // Expect the corresponding release event.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(false, KC_A, n); | ||||||
|  |         key_repeat.release(); // Release the Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_key(key_b); | ||||||
|  |     // Then after tapping key_b, the keycode to be repeated becomes KC_B.
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests repeating a macro. The keycode FOO_MACRO sends "foo" when pressed. The
 | ||||||
|  | // test taps "FOO_MACRO, Repeat, Repeat", producing "foofoofoo".
 | ||||||
|  | TEST_F(RepeatKey, Macro) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_foo(0, 0, 0, FOO_MACRO); | ||||||
|  |     KeymapKey  key_repeat(0, 1, 0, QK_REP); | ||||||
|  |     set_keymap({key_foo, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Define process_record_user() to handle FOO_MACRO.
 | ||||||
|  |     process_record_user_fun = [](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |         switch (keycode) { | ||||||
|  |             case FOO_MACRO: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     SEND_STRING("foo"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "foofoofoo"); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_foo); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), FOO_MACRO); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests a macro with customized repeat behavior: "foo" is sent normally, "bar"
 | ||||||
|  | // on the first repeat, and "baz" on subsequent repeats. The test taps
 | ||||||
|  | // "FOO_MACRO, Repeat, Repeat, FOO_MACRO, Repeat", producing "foobarbazfoobar".
 | ||||||
|  | TEST_F(RepeatKey, MacroCustomRepeat) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_foo(0, 0, 0, FOO_MACRO); | ||||||
|  |     KeymapKey  key_repeat(0, 1, 0, QK_REP); | ||||||
|  |     set_keymap({key_foo, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = [](uint16_t keycode, keyrecord_t* record) { | ||||||
|  |         switch (keycode) { | ||||||
|  |             case FOO_MACRO: | ||||||
|  |                 if (record->event.pressed) { | ||||||
|  |                     switch (get_repeat_key_count()) { | ||||||
|  |                         case 0: // When pressed normally.
 | ||||||
|  |                             SEND_STRING("foo"); | ||||||
|  |                             break; | ||||||
|  |                         case 1: // On first repeat.
 | ||||||
|  |                             SEND_STRING("bar"); | ||||||
|  |                             break; | ||||||
|  |                         default: // On subsequent repeats.
 | ||||||
|  |                             SEND_STRING("baz"); | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "foobarbazfoobar"); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_foo); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), FOO_MACRO); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat, key_foo, key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests repeating keys on different layers. A 2-layer keymap is defined:
 | ||||||
|  | //   Layer 0:   QK_REP , MO(1)  , KC_A
 | ||||||
|  | //   Layer 1:   KC_TRNS, KC_TRNS, KC_B
 | ||||||
|  | // The test does the following, which should produce "bbbaaa":
 | ||||||
|  | // 1. Hold MO(1), switching to layer 1.
 | ||||||
|  | // 2. Tap KC_B on layer 1.
 | ||||||
|  | // 3. Release MO(1), switching back to layer 0.
 | ||||||
|  | // 4. Tap Repeat twice.
 | ||||||
|  | // 5. Tap KC_A on layer 0.
 | ||||||
|  | // 6. Hold MO(1), switching to layer 1.
 | ||||||
|  | // 7. Tap Repeat twice.
 | ||||||
|  | TEST_F(RepeatKey, AcrossLayers) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_repeat(0, 0, 0, QK_REP); | ||||||
|  |     KeymapKey  key_mo_1(0, 1, 0, MO(1)); | ||||||
|  |     KeymapKey  regular_key(0, 2, 0, KC_A); | ||||||
|  |     set_keymap({// Layer 0.
 | ||||||
|  |                 key_repeat, key_mo_1, regular_key, | ||||||
|  |                 // Layer 1.
 | ||||||
|  |                 KeymapKey{1, 0, 0, KC_TRNS}, KeymapKey{1, 1, 0, KC_TRNS}, KeymapKey{1, 2, 0, KC_B}}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "bbbaaa"); | ||||||
|  | 
 | ||||||
|  |     key_mo_1.press(); // Hold the MO(1) layer key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(regular_key); // Taps the KC_B key on layer 1.
 | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B); | ||||||
|  | 
 | ||||||
|  |     key_mo_1.release(); // Release the layer key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  |     tap_key(regular_key); // Taps the KC_A key on layer 0.
 | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A); | ||||||
|  | 
 | ||||||
|  |     key_mo_1.press(); // Hold the layer key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests "A(down), Repeat(down), A(up), Repeat(up), Repeat" produces "aaa".
 | ||||||
|  | TEST_F(RepeatKey, RollingToRepeat) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_repeat(0, 1, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Perform a rolled press from A to Repeat.
 | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_A, 0); | ||||||
|  |     key_a.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_A, 1); | ||||||
|  |     key_repeat.press(); // Press the Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_A, 0); | ||||||
|  |     key_a.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_A, 1); | ||||||
|  |     key_repeat.release(); // Release the Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests "A, Repeat(down), B(down), Repeat(up), B(up), Repeat" produces "aabb".
 | ||||||
|  | TEST_F(RepeatKey, RollingFromRepeat) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_b(0, 1, 0, KC_B); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_b, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_A, KC_B)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_B)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |         EXPECT_REPORT(driver, (KC_B)); | ||||||
|  |         EXPECT_EMPTY_REPORT(driver); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tap_key(key_a); | ||||||
|  | 
 | ||||||
|  |     // Perform a rolled press from Repeat to B.
 | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_A, 1); | ||||||
|  |     key_repeat.press(); // Press the Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_B, 0); | ||||||
|  |     key_b.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_A, 1); | ||||||
|  |     key_repeat.release(); // Release the Repeat Key.
 | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_B, 0); | ||||||
|  |     key_b.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests Repeat Key with a modifier, types "AltGr+C, Repeat, Repeat, C".
 | ||||||
|  | TEST_F(RepeatKey, RecallMods) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_c(0, 0, 0, KC_C); | ||||||
|  |     KeymapKey  key_altgr(0, 1, 0, KC_RALT); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_c, key_altgr, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only KC_RALT.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_RALT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  | 
 | ||||||
|  |     { // Expect: "AltGr+C, AltGr+C, AltGr+C, C".
 | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_RALT, KC_C)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RALT, KC_C)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_RALT, KC_C)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_C)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     key_altgr.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_c); | ||||||
|  |     key_altgr.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_C); | ||||||
|  |     EXPECT_EQ(get_last_mods(), MOD_BIT(KC_RALT)); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat, key_c); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests that Repeat Key stacks mods, types
 | ||||||
|  | // "Ctrl+Left, Repeat, Shift+Repeat, Shift+Repeat, Repeat, Left".
 | ||||||
|  | TEST_F(RepeatKey, StackMods) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_left(0, 0, 0, KC_LEFT); | ||||||
|  |     KeymapKey  key_shift(0, 1, 0, KC_LSFT); | ||||||
|  |     KeymapKey  key_ctrl(0, 2, 0, KC_LCTL); | ||||||
|  |     KeymapKey  key_repeat(0, 3, 0, QK_REP); | ||||||
|  |     set_keymap({key_left, key_shift, key_ctrl, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only mods.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LCTL), | ||||||
|  |                 KeyboardReport(KC_LSFT), | ||||||
|  |                 KeyboardReport(KC_LCTL, KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  | 
 | ||||||
|  |     { // Expect: "Ctrl+Left, Ctrl+Shift+Left".
 | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LEFT)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LEFT)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     key_ctrl.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_left); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     key_ctrl.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_LEFT); | ||||||
|  |     EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL)); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     key_shift.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  |     key_shift.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL)); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_left); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Types: "S(KC_1), Repeat, Ctrl+Repeat, Ctrl+Repeat, Repeat, KC_2".
 | ||||||
|  | TEST_F(RepeatKey, ShiftedKeycode) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_exlm(0, 0, 0, S(KC_1)); | ||||||
|  |     KeymapKey  key_2(0, 1, 0, KC_2); | ||||||
|  |     KeymapKey  key_ctrl(0, 2, 0, KC_LCTL); | ||||||
|  |     KeymapKey  key_repeat(0, 3, 0, QK_REP); | ||||||
|  |     set_keymap({key_exlm, key_2, key_ctrl, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only mods.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LCTL), | ||||||
|  |                 KeyboardReport(KC_LSFT), | ||||||
|  |                 KeyboardReport(KC_LCTL, KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  | 
 | ||||||
|  |     { // Expect: "Shift+1, Shift+1, Ctrl+Shift+1, Ctrl+Shift+1, Shift+1, 2".
 | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_LSFT, KC_1)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LSFT, KC_1)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_1)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_1)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LSFT, KC_1)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_2)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tap_key(key_exlm); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), S(KC_1)); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     key_ctrl.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  |     key_ctrl.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_2); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests Repeat Key with a one-shot Shift, types
 | ||||||
|  | // "A, OSM(MOD_LSFT), Repeat, Repeat".
 | ||||||
|  | TEST_F(RepeatKey, WithOneShotShift) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_oneshot_shift(0, 1, 0, OSM(MOD_LSFT)); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_oneshot_shift, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only KC_RALT.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  |     ExpectString(driver, "aAa"); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_a, key_oneshot_shift, key_repeat, key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests Repeat Key with a mod-tap key, types
 | ||||||
|  | // "A, Repeat, Repeat, A(down), Repeat, Repeat, A(up), Repeat".
 | ||||||
|  | TEST_F(RepeatKey, ModTap) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_mt_a(0, 0, 0, LSFT_T(KC_A)); | ||||||
|  |     KeymapKey  key_repeat(0, 1, 0, QK_REP); | ||||||
|  |     set_keymap({key_mt_a, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only KC_LSFT.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  |     ExpectString(driver, "aaaAAa"); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_mt_a); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), LSFT_T(KC_A)); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  |     key_mt_a.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_repeat, TAPPING_TERM + 1); | ||||||
|  |     tap_key(key_repeat); | ||||||
|  |     key_mt_a.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests with Auto Shift. When repeating an autoshiftable key, it does not
 | ||||||
|  | // matter how long the original key was held, rather, quickly tapping vs.
 | ||||||
|  | // long-pressing the Repeat Key determines whether the shifted key is repeated.
 | ||||||
|  | //
 | ||||||
|  | // The test does the following, which should produce "aaABbB":
 | ||||||
|  | // 1. Tap KC_A quickly.
 | ||||||
|  | // 2. Tap Repeat Key quickly.
 | ||||||
|  | // 3. Long-press Repeat Key.
 | ||||||
|  | // 4. Long-press KC_B.
 | ||||||
|  | // 5. Tap Repeat Key quickly.
 | ||||||
|  | // 6. Long-press Repeat Key.
 | ||||||
|  | TEST_F(RepeatKey, AutoShift) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_b(0, 1, 0, KC_B); | ||||||
|  |     KeymapKey  key_repeat(0, 2, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_b, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     autoshift_enable(); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only KC_LSFT.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  |     ExpectString(driver, "aaABbB"); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_a); // Tap A quickly.
 | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A); | ||||||
|  |     EXPECT_EQ(get_last_mods(), 0); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  |     tap_key(key_repeat, AUTO_SHIFT_TIMEOUT + 1); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_b, AUTO_SHIFT_TIMEOUT + 1); // Long press B.
 | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_B); | ||||||
|  |     EXPECT_EQ(get_last_mods(), 0); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  |     tap_key(key_repeat, AUTO_SHIFT_TIMEOUT + 1); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Defines `remember_last_key_user()` to forget the Shift mod and types:
 | ||||||
|  | // "Ctrl+A, Repeat, Shift+A, Repeat, Shift+Repeat".
 | ||||||
|  | TEST_F(RepeatKey, FilterRememberedMods) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_a(0, 0, 0, KC_A); | ||||||
|  |     KeymapKey  key_ctrl(0, 1, 0, KC_LCTL); | ||||||
|  |     KeymapKey  key_shift(0, 2, 0, KC_LSFT); | ||||||
|  |     KeymapKey  key_repeat(0, 3, 0, QK_REP); | ||||||
|  |     set_keymap({key_a, key_ctrl, key_shift, key_repeat}); | ||||||
|  | 
 | ||||||
|  |     remember_last_key_user_fun = [](uint16_t keycode, keyrecord_t* record, uint8_t* remembered_mods) { | ||||||
|  |         *remembered_mods &= ~MOD_MASK_SHIFT; | ||||||
|  |         return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only mods.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LCTL), | ||||||
|  |                 KeyboardReport(KC_LSFT), | ||||||
|  |                 KeyboardReport(KC_LCTL, KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  | 
 | ||||||
|  |     { // Expect: "Ctrl+A, Ctrl+A, Shift+A, A, Shift+A".
 | ||||||
|  |         InSequence seq; | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_A)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LCTL, KC_A)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LSFT, KC_A)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_A)); | ||||||
|  |         EXPECT_REPORT(driver, (KC_LSFT, KC_A)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     key_ctrl.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_a); | ||||||
|  | 
 | ||||||
|  |     EXPECT_EQ(get_last_mods(), MOD_BIT(KC_LCTL)); | ||||||
|  | 
 | ||||||
|  |     key_ctrl.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  |     key_shift.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_a); | ||||||
|  | 
 | ||||||
|  |     EXPECT_EQ(get_last_mods(), 0); // Shift should be forgotten.
 | ||||||
|  | 
 | ||||||
|  |     key_shift.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_repeat); | ||||||
|  | 
 | ||||||
|  |     key_shift.press(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     tap_key(key_repeat); | ||||||
|  |     key_shift.release(); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests set_last_keycode() and set_last_mods().
 | ||||||
|  | TEST_F(RepeatKey, SetRepeatKeyKeycode) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_repeat(0, 0, 0, QK_REP); | ||||||
|  |     set_keymap({key_repeat}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of reports with no keys or only KC_LSFT.
 | ||||||
|  |     // clang-format off
 | ||||||
|  |     EXPECT_CALL(driver, send_keyboard_mock(AnyOf( | ||||||
|  |                 KeyboardReport(), | ||||||
|  |                 KeyboardReport(KC_LSFT)))) | ||||||
|  |         .Times(AnyNumber()); | ||||||
|  |     // clang-format on
 | ||||||
|  |     ExpectString(driver, "aaBB"); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_A); | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_A); | ||||||
|  | 
 | ||||||
|  |     for (int n = 1; n <= 2; ++n) { // Tap the Repeat Key twice.
 | ||||||
|  |         // When Repeat is pressed, process_record_user() should be called with a
 | ||||||
|  |         // press event with keycode == KC_A and repeat_key_count() == n.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(true, KC_A, n); | ||||||
|  |         key_repeat.press(); // Press the Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |         // Expect the corresponding release event.
 | ||||||
|  |         ExpectProcessRecordUserCalledWith(false, KC_A, n); | ||||||
|  |         key_repeat.release(); // Release the Repeat Key.
 | ||||||
|  |         run_one_scan_loop(); | ||||||
|  |         EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     process_record_user_fun = process_record_user_default; | ||||||
|  |     set_last_keycode(KC_B); | ||||||
|  |     set_last_mods(MOD_BIT(KC_LSFT)); | ||||||
|  | 
 | ||||||
|  |     tap_keys(key_repeat, key_repeat); | ||||||
|  | 
 | ||||||
|  |     set_last_keycode(KC_NO); | ||||||
|  |     tap_keys(key_repeat, key_repeat); // Has no effect.
 | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Tests the `repeat_key_invoke()` function.
 | ||||||
|  | TEST_F(RepeatKey, RepeatKeyInvoke) { | ||||||
|  |     TestDriver driver; | ||||||
|  |     KeymapKey  key_s(0, 0, 0, KC_S); | ||||||
|  |     set_keymap({key_s}); | ||||||
|  | 
 | ||||||
|  |     // Allow any number of empty reports.
 | ||||||
|  |     EXPECT_EMPTY_REPORT(driver).Times(AnyNumber()); | ||||||
|  |     ExpectString(driver, "ss"); | ||||||
|  | 
 | ||||||
|  |     tap_key(key_s); | ||||||
|  | 
 | ||||||
|  |     EXPECT_KEYCODE_EQ(get_last_keycode(), KC_S); | ||||||
|  | 
 | ||||||
|  |     // Calling repeat_key_invoke() should result in process_record_user()
 | ||||||
|  |     // getting a press event with keycode KC_S.
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(true, KC_S, 1); | ||||||
|  |     keyevent_t event; | ||||||
|  |     event.key     = {0, 0}; | ||||||
|  |     event.pressed = true; | ||||||
|  |     event.time    = timer_read(); | ||||||
|  |     event.type    = KEY_EVENT; | ||||||
|  |     repeat_key_invoke(&event); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     // Make the release event.
 | ||||||
|  |     ExpectProcessRecordUserCalledWith(false, KC_S, 1); | ||||||
|  |     event.pressed = false; | ||||||
|  |     event.time    = timer_read(); | ||||||
|  |     repeat_key_invoke(&event); | ||||||
|  |     run_one_scan_loop(); | ||||||
|  |     EXPECT_TRUE(process_record_user_was_called_); | ||||||
|  | 
 | ||||||
|  |     testing::Mock::VerifyAndClearExpectations(&driver); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace
 | ||||||
| @ -663,6 +663,8 @@ std::map<uint16_t, std::string> KEYCODE_ID_TABLE = { | |||||||
|     {QK_AUTOCORRECT_TOGGLE, "QK_AUTOCORRECT_TOGGLE"}, |     {QK_AUTOCORRECT_TOGGLE, "QK_AUTOCORRECT_TOGGLE"}, | ||||||
|     {QK_TRI_LAYER_LOWER, "QK_TRI_LAYER_LOWER"}, |     {QK_TRI_LAYER_LOWER, "QK_TRI_LAYER_LOWER"}, | ||||||
|     {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"}, |     {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"}, | ||||||
|  |     {QK_REPEAT_KEY, "QK_REPEAT_KEY"}, | ||||||
|  |     {QK_ALT_REPEAT_KEY, "QK_ALT_REPEAT_KEY"}, | ||||||
|     {QK_KB_0, "QK_KB_0"}, |     {QK_KB_0, "QK_KB_0"}, | ||||||
|     {QK_KB_1, "QK_KB_1"}, |     {QK_KB_1, "QK_KB_1"}, | ||||||
|     {QK_KB_2, "QK_KB_2"}, |     {QK_KB_2, "QK_KB_2"}, | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ | |||||||
| #include <stdint.h> | #include <stdint.h> | ||||||
| #include "host.h" | #include "host.h" | ||||||
| #include "keyboard_report_util.hpp" | #include "keyboard_report_util.hpp" | ||||||
|  | #include "keycode_util.hpp" | ||||||
| #include "test_logger.hpp" | #include "test_logger.hpp" | ||||||
| 
 | 
 | ||||||
| class TestDriver { | class TestDriver { | ||||||
| @ -98,6 +99,17 @@ class TestDriver { | |||||||
|  */ |  */ | ||||||
| #define EXPECT_NO_REPORT(driver) EXPECT_ANY_REPORT(driver).Times(0) | #define EXPECT_NO_REPORT(driver) EXPECT_ANY_REPORT(driver).Times(0) | ||||||
| 
 | 
 | ||||||
|  | /** @brief Tests whether keycode `actual` is equal to `expected`. */ | ||||||
|  | #define EXPECT_KEYCODE_EQ(actual, expected) EXPECT_THAT((actual), KeycodeEq((expected))) | ||||||
|  | 
 | ||||||
|  | MATCHER_P(KeycodeEq, expected_keycode, "is equal to " + testing::PrintToString(expected_keycode) + ", keycode " + get_keycode_identifier_or_default(expected_keycode)) { | ||||||
|  |     if (arg == expected_keycode) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |     *result_listener << "keycode " << get_keycode_identifier_or_default(arg); | ||||||
|  |     return false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /**
 | /**
 | ||||||
|  * @brief Verify and clear all gmock expectations that have been setup until |  * @brief Verify and clear all gmock expectations that have been setup until | ||||||
|  * this point. |  * this point. | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user