summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPascal Getreuer <50221757+getreuer@users.noreply.github.com>2022-05-13 23:00:32 -0700
committerGitHub <noreply@github.com>2022-05-14 16:00:32 +1000
commit68b16bba68d79c63bbd649a5be58756e937c96da (patch)
treebe0d3d672c13c080f4da58687b5b1a261f465911
parent90eef4cd153cdc1b00d973e6abb029828f656294 (diff)
[Core] Add Caps Word feature to core (#16588)
Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> Co-authored-by: Drashna Jaelre <drashna@live.com>
-rw-r--r--builddefs/generic_features.mk1
-rw-r--r--builddefs/show_options.mk3
-rw-r--r--data/mappings/info_config.json3
-rw-r--r--data/mappings/info_rules.json1
-rw-r--r--data/schemas/keyboard.jsonschema10
-rw-r--r--docs/_summary.md1
-rw-r--r--docs/feature_caps_word.md156
-rw-r--r--docs/keycodes.md8
-rw-r--r--quantum/caps_word.c80
-rw-r--r--quantum/caps_word.h43
-rw-r--r--quantum/keyboard.c7
-rw-r--r--quantum/process_keycode/process_caps_word.c160
-rw-r--r--quantum/process_keycode/process_caps_word.h37
-rw-r--r--quantum/quantum.c3
-rw-r--r--quantum/quantum.h5
-rw-r--r--quantum/quantum_keycodes.h3
-rw-r--r--tests/caps_word/config.h21
-rw-r--r--tests/caps_word/test.mk19
-rw-r--r--tests/caps_word/test_caps_word.cpp423
19 files changed, 983 insertions, 1 deletions
diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
index 0475a2ff09..c3f1ec0f72 100644
--- a/builddefs/generic_features.mk
+++ b/builddefs/generic_features.mk
@@ -17,6 +17,7 @@ SPACE_CADET_ENABLE ?= yes
17GRAVE_ESC_ENABLE ?= yes 17GRAVE_ESC_ENABLE ?= yes
18 18
19GENERIC_FEATURES = \ 19GENERIC_FEATURES = \
20 CAPS_WORD \
20 COMBO \ 21 COMBO \
21 COMMAND \ 22 COMMAND \
22 DEFERRED_EXEC \ 23 DEFERRED_EXEC \
diff --git a/builddefs/show_options.mk b/builddefs/show_options.mk
index 16b69ef0ea..f67d009191 100644
--- a/builddefs/show_options.mk
+++ b/builddefs/show_options.mk
@@ -81,7 +81,8 @@ OTHER_OPTION_NAMES = \
81 RGBLIGHT_FULL_POWER \ 81 RGBLIGHT_FULL_POWER \
82 LTO_ENABLE \ 82 LTO_ENABLE \
83 PROGRAMMABLE_BUTTON_ENABLE \ 83 PROGRAMMABLE_BUTTON_ENABLE \
84 SECURE_ENABLE 84 SECURE_ENABLE \
85 CAPS_WORD_ENABLE
85 86
86define NAME_ECHO 87define NAME_ECHO
87 @printf " %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)" 88 @printf " %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)"
diff --git a/data/mappings/info_config.json b/data/mappings/info_config.json
index 02ad3226c2..d9f96b5892 100644
--- a/data/mappings/info_config.json
+++ b/data/mappings/info_config.json
@@ -11,6 +11,8 @@
11 "BACKLIGHT_BREATHING": {"info_key": "backlight.breathing", "value_type": "bool"}, 11 "BACKLIGHT_BREATHING": {"info_key": "backlight.breathing", "value_type": "bool"},
12 "BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"}, 12 "BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"},
13 "BACKLIGHT_PIN": {"info_key": "backlight.pin"}, 13 "BACKLIGHT_PIN": {"info_key": "backlight.pin"},
14 "BOTH_SHIFTS_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.both_shifts_turns_on", "value_type": "bool"},
15 "CAPS_WORD_IDLE_TIMEOUT": {"info_key": "caps_word.idle_timeout", "value_type": "int"},
14 "COMBO_COUNT": {"info_key": "combo.count", "value_type": "int"}, 16 "COMBO_COUNT": {"info_key": "combo.count", "value_type": "int"},
15 "COMBO_TERM": {"info_key": "combo.term", "value_type": "int"}, 17 "COMBO_TERM": {"info_key": "combo.term", "value_type": "int"},
16 "DEBOUNCE": {"info_key": "debounce", "value_type": "int"}, 18 "DEBOUNCE": {"info_key": "debounce", "value_type": "int"},
@@ -19,6 +21,7 @@
19 #"DEVICE_VER": {"info_key": "usb.device_version", "value_type": "bcd_version"}, 21 #"DEVICE_VER": {"info_key": "usb.device_version", "value_type": "bcd_version"},
20 "DESCRIPTION": {"info_key": "keyboard_folder", "value_type": "str", "to_json": false}, 22 "DESCRIPTION": {"info_key": "keyboard_folder", "value_type": "str", "to_json": false},
21 "DIODE_DIRECTION": {"info_key": "diode_direction"}, 23 "DIODE_DIRECTION": {"info_key": "diode_direction"},
24 "DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.double_tap_shift_turns_on", "value_type": "bool"},
22 "FORCE_NKRO": {"info_key": "usb.force_nkro", "value_type": "bool"}, 25 "FORCE_NKRO": {"info_key": "usb.force_nkro", "value_type": "bool"},
23 "DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"}, 26 "DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"},
24 "DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"}, 27 "DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"},
diff --git a/data/mappings/info_rules.json b/data/mappings/info_rules.json
index 4b0fde5629..a8b39afbd1 100644
--- a/data/mappings/info_rules.json
+++ b/data/mappings/info_rules.json
@@ -10,6 +10,7 @@
10 "BOARD": {"info_key": "board"}, 10 "BOARD": {"info_key": "board"},
11 "BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false}, 11 "BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false},
12 "BLUETOOTH": {"info_key": "bluetooth.driver"}, 12 "BLUETOOTH": {"info_key": "bluetooth.driver"},
13 "CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"},
13 "FIRMWARE_FORMAT": {"info_key": "build.firmware_format"}, 14 "FIRMWARE_FORMAT": {"info_key": "build.firmware_format"},
14 "KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"}, 15 "KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"},
15 "MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"}, 16 "MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"},
diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
index 6b1e543e48..aaf6f887d5 100644
--- a/data/schemas/keyboard.jsonschema
+++ b/data/schemas/keyboard.jsonschema
@@ -92,6 +92,16 @@
92 "enum": ["COL2ROW", "ROW2COL"] 92 "enum": ["COL2ROW", "ROW2COL"]
93 }, 93 },
94 "debounce": {"$ref": "qmk.definitions.v1#/unsigned_int"}, 94 "debounce": {"$ref": "qmk.definitions.v1#/unsigned_int"},
95 "caps_word": {
96 "type": "object",
97 "additionalProperties": false,
98 "properties": {
99 "enabled": {"type": "boolean"},
100 "both_shifts_turns_on": {"type": "boolean"},
101 "double_tap_shift_turns_on": {"type": "boolean"},
102 "idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"},
103 },
104 },
95 "combo": { 105 "combo": {
96 "type": "object", 106 "type": "object",
97 "properties": { 107 "properties": {
diff --git a/docs/_summary.md b/docs/_summary.md
index 786685eba4..fed75196b4 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -75,6 +75,7 @@
75 75
76 * Software Features 76 * Software Features
77 * [Auto Shift](feature_auto_shift.md) 77 * [Auto Shift](feature_auto_shift.md)
78 * [Caps Word](feature_caps_word.md)
78 * [Combos](feature_combo.md) 79 * [Combos](feature_combo.md)
79 * [Debounce API](feature_debounce_type.md) 80 * [Debounce API](feature_debounce_type.md)
80 * [Key Lock](feature_key_lock.md) 81 * [Key Lock](feature_key_lock.md)
diff --git a/docs/feature_caps_word.md b/docs/feature_caps_word.md
new file mode 100644
index 0000000000..0b71119917
--- /dev/null
+++ b/docs/feature_caps_word.md
@@ -0,0 +1,156 @@
1# Caps Word
2
3It is often useful to type a single word in all capitals, for instance
4abbreviations like "QMK", or in code, identifiers like `KC_SPC`. "Caps Word" is
5a modern alternative to Caps Lock:
6
7* Letters are capitalized while active, and Caps Word automatically disables
8 itself at the end of the word. That is, it stops by default once a space or
9 any key other than `a`--`z`, `0`--`9`, `-`, `_`, delete, or backspace is
10 pressed. Caps Word also disables itself if the keyboard is idle for 5 seconds.
11 This is configurable, see below.
12
13* To avoid requiring a dedicated key for Caps Word, there is an option
14 (`BOTH_SHIFTS_TURNS_ON_CAPS_WORD`) to activate Caps Word by simultaneously
15 pressing both shift keys. See below for other options.
16
17* The implementation does not use the Caps Lock (`KC_CAPS`) keycode. Caps Word
18 works even if you're remapping Caps Lock at the OS level to Ctrl or something
19 else, as Emacs and Vim users often do.
20
21
22## How do I enable Caps Word :id=how-do-i-enable-caps-word
23
24In your `rules.mk`, add:
25
26```make
27CAPS_WORD_ENABLE = yes
28```
29
30Next, use one the following methods to activate Caps Word:
31
32* **Activate by pressing a key**: Use the `CAPS_WORD` keycode (short
33 alias `CAPSWRD`) in your keymap.
34
35* **Activate by pressing Left Shift + Right Shift**: Add `#define
36 BOTH_SHIFTS_TURNS_ON_CAPS_WORD` to config.h. You may also need to disable or
37 reconfigure Command, details below. Then, simultaneously pressing both left
38 and right shifts turns on Caps Word. This method works with the plain
39 `KC_LSFT` and `KC_RSFT` keycodes as well as one-shot shifts and Space Cadet
40 shifts. If your shift keys are mod-taps, hold both shift mod-tap keys until
41 the tapping term, then release them.
42
43* **Activate by double tapping Left Shift**: Add `#define
44 DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD` config.h. Then, double tapping Left Shift
45 turns on Caps Word. This method works with `KC_LSFT` or one-shot Left Shift
46 `OSM(MOD_LSFT)`. To count as a double tap, the maximum time in milliseconds
47 between taps is `TAPPING_TERM`, or if using `TAPPING_TERM_PER_KEY`, the time
48 returned by `get_tapping_term()` for the shift keycode being tapped.
49
50* **Custom activation**: You can activate Caps Word from code by calling
51 `caps_word_on()`. This may be used to activate Caps Word through [a
52 combo](feature_combo.md) or [tap dance](feature_tap_dance.md) or any means
53 you like.
54
55### Troubleshooting: Command :id=troubleshooting-command
56
57When using `BOTH_SHIFTS_TURNS_ON_CAPS_WORD`, you might see a compile message
58**"BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same
59time, since both use the Left Shift + Right Shift key combination."**
60
61Many keyboards enable the [Command feature](feature_command.md), which by
62default is also activated using the Left Shift + Right Shift key combination. To
63fix this conflict, please disable Command by adding in rules.mk:
64
65```make
66COMMAND_ENABLE = no
67```
68
69Or configure Command to use another key combination like Left Ctrl + Right Ctrl
70by defining `IS_COMMAND()` in config.h:
71
72```c
73// Activate Command with Left Ctrl + Right Ctrl.
74#define IS_COMMAND() (get_mods() == MOD_MASK_CTRL)
75```
76
77
78## Customizing Caps Word :id=customizing-caps-word
79
80### Idle timeout :id=idle-timeout
81
82Caps Word turns off automatically if no keys are pressed for
83`CAPS_WORD_IDLE_TIMEOUT` milliseconds. The default is 5000 (5 seconds).
84Configure the timeout duration in config.h, for instance
85
86```c
87#define CAPS_WORD_IDLE_TIMEOUT 3000 // 3 seconds.
88```
89
90Setting `CAPS_WORD_IDLE_TIMEOUT` to 0 configures Caps Word to never time out.
91Caps Word then remains active indefinitely until a word breaking key is pressed.
92
93
94### Functions :id=functions
95
96Functions to manipulate Caps Word:
97
98| Function | Description |
99|-------------------------|------------------------------------------------|
100| `caps_word_on()` | Turns Caps Word on. |
101| `caps_word_off()` | Turns Caps Word off. |
102| `caps_word_toggle()` | Toggles Caps Word. |
103| `is_caps_word_on()` | Returns true if Caps Word is currently on. |
104
105
106### Configure which keys are "word breaking" :id=configure-which-keys-are-word-breaking
107
108You can define the `caps_word_press_user(uint16_t keycode)` callback to
109configure which keys should be shifted and which keys are considered "word
110breaking" and stop Caps Word.
111
112The callback is called on every key press while Caps Word is active. When the
113key should be shifted (that is, a letter key), the callback should call
114`add_weak_mods(MOD_BIT(KC_LSFT))` to shift the key. Returning true continues the
115current "word," while returning false is "word breaking" and deactivates Caps
116Word. The default callback is
117
118```c
119bool caps_word_press_user(uint16_t keycode) {
120 switch (keycode) {
121 // Keycodes that continue Caps Word, with shift applied.
122 case KC_A ... KC_Z:
123 case KC_MINS:
124 add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
125 return true;
126
127 // Keycodes that continue Caps Word, without shifting.
128 case KC_1 ... KC_0:
129 case KC_BSPC:
130 case KC_DEL:
131 case KC_UNDS:
132 return true;
133
134 default:
135 return false; // Deactivate Caps Word.
136 }
137}
138```
139
140
141### Representing Caps Word state :id=representing-caps-word-state
142
143Define `caps_word_set_user(bool active)` to get callbacks when Caps Word turns
144on or off. This is useful to represent the current Caps Word state, e.g. by
145setting an LED or playing a sound. In your keymap, define
146
147```c
148void caps_word_set_user(bool active) {
149 if (active) {
150 // Do something when Caps Word activates.
151 } else {
152 // Do something when Caps Word deactivates.
153 }
154}
155```
156
diff --git a/docs/keycodes.md b/docs/keycodes.md
index 10652bdb2f..bd5af32dd3 100644
--- a/docs/keycodes.md
+++ b/docs/keycodes.md
@@ -269,6 +269,14 @@ See also: [Bluetooth](feature_bluetooth.md)
269|`OUT_USB` |USB only | 269|`OUT_USB` |USB only |
270|`OUT_BT` |Bluetooth only | 270|`OUT_BT` |Bluetooth only |
271 271
272## Caps Word :id=caps-word
273
274See also: [Caps Word](feature_caps_word.md)
275
276|Key |Aliases |Description |
277|-----------|---------|------------------------------|
278|`CAPS_WORD`|`CAPSWRD`|Toggles Caps Word |
279
272## Dynamic Macros :id=dynamic-macros 280## Dynamic Macros :id=dynamic-macros
273 281
274See also: [Dynamic Macros](feature_dynamic_macros.md) 282See also: [Dynamic Macros](feature_dynamic_macros.md)
diff --git a/quantum/caps_word.c b/quantum/caps_word.c
new file mode 100644
index 0000000000..5b83659f28
--- /dev/null
+++ b/quantum/caps_word.c
@@ -0,0 +1,80 @@
1// Copyright 2021-2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "caps_word.h"
16
17/** @brief True when Caps Word is active. */
18static bool caps_word_active = false;
19
20#if CAPS_WORD_IDLE_TIMEOUT > 0
21// Constrain timeout to a sensible range. With 16-bit timers, the longest
22// timeout possible is 32768 ms, rounded here to 30000 ms = half a minute.
23# if CAPS_WORD_IDLE_TIMEOUT < 100 || CAPS_WORD_IDLE_TIMEOUT > 30000
24# error "CAPS_WORD_IDLE_TIMEOUT must be between 100 and 30000 ms"
25# endif
26
27/** @brief Deadline for idle timeout. */
28static uint16_t idle_timer = 0;
29
30void caps_word_task(void) {
31 if (caps_word_active && timer_expired(timer_read(), idle_timer)) {
32 caps_word_off();
33 }
34}
35
36void caps_word_reset_idle_timer(void) {
37 idle_timer = timer_read() + CAPS_WORD_IDLE_TIMEOUT;
38}
39#endif // CAPS_WORD_IDLE_TIMEOUT > 0
40
41void caps_word_on(void) {
42 if (caps_word_active) {
43 return;
44 }
45
46 clear_mods();
47#ifndef NO_ACTION_ONESHOT
48 clear_oneshot_mods();
49#endif // NO_ACTION_ONESHOT
50#if CAPS_WORD_IDLE_TIMEOUT > 0
51 caps_word_reset_idle_timer();
52#endif // CAPS_WORD_IDLE_TIMEOUT > 0
53
54 caps_word_active = true;
55 caps_word_set_user(true);
56}
57
58void caps_word_off(void) {
59 if (!caps_word_active) {
60 return;
61 }
62
63 unregister_weak_mods(MOD_MASK_SHIFT); // Make sure weak shift is off.
64 caps_word_active = false;
65 caps_word_set_user(false);
66}
67
68void caps_word_toggle(void) {
69 if (caps_word_active) {
70 caps_word_off();
71 } else {
72 caps_word_on();
73 }
74}
75
76bool is_caps_word_on(void) {
77 return caps_word_active;
78}
79
80__attribute__((weak)) void caps_word_set_user(bool active) {}
diff --git a/quantum/caps_word.h b/quantum/caps_word.h
new file mode 100644
index 0000000000..b83f73371e
--- /dev/null
+++ b/quantum/caps_word.h
@@ -0,0 +1,43 @@
1// Copyright 2021-2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#pragma once
16
17#include "quantum.h"
18
19#ifndef CAPS_WORD_IDLE_TIMEOUT
20# define CAPS_WORD_IDLE_TIMEOUT 5000 // Default timeout of 5 seconds.
21#endif // CAPS_WORD_IDLE_TIMEOUT
22
23#if CAPS_WORD_IDLE_TIMEOUT > 0
24/** @brief Matrix scan task for Caps Word feature */
25void caps_word_task(void);
26
27/** @brief Resets timer for Caps Word idle timeout. */
28void caps_word_reset_idle_timer(void);
29#else
30static inline void caps_word_task(void) {}
31#endif // CAPS_WORD_IDLE_TIMEOUT > 0
32
33void caps_word_on(void); /**< Activates Caps Word. */
34void caps_word_off(void); /**< Deactivates Caps Word. */
35void caps_word_toggle(void); /**< Toggles Caps Word. */
36bool is_caps_word_on(void); /**< Gets whether currently active. */
37
38/**
39 * @brief Caps Word set callback.
40 *
41 * @param active True if Caps Word is active, false otherwise
42 */
43void caps_word_set_user(bool active);
diff --git a/quantum/keyboard.c b/quantum/keyboard.c
index 1dccd48e2f..a65f9d6d18 100644
--- a/quantum/keyboard.c
+++ b/quantum/keyboard.c
@@ -108,6 +108,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
108#ifdef BLUETOOTH_ENABLE 108#ifdef BLUETOOTH_ENABLE
109# include "outputselect.h" 109# include "outputselect.h"
110#endif 110#endif
111#ifdef CAPS_WORD_ENABLE
112# include "caps_word.h"
113#endif
111 114
112static uint32_t last_input_modification_time = 0; 115static uint32_t last_input_modification_time = 0;
113uint32_t last_input_activity_time(void) { 116uint32_t last_input_activity_time(void) {
@@ -549,6 +552,10 @@ void quantum_task(void) {
549 autoshift_matrix_scan(); 552 autoshift_matrix_scan();
550#endif 553#endif
551 554
555#ifdef CAPS_WORD_ENABLE
556 caps_word_task();
557#endif
558
552#ifdef SECURE_ENABLE 559#ifdef SECURE_ENABLE
553 secure_task(); 560 secure_task();
554#endif 561#endif
diff --git a/quantum/process_keycode/process_caps_word.c b/quantum/process_keycode/process_caps_word.c
new file mode 100644
index 0000000000..15238f04a1
--- /dev/null
+++ b/quantum/process_keycode/process_caps_word.c
@@ -0,0 +1,160 @@
1// Copyright 2021-2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "process_caps_word.h"
16
17bool process_caps_word(uint16_t keycode, keyrecord_t* record) {
18 if (keycode == CAPSWRD) { // Pressing CAPSWRD toggles Caps Word.
19 if (record->event.pressed) {
20 caps_word_toggle();
21 }
22 return false;
23 }
24
25#ifndef NO_ACTION_ONESHOT
26 const uint8_t mods = get_mods() | get_oneshot_mods();
27#else
28 const uint8_t mods = get_mods();
29#endif // NO_ACTION_ONESHOT
30
31 if (!is_caps_word_on()) {
32 // The following optionally turns on Caps Word by holding left and
33 // right shifts or by double tapping left shift. This way Caps Word
34 // may be used without needing a dedicated key and also without
35 // needing combos or tap dance.
36
37#ifdef BOTH_SHIFTS_TURNS_ON_CAPS_WORD
38 // Many keyboards enable the Command feature by default, which also
39 // uses left+right shift. It can be configured to use a different
40 // key combination by defining IS_COMMAND(). We make a non-fatal
41 // warning if Command is enabled but IS_COMMAND() is *not* defined.
42# if defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
43# pragma message "BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same time, since both use the Left Shift + Right Shift key combination. Please disable Command, or ensure that `IS_COMMAND` is not set to (get_mods() == MOD_MASK_SHIFT)."
44# else
45 if (mods == MOD_MASK_SHIFT
46# ifdef COMMAND_ENABLE
47 // Don't activate Caps Word at the same time as Command.
48 && !(IS_COMMAND())
49# endif // COMMAND_ENABLE
50 ) {
51 caps_word_on();
52 }
53# endif // defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
54#endif // BOTH_SHIFTS_TURNS_ON_CAPS_WORD
55
56#ifdef DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
57 // Double tapping left shift turns on Caps Word.
58 //
59 // NOTE: This works with KC_LSFT and one-shot left shift. It
60 // wouldn't make sense with mod-tap or Space Cadet shift since
61 // double tapping would of course trigger the tapping action.
62 if (record->event.pressed) {
63 static bool tapped = false;
64 static uint16_t timer = 0;
65 if (keycode == KC_LSFT || keycode == OSM(MOD_LSFT)) {
66 if (tapped && !timer_expired(record->event.time, timer)) {
67 // Left shift was double tapped, activate Caps Word.
68 caps_word_on();
69 }
70 tapped = true;
71 timer = record->event.time + GET_TAPPING_TERM(keycode, record);
72 } else {
73 tapped = false; // Reset when any other key is pressed.
74 }
75 }
76#endif // DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
77
78 return true;
79 }
80
81#if CAPS_WORD_IDLE_TIMEOUT > 0
82 caps_word_reset_idle_timer();
83#endif // CAPS_WORD_IDLE_TIMEOUT > 0
84
85 // From here on, we only take action on press events.
86 if (!record->event.pressed) {
87 return true;
88 }
89
90 if (!(mods & ~MOD_MASK_SHIFT)) {
91 switch (keycode) {
92 // Ignore MO, TO, TG, TT, and OSL layer switch keys.
93 case QK_MOMENTARY ... QK_MOMENTARY_MAX:
94 case QK_TO ... QK_TO_MAX:
95 case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
96 case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
97 case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
98 return true;
99
100#ifndef NO_ACTION_TAPPING
101 case QK_MOD_TAP ... QK_MOD_TAP_MAX:
102 if (record->tap.count == 0) {
103 // Deactivate if a mod becomes active through holding
104 // a mod-tap key.
105 caps_word_off();
106 return true;
107 }
108 keycode &= 0xff;
109 break;
110
111# ifndef NO_ACTION_LAYER
112 case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
113# endif // NO_ACTION_LAYER
114 if (record->tap.count == 0) {
115 return true;
116 }
117 keycode &= 0xff;
118 break;
119#endif // NO_ACTION_TAPPING
120
121#ifdef SWAP_HANDS_ENABLE
122 case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
123 if (keycode > 0x56F0 || record->tap.count == 0) {
124 return true;
125 }
126 keycode &= 0xff;
127 break;
128#endif // SWAP_HANDS_ENABLE
129 }
130
131 clear_weak_mods();
132 if (caps_word_press_user(keycode)) {
133 send_keyboard_report();
134 return true;
135 }
136 }
137
138 caps_word_off();
139 return true;
140}
141
142__attribute__((weak)) bool caps_word_press_user(uint16_t keycode) {
143 switch (keycode) {
144 // Keycodes that continue Caps Word, with shift applied.
145 case KC_A ... KC_Z:
146 case KC_MINS:
147 add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
148 return true;
149
150 // Keycodes that continue Caps Word, without shifting.
151 case KC_1 ... KC_0:
152 case KC_BSPC:
153 case KC_DEL:
154 case KC_UNDS:
155 return true;
156
157 default:
158 return false; // Deactivate Caps Word.
159 }
160}
diff --git a/quantum/process_keycode/process_caps_word.h b/quantum/process_keycode/process_caps_word.h
new file mode 100644
index 0000000000..f215bbc3a3
--- /dev/null
+++ b/quantum/process_keycode/process_caps_word.h
@@ -0,0 +1,37 @@
1// Copyright 2021-2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#pragma once
16
17#include "quantum.h"
18#include "caps_word.h"
19
20/**
21 * @brief Process handler for Caps Word feature.
22 *
23 * @param keycode Keycode registered by matrix press, per keymap
24 * @param record keyrecord_t structure
25 * @return true Continue processing keycodes, and send to host
26 * @return false Stop processing keycodes, and don't send to host
27 */
28bool process_caps_word(uint16_t keycode, keyrecord_t* record);
29
30/**
31 * @brief Weak function for user-level Caps Word press modification.
32 *
33 * @param keycode Keycode registered by matrix press, per keymap
34 * @return true Continue Caps Word
35 * @return false Stop Caps Word
36 */
37bool caps_word_press_user(uint16_t keycode);
diff --git a/quantum/quantum.c b/quantum/quantum.c
index c0e801a4bb..b54b46760c 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -307,6 +307,9 @@ bool process_record_quantum(keyrecord_t *record) {
307#ifdef TERMINAL_ENABLE 307#ifdef TERMINAL_ENABLE
308 process_terminal(keycode, record) && 308 process_terminal(keycode, record) &&
309#endif 309#endif
310#ifdef CAPS_WORD_ENABLE
311 process_caps_word(keycode, record) &&
312#endif
310#ifdef SPACE_CADET_ENABLE 313#ifdef SPACE_CADET_ENABLE
311 process_space_cadet(keycode, record) && 314 process_space_cadet(keycode, record) &&
312#endif 315#endif
diff --git a/quantum/quantum.h b/quantum/quantum.h
index 7829ec7e0c..92e1af1c40 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -233,6 +233,11 @@ extern layer_state_t layer_state;
233# include "pointing_device.h" 233# include "pointing_device.h"
234#endif 234#endif
235 235
236#ifdef CAPS_WORD_ENABLE
237# include "caps_word.h"
238# include "process_caps_word.h"
239#endif
240
236// For tri-layer 241// For tri-layer
237void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3); 242void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3);
238layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3); 243layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3);
diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h
index 5d5c4ed8c4..2f8ee2322e 100644
--- a/quantum/quantum_keycodes.h
+++ b/quantum/quantum_keycodes.h
@@ -602,6 +602,8 @@ enum quantum_keycodes {
602 SECURE_UNLOCK, 602 SECURE_UNLOCK,
603 SECURE_TOGGLE, 603 SECURE_TOGGLE,
604 604
605 CAPS_WORD,
606
605 // Start of custom keycode range for keyboards and keymaps - always leave at the end 607 // Start of custom keycode range for keyboards and keymaps - always leave at the end
606 SAFE_RANGE 608 SAFE_RANGE
607}; 609};
@@ -964,5 +966,6 @@ enum quantum_keycodes {
964#define PB_32 PROGRAMMABLE_BUTTON_32 966#define PB_32 PROGRAMMABLE_BUTTON_32
965#define PROGRAMMABLE_BUTTON_MIN PROGRAMMABLE_BUTTON_1 967#define PROGRAMMABLE_BUTTON_MIN PROGRAMMABLE_BUTTON_1
966#define PROGRAMMABLE_BUTTON_MAX PROGRAMMABLE_BUTTON_32 968#define PROGRAMMABLE_BUTTON_MAX PROGRAMMABLE_BUTTON_32
969#define CAPSWRD CAPS_WORD
967 970
968#include "quantum_keycodes_legacy.h" 971#include "quantum_keycodes_legacy.h"
diff --git a/tests/caps_word/config.h b/tests/caps_word/config.h
new file mode 100644
index 0000000000..0d5cebd778
--- /dev/null
+++ b/tests/caps_word/config.h
@@ -0,0 +1,21 @@
1// Copyright 2022 Google LLC
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 2 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16#pragma once
17
18#include "test_common.h"
19
20#define BOTH_SHIFTS_TURNS_ON_CAPS_WORD
21#define DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
diff --git a/tests/caps_word/test.mk b/tests/caps_word/test.mk
new file mode 100644
index 0000000000..2509b01858
--- /dev/null
+++ b/tests/caps_word/test.mk
@@ -0,0 +1,19 @@
1# Copyright 2022 Google LLC
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16CAPS_WORD_ENABLE = yes
17COMMAND_ENABLE = no
18SPACE_CADET_ENABLE = yes
19
diff --git a/tests/caps_word/test_caps_word.cpp b/tests/caps_word/test_caps_word.cpp
new file mode 100644
index 0000000000..bcc8c53326
--- /dev/null
+++ b/tests/caps_word/test_caps_word.cpp
@@ -0,0 +1,423 @@
1// Copyright 2022 Google LLC
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 2 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16#include "keyboard_report_util.hpp"
17#include "keycode.h"
18#include "test_common.hpp"
19#include "test_fixture.hpp"
20#include "test_keymap_key.hpp"
21
22using ::testing::_;
23using ::testing::AnyNumber;
24using ::testing::AnyOf;
25using ::testing::InSequence;
26using ::testing::TestParamInfo;
27
28class CapsWord : public TestFixture {
29 public:
30 void SetUp() override {
31 caps_word_off();
32 }
33
34 // Convenience function to tap `key`.
35 void TapKey(KeymapKey key) {
36 key.press();
37 run_one_scan_loop();
38 key.release();
39 run_one_scan_loop();
40 }
41
42 // Taps in order each key in `keys`.
43 template <typename... Ts>
44 void TapKeys(Ts... keys) {
45 for (KeymapKey key : {keys...}) {
46 TapKey(key);
47 }
48 }
49};
50
51// Tests caps_word_on(), _off(), and _toggle() functions.
52TEST_F(CapsWord, OnOffToggleFuns) {
53 TestDriver driver;
54
55 EXPECT_EQ(is_caps_word_on(), false);
56
57 caps_word_on();
58 EXPECT_EQ(is_caps_word_on(), true);
59 caps_word_on();
60 EXPECT_EQ(is_caps_word_on(), true);
61
62 caps_word_off();
63 EXPECT_EQ(is_caps_word_on(), false);
64 caps_word_off();
65 EXPECT_EQ(is_caps_word_on(), false);
66
67 caps_word_toggle();
68 EXPECT_EQ(is_caps_word_on(), true);
69 caps_word_toggle();
70 EXPECT_EQ(is_caps_word_on(), false);
71
72 testing::Mock::VerifyAndClearExpectations(&driver);
73}
74
75// Tests the default `caps_word_press_user()` function.
76TEST_F(CapsWord, DefaultCapsWordPressUserFun) {
77 // Spot check some keycodes that continue Caps Word, with shift applied.
78 for (uint16_t keycode : {KC_A, KC_B, KC_Z, KC_MINS}) {
79 SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
80 clear_weak_mods();
81 EXPECT_TRUE(caps_word_press_user(keycode));
82 EXPECT_EQ(get_weak_mods(), MOD_BIT(KC_LSFT));
83 }
84
85 // Some keycodes that continue Caps Word, without shifting.
86 for (uint16_t keycode : {KC_1, KC_9, KC_0, KC_BSPC, KC_DEL}) {
87 SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
88 clear_weak_mods();
89 EXPECT_TRUE(caps_word_press_user(keycode));
90 EXPECT_EQ(get_weak_mods(), 0);
91 }
92
93 // Some keycodes that turn off Caps Word.
94 for (uint16_t keycode : {KC_SPC, KC_DOT, KC_COMM, KC_TAB, KC_ESC, KC_ENT}) {
95 SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
96 EXPECT_FALSE(caps_word_press_user(keycode));
97 }
98}
99
100// Tests that `CAPSWRD` key toggles Caps Word.
101TEST_F(CapsWord, CapswrdKey) {
102 TestDriver driver;
103 KeymapKey key_capswrd(0, 0, 0, CAPSWRD);
104 set_keymap({key_capswrd});
105
106 // No keyboard reports should be sent.
107 EXPECT_CALL(driver, send_keyboard_mock(_)).Times(0);
108
109 TapKey(key_capswrd); // Tap the CAPSWRD key.
110 EXPECT_EQ(is_caps_word_on(), true);
111
112 TapKey(key_capswrd); // Tap the CAPSWRD key again.
113 EXPECT_EQ(is_caps_word_on(), false);
114
115 testing::Mock::VerifyAndClearExpectations(&driver);
116}
117
118// Tests that being idle for CAPS_WORD_IDLE_TIMEOUT turns off Caps Word.
119TEST_F(CapsWord, IdleTimeout) {
120 TestDriver driver;
121 KeymapKey key_a(0, 0, 0, KC_A);
122 set_keymap({key_a});
123
124 // Allow any number of reports with no keys or only KC_LSFT.
125 // clang-format off
126 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
127 KeyboardReport(),
128 KeyboardReport(KC_LSFT))))
129 .Times(AnyNumber());
130 // clang-format on
131
132 // Expect "Shift+A".
133 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
134
135 // Turn on Caps Word and tap "A".
136 caps_word_on();
137 TapKey(key_a);
138
139 testing::Mock::VerifyAndClearExpectations(&driver);
140
141 idle_for(CAPS_WORD_IDLE_TIMEOUT);
142 run_one_scan_loop();
143
144 // Caps Word should be off and mods should be clear.
145 EXPECT_EQ(is_caps_word_on(), false);
146 EXPECT_EQ(get_mods() | get_weak_mods(), 0);
147
148 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport())).Times(AnyNumber());
149 // Expect unshifted "A".
150 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
151 TapKey(key_a);
152
153 testing::Mock::VerifyAndClearExpectations(&driver);
154}
155
156// Tests that typing "A, 4, A, 4" produces "Shift+A, 4, Shift+A, 4".
157TEST_F(CapsWord, ShiftsLettersButNotDigits) {
158 TestDriver driver;
159 KeymapKey key_a(0, 0, 0, KC_A);
160 KeymapKey key_4(0, 1, 0, KC_4);
161 set_keymap({key_a, key_4});
162
163 // Allow any number of reports with no keys or only KC_LSFT.
164 // clang-format off
165 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
166 KeyboardReport(),
167 KeyboardReport(KC_LSFT))))
168 .Times(AnyNumber());
169 // clang-format on
170
171 { // Expect: "Shift+A, 4, Shift+A, 4".
172 InSequence s;
173 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
174 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
175 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
176 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
177 }
178
179 // Turn on Caps Word and tap "A, 4, A, 4".
180 caps_word_on();
181 TapKeys(key_a, key_4, key_a, key_4);
182
183 testing::Mock::VerifyAndClearExpectations(&driver);
184}
185
186// Tests that typing "A, Space, A" produces "Shift+A, Space, A".
187TEST_F(CapsWord, SpaceTurnsOffCapsWord) {
188 TestDriver driver;
189 KeymapKey key_a(0, 0, 0, KC_A);
190 KeymapKey key_spc(0, 1, 0, KC_SPC);
191 set_keymap({key_a, key_spc});
192
193 // Allow any number of reports with no keys or only KC_LSFT.
194 // clang-format off
195 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
196 KeyboardReport(),
197 KeyboardReport(KC_LSFT))))
198 .Times(AnyNumber());
199 // clang-format on
200
201 { // Expect: "Shift+A, Space, A".
202 InSequence seq;
203 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
204 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_SPC)));
205 EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
206 }
207
208 // Turn on Caps Word and tap "A, Space, A".
209 caps_word_on();
210 TapKeys(key_a, key_spc, key_a);
211
212 testing::Mock::VerifyAndClearExpectations(&driver);
213}
214
215struct CapsWordBothShiftsParams {
216 std::string name;
217 uint16_t left_shift_keycode;
218 uint16_t right_shift_keycode;
219
220 static const std::string& GetName(const TestParamInfo<CapsWordBothShiftsParams>& info) {
221 return info.param.name;
222 }
223};
224
225// Tests the BOTH_SHIFTS_TURNS_ON_CAPS_WORD method to turn on Caps Word.
226class CapsWordBothShifts : public ::testing::WithParamInterface<CapsWordBothShiftsParams>, public CapsWord {};
227
228// Pressing shifts as "Left down, Right down, Left up, Right up".
229TEST_P(CapsWordBothShifts, PressLRLR) {
230 TestDriver driver;
231 KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
232 KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
233 set_keymap({left_shift, right_shift});
234
235 // clang-format off
236 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
237 KeyboardReport(),
238 KeyboardReport(KC_LSFT),
239 KeyboardReport(KC_RSFT),
240 KeyboardReport(KC_LSFT, KC_RSFT))))
241 .Times(AnyNumber());
242 // clang-format on
243
244 EXPECT_EQ(is_caps_word_on(), false);
245
246 left_shift.press(); // Press both shifts.
247 run_one_scan_loop();
248 right_shift.press();
249
250 // For mod-tap and Space Cadet keys, wait for the tapping term.
251 if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
252 idle_for(TAPPING_TERM);
253 }
254
255 run_one_scan_loop();
256 left_shift.release(); // Release both.
257 run_one_scan_loop();
258 right_shift.release();
259 run_one_scan_loop();
260
261 EXPECT_EQ(is_caps_word_on(), true);
262
263 testing::Mock::VerifyAndClearExpectations(&driver);
264}
265
266// Pressing shifts as "Left down, Right down, Right up, Left up".
267TEST_P(CapsWordBothShifts, PressLRRL) {
268 TestDriver driver;
269 KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
270 KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
271 set_keymap({left_shift, right_shift});
272
273 // clang-format off
274 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
275 KeyboardReport(),
276 KeyboardReport(KC_LSFT),
277 KeyboardReport(KC_RSFT),
278 KeyboardReport(KC_LSFT, KC_RSFT))))
279 .Times(AnyNumber());
280 // clang-format on
281
282 EXPECT_EQ(is_caps_word_on(), false);
283
284 left_shift.press(); // Press both shifts.
285 run_one_scan_loop();
286 right_shift.press();
287
288 if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
289 idle_for(TAPPING_TERM);
290 }
291 run_one_scan_loop();
292
293 right_shift.release(); // Release both.
294 run_one_scan_loop();
295 left_shift.release();
296 run_one_scan_loop();
297
298 EXPECT_EQ(is_caps_word_on(), true);
299
300 testing::Mock::VerifyAndClearExpectations(&driver);
301}
302
303// clang-format off
304INSTANTIATE_TEST_CASE_P(
305 ShiftPairs,
306 CapsWordBothShifts,
307 ::testing::Values(
308 CapsWordBothShiftsParams{
309 "PlainShifts", KC_LSFT, KC_RSFT},
310 CapsWordBothShiftsParams{
311 "OneshotShifts", OSM(MOD_LSFT), OSM(MOD_RSFT)},
312 CapsWordBothShiftsParams{
313 "SpaceCadetShifts", KC_LSPO, KC_RSPC},
314 CapsWordBothShiftsParams{
315 "ModTapShifts", LSFT_T(KC_A), RSFT_T(KC_B)}
316 ),
317 CapsWordBothShiftsParams::GetName
318 );
319// clang-format on
320
321struct CapsWordDoubleTapShiftParams {
322 std::string name;
323 uint16_t left_shift_keycode;
324
325 static const std::string& GetName(const TestParamInfo<CapsWordDoubleTapShiftParams>& info) {
326 return info.param.name;
327 }
328};
329
330// Tests the DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD method to turn on Caps Word.
331class CapsWordDoubleTapShift : public ::testing::WithParamInterface<CapsWordDoubleTapShiftParams>, public CapsWord {};
332
333// Tests that double tapping activates Caps Word.
334TEST_P(CapsWordDoubleTapShift, Activation) {
335 TestDriver driver;
336 KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
337 set_keymap({left_shift});
338
339 // clang-format off
340 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
341 KeyboardReport(),
342 KeyboardReport(KC_LSFT))))
343 .Times(AnyNumber());
344 // clang-format on
345
346 EXPECT_EQ(is_caps_word_on(), false);
347
348 // Tapping shift twice within the tapping term turns on Caps Word.
349 TapKey(left_shift);
350 idle_for(TAPPING_TERM - 10);
351 TapKey(left_shift);
352
353 EXPECT_EQ(is_caps_word_on(), true);
354
355 testing::Mock::VerifyAndClearExpectations(&driver);
356}
357
358// Double tap doesn't count if another key is pressed between the taps.
359TEST_P(CapsWordDoubleTapShift, Interrupted) {
360 TestDriver driver;
361 KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
362 KeymapKey key_a(0, 1, 0, KC_A);
363 set_keymap({left_shift, key_a});
364
365 // clang-format off
366 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
367 KeyboardReport(),
368 KeyboardReport(KC_LSFT),
369 KeyboardReport(KC_LSFT, KC_A))))
370 .Times(AnyNumber());
371 // clang-format on
372
373 left_shift.press();
374 run_one_scan_loop();
375
376 TapKey(key_a); // 'A' key interrupts the double tap.
377
378 left_shift.release();
379 run_one_scan_loop();
380
381 idle_for(TAPPING_TERM - 10);
382 TapKey(left_shift);
383
384 EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
385 clear_oneshot_mods();
386
387 testing::Mock::VerifyAndClearExpectations(&driver);
388}
389
390// Double tap doesn't count if taps are more than tapping term apart.
391TEST_P(CapsWordDoubleTapShift, SlowTaps) {
392 TestDriver driver;
393 KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
394 set_keymap({left_shift});
395
396 // clang-format off
397 EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
398 KeyboardReport(),
399 KeyboardReport(KC_LSFT))))
400 .Times(AnyNumber());
401 // clang-format on
402
403 TapKey(left_shift);
404 idle_for(TAPPING_TERM + 1);
405 TapKey(left_shift);
406
407 EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
408 clear_oneshot_mods();
409
410 testing::Mock::VerifyAndClearExpectations(&driver);
411}
412
413// clang-format off
414INSTANTIATE_TEST_CASE_P(
415 Shifts,
416 CapsWordDoubleTapShift,
417 ::testing::Values(
418 CapsWordDoubleTapShiftParams{"PlainShift", KC_LSFT},
419 CapsWordDoubleTapShiftParams{"OneshotShift", OSM(MOD_LSFT)}
420 ),
421 CapsWordDoubleTapShiftParams::GetName
422 );
423// clang-format on