diff options
| author | Pascal Getreuer <50221757+getreuer@users.noreply.github.com> | 2022-05-13 23:00:32 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-14 16:00:32 +1000 |
| commit | 68b16bba68d79c63bbd649a5be58756e937c96da (patch) | |
| tree | be0d3d672c13c080f4da58687b5b1a261f465911 | |
| parent | 90eef4cd153cdc1b00d973e6abb029828f656294 (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.mk | 1 | ||||
| -rw-r--r-- | builddefs/show_options.mk | 3 | ||||
| -rw-r--r-- | data/mappings/info_config.json | 3 | ||||
| -rw-r--r-- | data/mappings/info_rules.json | 1 | ||||
| -rw-r--r-- | data/schemas/keyboard.jsonschema | 10 | ||||
| -rw-r--r-- | docs/_summary.md | 1 | ||||
| -rw-r--r-- | docs/feature_caps_word.md | 156 | ||||
| -rw-r--r-- | docs/keycodes.md | 8 | ||||
| -rw-r--r-- | quantum/caps_word.c | 80 | ||||
| -rw-r--r-- | quantum/caps_word.h | 43 | ||||
| -rw-r--r-- | quantum/keyboard.c | 7 | ||||
| -rw-r--r-- | quantum/process_keycode/process_caps_word.c | 160 | ||||
| -rw-r--r-- | quantum/process_keycode/process_caps_word.h | 37 | ||||
| -rw-r--r-- | quantum/quantum.c | 3 | ||||
| -rw-r--r-- | quantum/quantum.h | 5 | ||||
| -rw-r--r-- | quantum/quantum_keycodes.h | 3 | ||||
| -rw-r--r-- | tests/caps_word/config.h | 21 | ||||
| -rw-r--r-- | tests/caps_word/test.mk | 19 | ||||
| -rw-r--r-- | tests/caps_word/test_caps_word.cpp | 423 |
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 | |||
| 17 | GRAVE_ESC_ENABLE ?= yes | 17 | GRAVE_ESC_ENABLE ?= yes |
| 18 | 18 | ||
| 19 | GENERIC_FEATURES = \ | 19 | GENERIC_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 | ||
| 86 | define NAME_ECHO | 87 | define 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 | |||
| 3 | It is often useful to type a single word in all capitals, for instance | ||
| 4 | abbreviations like "QMK", or in code, identifiers like `KC_SPC`. "Caps Word" is | ||
| 5 | a 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 | |||
| 24 | In your `rules.mk`, add: | ||
| 25 | |||
| 26 | ```make | ||
| 27 | CAPS_WORD_ENABLE = yes | ||
| 28 | ``` | ||
| 29 | |||
| 30 | Next, 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 | |||
| 57 | When 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 | ||
| 59 | time, since both use the Left Shift + Right Shift key combination."** | ||
| 60 | |||
| 61 | Many keyboards enable the [Command feature](feature_command.md), which by | ||
| 62 | default is also activated using the Left Shift + Right Shift key combination. To | ||
| 63 | fix this conflict, please disable Command by adding in rules.mk: | ||
| 64 | |||
| 65 | ```make | ||
| 66 | COMMAND_ENABLE = no | ||
| 67 | ``` | ||
| 68 | |||
| 69 | Or configure Command to use another key combination like Left Ctrl + Right Ctrl | ||
| 70 | by 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 | |||
| 82 | Caps Word turns off automatically if no keys are pressed for | ||
| 83 | `CAPS_WORD_IDLE_TIMEOUT` milliseconds. The default is 5000 (5 seconds). | ||
| 84 | Configure the timeout duration in config.h, for instance | ||
| 85 | |||
| 86 | ```c | ||
| 87 | #define CAPS_WORD_IDLE_TIMEOUT 3000 // 3 seconds. | ||
| 88 | ``` | ||
| 89 | |||
| 90 | Setting `CAPS_WORD_IDLE_TIMEOUT` to 0 configures Caps Word to never time out. | ||
| 91 | Caps Word then remains active indefinitely until a word breaking key is pressed. | ||
| 92 | |||
| 93 | |||
| 94 | ### Functions :id=functions | ||
| 95 | |||
| 96 | Functions 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 | |||
| 108 | You can define the `caps_word_press_user(uint16_t keycode)` callback to | ||
| 109 | configure which keys should be shifted and which keys are considered "word | ||
| 110 | breaking" and stop Caps Word. | ||
| 111 | |||
| 112 | The callback is called on every key press while Caps Word is active. When the | ||
| 113 | key 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 | ||
| 115 | current "word," while returning false is "word breaking" and deactivates Caps | ||
| 116 | Word. The default callback is | ||
| 117 | |||
| 118 | ```c | ||
| 119 | bool 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 | |||
| 143 | Define `caps_word_set_user(bool active)` to get callbacks when Caps Word turns | ||
| 144 | on or off. This is useful to represent the current Caps Word state, e.g. by | ||
| 145 | setting an LED or playing a sound. In your keymap, define | ||
| 146 | |||
| 147 | ```c | ||
| 148 | void 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 | |||
| 274 | See 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 | ||
| 274 | See also: [Dynamic Macros](feature_dynamic_macros.md) | 282 | See 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. */ | ||
| 18 | static 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. */ | ||
| 28 | static uint16_t idle_timer = 0; | ||
| 29 | |||
| 30 | void caps_word_task(void) { | ||
| 31 | if (caps_word_active && timer_expired(timer_read(), idle_timer)) { | ||
| 32 | caps_word_off(); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | void caps_word_reset_idle_timer(void) { | ||
| 37 | idle_timer = timer_read() + CAPS_WORD_IDLE_TIMEOUT; | ||
| 38 | } | ||
| 39 | #endif // CAPS_WORD_IDLE_TIMEOUT > 0 | ||
| 40 | |||
| 41 | void 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 | |||
| 58 | void 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 | |||
| 68 | void caps_word_toggle(void) { | ||
| 69 | if (caps_word_active) { | ||
| 70 | caps_word_off(); | ||
| 71 | } else { | ||
| 72 | caps_word_on(); | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | bool 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 */ | ||
| 25 | void caps_word_task(void); | ||
| 26 | |||
| 27 | /** @brief Resets timer for Caps Word idle timeout. */ | ||
| 28 | void caps_word_reset_idle_timer(void); | ||
| 29 | #else | ||
| 30 | static inline void caps_word_task(void) {} | ||
| 31 | #endif // CAPS_WORD_IDLE_TIMEOUT > 0 | ||
| 32 | |||
| 33 | void caps_word_on(void); /**< Activates Caps Word. */ | ||
| 34 | void caps_word_off(void); /**< Deactivates Caps Word. */ | ||
| 35 | void caps_word_toggle(void); /**< Toggles Caps Word. */ | ||
| 36 | bool 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 | */ | ||
| 43 | void 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 | ||
| 112 | static uint32_t last_input_modification_time = 0; | 115 | static uint32_t last_input_modification_time = 0; |
| 113 | uint32_t last_input_activity_time(void) { | 116 | uint32_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 | |||
| 17 | bool 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 | */ | ||
| 28 | bool 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 | */ | ||
| 37 | bool 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 |
| 237 | void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3); | 242 | void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3); |
| 238 | layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3); | 243 | layer_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 | |||
| 16 | CAPS_WORD_ENABLE = yes | ||
| 17 | COMMAND_ENABLE = no | ||
| 18 | SPACE_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 | |||
| 22 | using ::testing::_; | ||
| 23 | using ::testing::AnyNumber; | ||
| 24 | using ::testing::AnyOf; | ||
| 25 | using ::testing::InSequence; | ||
| 26 | using ::testing::TestParamInfo; | ||
| 27 | |||
| 28 | class 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. | ||
| 52 | TEST_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. | ||
| 76 | TEST_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. | ||
| 101 | TEST_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. | ||
| 119 | TEST_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". | ||
| 157 | TEST_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". | ||
| 187 | TEST_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 | |||
| 215 | struct 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. | ||
| 226 | class CapsWordBothShifts : public ::testing::WithParamInterface<CapsWordBothShiftsParams>, public CapsWord {}; | ||
| 227 | |||
| 228 | // Pressing shifts as "Left down, Right down, Left up, Right up". | ||
| 229 | TEST_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". | ||
| 267 | TEST_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 | ||
| 304 | INSTANTIATE_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 | |||
| 321 | struct 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. | ||
| 331 | class CapsWordDoubleTapShift : public ::testing::WithParamInterface<CapsWordDoubleTapShiftParams>, public CapsWord {}; | ||
| 332 | |||
| 333 | // Tests that double tapping activates Caps Word. | ||
| 334 | TEST_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. | ||
| 359 | TEST_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. | ||
| 391 | TEST_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 | ||
| 414 | INSTANTIATE_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 | ||
