[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>
This commit is contained in:
Pascal Getreuer 2022-05-13 23:00:32 -07:00 committed by GitHub
parent 90eef4cd15
commit 68b16bba68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 983 additions and 1 deletions

View file

@ -17,6 +17,7 @@ SPACE_CADET_ENABLE ?= yes
GRAVE_ESC_ENABLE ?= yes
GENERIC_FEATURES = \
CAPS_WORD \
COMBO \
COMMAND \
DEFERRED_EXEC \

View file

@ -81,7 +81,8 @@ OTHER_OPTION_NAMES = \
RGBLIGHT_FULL_POWER \
LTO_ENABLE \
PROGRAMMABLE_BUTTON_ENABLE \
SECURE_ENABLE
SECURE_ENABLE \
CAPS_WORD_ENABLE
define NAME_ECHO
@printf " %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)"

View file

@ -11,6 +11,8 @@
"BACKLIGHT_BREATHING": {"info_key": "backlight.breathing", "value_type": "bool"},
"BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"},
"BACKLIGHT_PIN": {"info_key": "backlight.pin"},
"BOTH_SHIFTS_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.both_shifts_turns_on", "value_type": "bool"},
"CAPS_WORD_IDLE_TIMEOUT": {"info_key": "caps_word.idle_timeout", "value_type": "int"},
"COMBO_COUNT": {"info_key": "combo.count", "value_type": "int"},
"COMBO_TERM": {"info_key": "combo.term", "value_type": "int"},
"DEBOUNCE": {"info_key": "debounce", "value_type": "int"},
@ -19,6 +21,7 @@
#"DEVICE_VER": {"info_key": "usb.device_version", "value_type": "bcd_version"},
"DESCRIPTION": {"info_key": "keyboard_folder", "value_type": "str", "to_json": false},
"DIODE_DIRECTION": {"info_key": "diode_direction"},
"DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.double_tap_shift_turns_on", "value_type": "bool"},
"FORCE_NKRO": {"info_key": "usb.force_nkro", "value_type": "bool"},
"DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"},
"DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"},

View file

@ -10,6 +10,7 @@
"BOARD": {"info_key": "board"},
"BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false},
"BLUETOOTH": {"info_key": "bluetooth.driver"},
"CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"},
"FIRMWARE_FORMAT": {"info_key": "build.firmware_format"},
"KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"},
"MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"},

View file

@ -92,6 +92,16 @@
"enum": ["COL2ROW", "ROW2COL"]
},
"debounce": {"$ref": "qmk.definitions.v1#/unsigned_int"},
"caps_word": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean"},
"both_shifts_turns_on": {"type": "boolean"},
"double_tap_shift_turns_on": {"type": "boolean"},
"idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"},
},
},
"combo": {
"type": "object",
"properties": {

View file

@ -75,6 +75,7 @@
* Software Features
* [Auto Shift](feature_auto_shift.md)
* [Caps Word](feature_caps_word.md)
* [Combos](feature_combo.md)
* [Debounce API](feature_debounce_type.md)
* [Key Lock](feature_key_lock.md)

156
docs/feature_caps_word.md Normal file
View file

@ -0,0 +1,156 @@
# Caps Word
It is often useful to type a single word in all capitals, for instance
abbreviations like "QMK", or in code, identifiers like `KC_SPC`. "Caps Word" is
a modern alternative to Caps Lock:
* Letters are capitalized while active, and Caps Word automatically disables
itself at the end of the word. That is, it stops by default once a space or
any key other than `a`--`z`, `0`--`9`, `-`, `_`, delete, or backspace is
pressed. Caps Word also disables itself if the keyboard is idle for 5 seconds.
This is configurable, see below.
* To avoid requiring a dedicated key for Caps Word, there is an option
(`BOTH_SHIFTS_TURNS_ON_CAPS_WORD`) to activate Caps Word by simultaneously
pressing both shift keys. See below for other options.
* The implementation does not use the Caps Lock (`KC_CAPS`) keycode. Caps Word
works even if you're remapping Caps Lock at the OS level to Ctrl or something
else, as Emacs and Vim users often do.
## How do I enable Caps Word :id=how-do-i-enable-caps-word
In your `rules.mk`, add:
```make
CAPS_WORD_ENABLE = yes
```
Next, use one the following methods to activate Caps Word:
* **Activate by pressing a key**: Use the `CAPS_WORD` keycode (short
alias `CAPSWRD`) in your keymap.
* **Activate by pressing Left Shift + Right Shift**: Add `#define
BOTH_SHIFTS_TURNS_ON_CAPS_WORD` to config.h. You may also need to disable or
reconfigure Command, details below. Then, simultaneously pressing both left
and right shifts turns on Caps Word. This method works with the plain
`KC_LSFT` and `KC_RSFT` keycodes as well as one-shot shifts and Space Cadet
shifts. If your shift keys are mod-taps, hold both shift mod-tap keys until
the tapping term, then release them.
* **Activate by double tapping Left Shift**: Add `#define
DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD` config.h. Then, double tapping Left Shift
turns on Caps Word. This method works with `KC_LSFT` or one-shot Left Shift
`OSM(MOD_LSFT)`. To count as a double tap, the maximum time in milliseconds
between taps is `TAPPING_TERM`, or if using `TAPPING_TERM_PER_KEY`, the time
returned by `get_tapping_term()` for the shift keycode being tapped.
* **Custom activation**: You can activate Caps Word from code by calling
`caps_word_on()`. This may be used to activate Caps Word through [a
combo](feature_combo.md) or [tap dance](feature_tap_dance.md) or any means
you like.
### Troubleshooting: Command :id=troubleshooting-command
When using `BOTH_SHIFTS_TURNS_ON_CAPS_WORD`, you might see a compile 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."**
Many keyboards enable the [Command feature](feature_command.md), which by
default is also activated using the Left Shift + Right Shift key combination. To
fix this conflict, please disable Command by adding in rules.mk:
```make
COMMAND_ENABLE = no
```
Or configure Command to use another key combination like Left Ctrl + Right Ctrl
by defining `IS_COMMAND()` in config.h:
```c
// Activate Command with Left Ctrl + Right Ctrl.
#define IS_COMMAND() (get_mods() == MOD_MASK_CTRL)
```
## Customizing Caps Word :id=customizing-caps-word
### Idle timeout :id=idle-timeout
Caps Word turns off automatically if no keys are pressed for
`CAPS_WORD_IDLE_TIMEOUT` milliseconds. The default is 5000 (5 seconds).
Configure the timeout duration in config.h, for instance
```c
#define CAPS_WORD_IDLE_TIMEOUT 3000 // 3 seconds.
```
Setting `CAPS_WORD_IDLE_TIMEOUT` to 0 configures Caps Word to never time out.
Caps Word then remains active indefinitely until a word breaking key is pressed.
### Functions :id=functions
Functions to manipulate Caps Word:
| Function | Description |
|-------------------------|------------------------------------------------|
| `caps_word_on()` | Turns Caps Word on. |
| `caps_word_off()` | Turns Caps Word off. |
| `caps_word_toggle()` | Toggles Caps Word. |
| `is_caps_word_on()` | Returns true if Caps Word is currently on. |
### Configure which keys are "word breaking" :id=configure-which-keys-are-word-breaking
You can define the `caps_word_press_user(uint16_t keycode)` callback to
configure which keys should be shifted and which keys are considered "word
breaking" and stop Caps Word.
The callback is called on every key press while Caps Word is active. When the
key should be shifted (that is, a letter key), the callback should call
`add_weak_mods(MOD_BIT(KC_LSFT))` to shift the key. Returning true continues the
current "word," while returning false is "word breaking" and deactivates Caps
Word. The default callback is
```c
bool caps_word_press_user(uint16_t keycode) {
switch (keycode) {
// Keycodes that continue Caps Word, with shift applied.
case KC_A ... KC_Z:
case KC_MINS:
add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
return true;
// Keycodes that continue Caps Word, without shifting.
case KC_1 ... KC_0:
case KC_BSPC:
case KC_DEL:
case KC_UNDS:
return true;
default:
return false; // Deactivate Caps Word.
}
}
```
### Representing Caps Word state :id=representing-caps-word-state
Define `caps_word_set_user(bool active)` to get callbacks when Caps Word turns
on or off. This is useful to represent the current Caps Word state, e.g. by
setting an LED or playing a sound. In your keymap, define
```c
void caps_word_set_user(bool active) {
if (active) {
// Do something when Caps Word activates.
} else {
// Do something when Caps Word deactivates.
}
}
```

View file

@ -269,6 +269,14 @@ See also: [Bluetooth](feature_bluetooth.md)
|`OUT_USB` |USB only |
|`OUT_BT` |Bluetooth only |
## Caps Word :id=caps-word
See also: [Caps Word](feature_caps_word.md)
|Key |Aliases |Description |
|-----------|---------|------------------------------|
|`CAPS_WORD`|`CAPSWRD`|Toggles Caps Word |
## Dynamic Macros :id=dynamic-macros
See also: [Dynamic Macros](feature_dynamic_macros.md)

80
quantum/caps_word.c Normal file
View file

@ -0,0 +1,80 @@
// Copyright 2021-2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "caps_word.h"
/** @brief True when Caps Word is active. */
static bool caps_word_active = false;
#if CAPS_WORD_IDLE_TIMEOUT > 0
// Constrain timeout to a sensible range. With 16-bit timers, the longest
// timeout possible is 32768 ms, rounded here to 30000 ms = half a minute.
# if CAPS_WORD_IDLE_TIMEOUT < 100 || CAPS_WORD_IDLE_TIMEOUT > 30000
# error "CAPS_WORD_IDLE_TIMEOUT must be between 100 and 30000 ms"
# endif
/** @brief Deadline for idle timeout. */
static uint16_t idle_timer = 0;
void caps_word_task(void) {
if (caps_word_active && timer_expired(timer_read(), idle_timer)) {
caps_word_off();
}
}
void caps_word_reset_idle_timer(void) {
idle_timer = timer_read() + CAPS_WORD_IDLE_TIMEOUT;
}
#endif // CAPS_WORD_IDLE_TIMEOUT > 0
void caps_word_on(void) {
if (caps_word_active) {
return;
}
clear_mods();
#ifndef NO_ACTION_ONESHOT
clear_oneshot_mods();
#endif // NO_ACTION_ONESHOT
#if CAPS_WORD_IDLE_TIMEOUT > 0
caps_word_reset_idle_timer();
#endif // CAPS_WORD_IDLE_TIMEOUT > 0
caps_word_active = true;
caps_word_set_user(true);
}
void caps_word_off(void) {
if (!caps_word_active) {
return;
}
unregister_weak_mods(MOD_MASK_SHIFT); // Make sure weak shift is off.
caps_word_active = false;
caps_word_set_user(false);
}
void caps_word_toggle(void) {
if (caps_word_active) {
caps_word_off();
} else {
caps_word_on();
}
}
bool is_caps_word_on(void) {
return caps_word_active;
}
__attribute__((weak)) void caps_word_set_user(bool active) {}

43
quantum/caps_word.h Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2021-2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "quantum.h"
#ifndef CAPS_WORD_IDLE_TIMEOUT
# define CAPS_WORD_IDLE_TIMEOUT 5000 // Default timeout of 5 seconds.
#endif // CAPS_WORD_IDLE_TIMEOUT
#if CAPS_WORD_IDLE_TIMEOUT > 0
/** @brief Matrix scan task for Caps Word feature */
void caps_word_task(void);
/** @brief Resets timer for Caps Word idle timeout. */
void caps_word_reset_idle_timer(void);
#else
static inline void caps_word_task(void) {}
#endif // CAPS_WORD_IDLE_TIMEOUT > 0
void caps_word_on(void); /**< Activates Caps Word. */
void caps_word_off(void); /**< Deactivates Caps Word. */
void caps_word_toggle(void); /**< Toggles Caps Word. */
bool is_caps_word_on(void); /**< Gets whether currently active. */
/**
* @brief Caps Word set callback.
*
* @param active True if Caps Word is active, false otherwise
*/
void caps_word_set_user(bool active);

View file

@ -108,6 +108,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifdef BLUETOOTH_ENABLE
# include "outputselect.h"
#endif
#ifdef CAPS_WORD_ENABLE
# include "caps_word.h"
#endif
static uint32_t last_input_modification_time = 0;
uint32_t last_input_activity_time(void) {
@ -549,6 +552,10 @@ void quantum_task(void) {
autoshift_matrix_scan();
#endif
#ifdef CAPS_WORD_ENABLE
caps_word_task();
#endif
#ifdef SECURE_ENABLE
secure_task();
#endif

View file

@ -0,0 +1,160 @@
// Copyright 2021-2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "process_caps_word.h"
bool process_caps_word(uint16_t keycode, keyrecord_t* record) {
if (keycode == CAPSWRD) { // Pressing CAPSWRD toggles Caps Word.
if (record->event.pressed) {
caps_word_toggle();
}
return false;
}
#ifndef NO_ACTION_ONESHOT
const uint8_t mods = get_mods() | get_oneshot_mods();
#else
const uint8_t mods = get_mods();
#endif // NO_ACTION_ONESHOT
if (!is_caps_word_on()) {
// The following optionally turns on Caps Word by holding left and
// right shifts or by double tapping left shift. This way Caps Word
// may be used without needing a dedicated key and also without
// needing combos or tap dance.
#ifdef BOTH_SHIFTS_TURNS_ON_CAPS_WORD
// Many keyboards enable the Command feature by default, which also
// uses left+right shift. It can be configured to use a different
// key combination by defining IS_COMMAND(). We make a non-fatal
// warning if Command is enabled but IS_COMMAND() is *not* defined.
# if defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
# 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)."
# else
if (mods == MOD_MASK_SHIFT
# ifdef COMMAND_ENABLE
// Don't activate Caps Word at the same time as Command.
&& !(IS_COMMAND())
# endif // COMMAND_ENABLE
) {
caps_word_on();
}
# endif // defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
#endif // BOTH_SHIFTS_TURNS_ON_CAPS_WORD
#ifdef DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
// Double tapping left shift turns on Caps Word.
//
// NOTE: This works with KC_LSFT and one-shot left shift. It
// wouldn't make sense with mod-tap or Space Cadet shift since
// double tapping would of course trigger the tapping action.
if (record->event.pressed) {
static bool tapped = false;
static uint16_t timer = 0;
if (keycode == KC_LSFT || keycode == OSM(MOD_LSFT)) {
if (tapped && !timer_expired(record->event.time, timer)) {
// Left shift was double tapped, activate Caps Word.
caps_word_on();
}
tapped = true;
timer = record->event.time + GET_TAPPING_TERM(keycode, record);
} else {
tapped = false; // Reset when any other key is pressed.
}
}
#endif // DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
return true;
}
#if CAPS_WORD_IDLE_TIMEOUT > 0
caps_word_reset_idle_timer();
#endif // CAPS_WORD_IDLE_TIMEOUT > 0
// From here on, we only take action on press events.
if (!record->event.pressed) {
return true;
}
if (!(mods & ~MOD_MASK_SHIFT)) {
switch (keycode) {
// Ignore MO, TO, TG, TT, and OSL layer switch keys.
case QK_MOMENTARY ... QK_MOMENTARY_MAX:
case QK_TO ... QK_TO_MAX:
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
return true;
#ifndef NO_ACTION_TAPPING
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
if (record->tap.count == 0) {
// Deactivate if a mod becomes active through holding
// a mod-tap key.
caps_word_off();
return true;
}
keycode &= 0xff;
break;
# ifndef NO_ACTION_LAYER
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
# endif // NO_ACTION_LAYER
if (record->tap.count == 0) {
return true;
}
keycode &= 0xff;
break;
#endif // NO_ACTION_TAPPING
#ifdef SWAP_HANDS_ENABLE
case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
if (keycode > 0x56F0 || record->tap.count == 0) {
return true;
}
keycode &= 0xff;
break;
#endif // SWAP_HANDS_ENABLE
}
clear_weak_mods();
if (caps_word_press_user(keycode)) {
send_keyboard_report();
return true;
}
}
caps_word_off();
return true;
}
__attribute__((weak)) bool caps_word_press_user(uint16_t keycode) {
switch (keycode) {
// Keycodes that continue Caps Word, with shift applied.
case KC_A ... KC_Z:
case KC_MINS:
add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
return true;
// Keycodes that continue Caps Word, without shifting.
case KC_1 ... KC_0:
case KC_BSPC:
case KC_DEL:
case KC_UNDS:
return true;
default:
return false; // Deactivate Caps Word.
}
}

View file

@ -0,0 +1,37 @@
// Copyright 2021-2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "quantum.h"
#include "caps_word.h"
/**
* @brief Process handler for Caps Word feature.
*
* @param keycode Keycode registered by matrix press, per keymap
* @param record keyrecord_t structure
* @return true Continue processing keycodes, and send to host
* @return false Stop processing keycodes, and don't send to host
*/
bool process_caps_word(uint16_t keycode, keyrecord_t* record);
/**
* @brief Weak function for user-level Caps Word press modification.
*
* @param keycode Keycode registered by matrix press, per keymap
* @return true Continue Caps Word
* @return false Stop Caps Word
*/
bool caps_word_press_user(uint16_t keycode);

View file

@ -307,6 +307,9 @@ bool process_record_quantum(keyrecord_t *record) {
#ifdef TERMINAL_ENABLE
process_terminal(keycode, record) &&
#endif
#ifdef CAPS_WORD_ENABLE
process_caps_word(keycode, record) &&
#endif
#ifdef SPACE_CADET_ENABLE
process_space_cadet(keycode, record) &&
#endif

View file

@ -233,6 +233,11 @@ extern layer_state_t layer_state;
# include "pointing_device.h"
#endif
#ifdef CAPS_WORD_ENABLE
# include "caps_word.h"
# include "process_caps_word.h"
#endif
// For tri-layer
void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3);
layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3);

View file

@ -602,6 +602,8 @@ enum quantum_keycodes {
SECURE_UNLOCK,
SECURE_TOGGLE,
CAPS_WORD,
// Start of custom keycode range for keyboards and keymaps - always leave at the end
SAFE_RANGE
};
@ -964,5 +966,6 @@ enum quantum_keycodes {
#define PB_32 PROGRAMMABLE_BUTTON_32
#define PROGRAMMABLE_BUTTON_MIN PROGRAMMABLE_BUTTON_1
#define PROGRAMMABLE_BUTTON_MAX PROGRAMMABLE_BUTTON_32
#define CAPSWRD CAPS_WORD
#include "quantum_keycodes_legacy.h"

21
tests/caps_word/config.h Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2022 Google LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#pragma once
#include "test_common.h"
#define BOTH_SHIFTS_TURNS_ON_CAPS_WORD
#define DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD

19
tests/caps_word/test.mk Normal file
View file

@ -0,0 +1,19 @@
# Copyright 2022 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
CAPS_WORD_ENABLE = yes
COMMAND_ENABLE = no
SPACE_CADET_ENABLE = yes

View file

@ -0,0 +1,423 @@
// Copyright 2022 Google LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AnyOf;
using ::testing::InSequence;
using ::testing::TestParamInfo;
class CapsWord : public TestFixture {
public:
void SetUp() override {
caps_word_off();
}
// Convenience function to tap `key`.
void TapKey(KeymapKey key) {
key.press();
run_one_scan_loop();
key.release();
run_one_scan_loop();
}
// Taps in order each key in `keys`.
template <typename... Ts>
void TapKeys(Ts... keys) {
for (KeymapKey key : {keys...}) {
TapKey(key);
}
}
};
// Tests caps_word_on(), _off(), and _toggle() functions.
TEST_F(CapsWord, OnOffToggleFuns) {
TestDriver driver;
EXPECT_EQ(is_caps_word_on(), false);
caps_word_on();
EXPECT_EQ(is_caps_word_on(), true);
caps_word_on();
EXPECT_EQ(is_caps_word_on(), true);
caps_word_off();
EXPECT_EQ(is_caps_word_on(), false);
caps_word_off();
EXPECT_EQ(is_caps_word_on(), false);
caps_word_toggle();
EXPECT_EQ(is_caps_word_on(), true);
caps_word_toggle();
EXPECT_EQ(is_caps_word_on(), false);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Tests the default `caps_word_press_user()` function.
TEST_F(CapsWord, DefaultCapsWordPressUserFun) {
// Spot check some keycodes that continue Caps Word, with shift applied.
for (uint16_t keycode : {KC_A, KC_B, KC_Z, KC_MINS}) {
SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
clear_weak_mods();
EXPECT_TRUE(caps_word_press_user(keycode));
EXPECT_EQ(get_weak_mods(), MOD_BIT(KC_LSFT));
}
// Some keycodes that continue Caps Word, without shifting.
for (uint16_t keycode : {KC_1, KC_9, KC_0, KC_BSPC, KC_DEL}) {
SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
clear_weak_mods();
EXPECT_TRUE(caps_word_press_user(keycode));
EXPECT_EQ(get_weak_mods(), 0);
}
// Some keycodes that turn off Caps Word.
for (uint16_t keycode : {KC_SPC, KC_DOT, KC_COMM, KC_TAB, KC_ESC, KC_ENT}) {
SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
EXPECT_FALSE(caps_word_press_user(keycode));
}
}
// Tests that `CAPSWRD` key toggles Caps Word.
TEST_F(CapsWord, CapswrdKey) {
TestDriver driver;
KeymapKey key_capswrd(0, 0, 0, CAPSWRD);
set_keymap({key_capswrd});
// No keyboard reports should be sent.
EXPECT_CALL(driver, send_keyboard_mock(_)).Times(0);
TapKey(key_capswrd); // Tap the CAPSWRD key.
EXPECT_EQ(is_caps_word_on(), true);
TapKey(key_capswrd); // Tap the CAPSWRD key again.
EXPECT_EQ(is_caps_word_on(), false);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Tests that being idle for CAPS_WORD_IDLE_TIMEOUT turns off Caps Word.
TEST_F(CapsWord, IdleTimeout) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
set_keymap({key_a});
// Allow any number of reports with no keys or only KC_LSFT.
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
// Expect "Shift+A".
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
// Turn on Caps Word and tap "A".
caps_word_on();
TapKey(key_a);
testing::Mock::VerifyAndClearExpectations(&driver);
idle_for(CAPS_WORD_IDLE_TIMEOUT);
run_one_scan_loop();
// Caps Word should be off and mods should be clear.
EXPECT_EQ(is_caps_word_on(), false);
EXPECT_EQ(get_mods() | get_weak_mods(), 0);
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport())).Times(AnyNumber());
// Expect unshifted "A".
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
TapKey(key_a);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Tests that typing "A, 4, A, 4" produces "Shift+A, 4, Shift+A, 4".
TEST_F(CapsWord, ShiftsLettersButNotDigits) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_4(0, 1, 0, KC_4);
set_keymap({key_a, key_4});
// Allow any number of reports with no keys or only KC_LSFT.
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
{ // Expect: "Shift+A, 4, Shift+A, 4".
InSequence s;
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
}
// Turn on Caps Word and tap "A, 4, A, 4".
caps_word_on();
TapKeys(key_a, key_4, key_a, key_4);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Tests that typing "A, Space, A" produces "Shift+A, Space, A".
TEST_F(CapsWord, SpaceTurnsOffCapsWord) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_spc(0, 1, 0, KC_SPC);
set_keymap({key_a, key_spc});
// Allow any number of reports with no keys or only KC_LSFT.
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
{ // Expect: "Shift+A, Space, A".
InSequence seq;
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_SPC)));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
}
// Turn on Caps Word and tap "A, Space, A".
caps_word_on();
TapKeys(key_a, key_spc, key_a);
testing::Mock::VerifyAndClearExpectations(&driver);
}
struct CapsWordBothShiftsParams {
std::string name;
uint16_t left_shift_keycode;
uint16_t right_shift_keycode;
static const std::string& GetName(const TestParamInfo<CapsWordBothShiftsParams>& info) {
return info.param.name;
}
};
// Tests the BOTH_SHIFTS_TURNS_ON_CAPS_WORD method to turn on Caps Word.
class CapsWordBothShifts : public ::testing::WithParamInterface<CapsWordBothShiftsParams>, public CapsWord {};
// Pressing shifts as "Left down, Right down, Left up, Right up".
TEST_P(CapsWordBothShifts, PressLRLR) {
TestDriver driver;
KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
set_keymap({left_shift, right_shift});
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT),
KeyboardReport(KC_RSFT),
KeyboardReport(KC_LSFT, KC_RSFT))))
.Times(AnyNumber());
// clang-format on
EXPECT_EQ(is_caps_word_on(), false);
left_shift.press(); // Press both shifts.
run_one_scan_loop();
right_shift.press();
// For mod-tap and Space Cadet keys, wait for the tapping term.
if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
idle_for(TAPPING_TERM);
}
run_one_scan_loop();
left_shift.release(); // Release both.
run_one_scan_loop();
right_shift.release();
run_one_scan_loop();
EXPECT_EQ(is_caps_word_on(), true);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Pressing shifts as "Left down, Right down, Right up, Left up".
TEST_P(CapsWordBothShifts, PressLRRL) {
TestDriver driver;
KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
set_keymap({left_shift, right_shift});
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT),
KeyboardReport(KC_RSFT),
KeyboardReport(KC_LSFT, KC_RSFT))))
.Times(AnyNumber());
// clang-format on
EXPECT_EQ(is_caps_word_on(), false);
left_shift.press(); // Press both shifts.
run_one_scan_loop();
right_shift.press();
if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
idle_for(TAPPING_TERM);
}
run_one_scan_loop();
right_shift.release(); // Release both.
run_one_scan_loop();
left_shift.release();
run_one_scan_loop();
EXPECT_EQ(is_caps_word_on(), true);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// clang-format off
INSTANTIATE_TEST_CASE_P(
ShiftPairs,
CapsWordBothShifts,
::testing::Values(
CapsWordBothShiftsParams{
"PlainShifts", KC_LSFT, KC_RSFT},
CapsWordBothShiftsParams{
"OneshotShifts", OSM(MOD_LSFT), OSM(MOD_RSFT)},
CapsWordBothShiftsParams{
"SpaceCadetShifts", KC_LSPO, KC_RSPC},
CapsWordBothShiftsParams{
"ModTapShifts", LSFT_T(KC_A), RSFT_T(KC_B)}
),
CapsWordBothShiftsParams::GetName
);
// clang-format on
struct CapsWordDoubleTapShiftParams {
std::string name;
uint16_t left_shift_keycode;
static const std::string& GetName(const TestParamInfo<CapsWordDoubleTapShiftParams>& info) {
return info.param.name;
}
};
// Tests the DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD method to turn on Caps Word.
class CapsWordDoubleTapShift : public ::testing::WithParamInterface<CapsWordDoubleTapShiftParams>, public CapsWord {};
// Tests that double tapping activates Caps Word.
TEST_P(CapsWordDoubleTapShift, Activation) {
TestDriver driver;
KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
set_keymap({left_shift});
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
EXPECT_EQ(is_caps_word_on(), false);
// Tapping shift twice within the tapping term turns on Caps Word.
TapKey(left_shift);
idle_for(TAPPING_TERM - 10);
TapKey(left_shift);
EXPECT_EQ(is_caps_word_on(), true);
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Double tap doesn't count if another key is pressed between the taps.
TEST_P(CapsWordDoubleTapShift, Interrupted) {
TestDriver driver;
KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
KeymapKey key_a(0, 1, 0, KC_A);
set_keymap({left_shift, key_a});
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT),
KeyboardReport(KC_LSFT, KC_A))))
.Times(AnyNumber());
// clang-format on
left_shift.press();
run_one_scan_loop();
TapKey(key_a); // 'A' key interrupts the double tap.
left_shift.release();
run_one_scan_loop();
idle_for(TAPPING_TERM - 10);
TapKey(left_shift);
EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
clear_oneshot_mods();
testing::Mock::VerifyAndClearExpectations(&driver);
}
// Double tap doesn't count if taps are more than tapping term apart.
TEST_P(CapsWordDoubleTapShift, SlowTaps) {
TestDriver driver;
KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
set_keymap({left_shift});
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
TapKey(left_shift);
idle_for(TAPPING_TERM + 1);
TapKey(left_shift);
EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
clear_oneshot_mods();
testing::Mock::VerifyAndClearExpectations(&driver);
}
// clang-format off
INSTANTIATE_TEST_CASE_P(
Shifts,
CapsWordDoubleTapShift,
::testing::Values(
CapsWordDoubleTapShiftParams{"PlainShift", KC_LSFT},
CapsWordDoubleTapShiftParams{"OneshotShift", OSM(MOD_LSFT)}
),
CapsWordDoubleTapShiftParams::GetName
);
// clang-format on