diff options
| -rw-r--r-- | data/mappings/info_config.hjson | 1 | ||||
| -rw-r--r-- | data/schemas/keyboard.jsonschema | 3 | ||||
| -rw-r--r-- | docs/feature_caps_word.md | 20 | ||||
| -rw-r--r-- | quantum/process_keycode/process_caps_word.c | 66 | ||||
| -rw-r--r-- | tests/caps_word/caps_word_invert_on_shift/config.h | 21 | ||||
| -rw-r--r-- | tests/caps_word/caps_word_invert_on_shift/test.mk | 17 | ||||
| -rw-r--r-- | tests/caps_word/caps_word_invert_on_shift/test_caps_word_invert_on_shift.cpp | 215 | 
7 files changed, 342 insertions, 1 deletions
| diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index bc4f46c353..7c1a4ee36b 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -28,6 +28,7 @@      "BOOTMAGIC_LITE_COLUMN_RIGHT": {"info_key": "split.bootmagic.matrix.1", "value_type": "int"},      "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"}, +    "CAPS_WORD_INVERT_ON_SHIFT": {"info_key": "caps_word.invert_on_shift", "value_type": "bool"},      "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"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index ee6ecf28e4..6c4ff49855 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -227,7 +227,8 @@                  "enabled": {"type": "boolean"},                  "both_shifts_turns_on": {"type": "boolean"},                  "double_tap_shift_turns_on": {"type": "boolean"}, -                "idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"} +                "idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"}, +                "invert_on_shift": {"type": "boolean"}              }          },          "combo": { diff --git a/docs/feature_caps_word.md b/docs/feature_caps_word.md index c58d1a56e2..7f726b059d 100644 --- a/docs/feature_caps_word.md +++ b/docs/feature_caps_word.md @@ -90,6 +90,26 @@ by defining `IS_COMMAND()` in config.h:  ## Customizing Caps Word :id=customizing-caps-word +### Invert on shift :id=invert-on-shift + +By default, Caps Word turns off when Shift keys are pressed, considering them as +word-breaking. Alternatively with the `CAPS_WORD_INVERT_ON_SHIFT` option, +pressing the Shift key continues Caps Word and inverts the shift state. This +is convenient for uncapitalizing one or a few letters within a word, for +example with Caps Word on, typing "D, B, Shift+A, Shift+A, S" produces "DBaaS", +or typing "P, D, F, Shift+S" produces "PDFs". + +Enable it by adding in config.h + +```c +#define CAPS_WORD_INVERT_ON_SHIFT +``` + +This option works with regular Shift keys `KC_LSFT` and `KC_RSFT`, mod-tap Shift +keys, and one-shot Shift keys. Note that while Caps Word is on, one-shot Shift +keys behave like regular Shift keys, and have effect only while they are held. + +  ### Idle timeout :id=idle-timeout  Caps Word turns off automatically if no keys are pressed for diff --git a/quantum/process_keycode/process_caps_word.c b/quantum/process_keycode/process_caps_word.c index 94302b29ae..8f2ee1db8b 100644 --- a/quantum/process_keycode/process_caps_word.c +++ b/quantum/process_keycode/process_caps_word.c @@ -14,6 +14,54 @@  #include "process_caps_word.h" +#ifdef CAPS_WORD_INVERT_ON_SHIFT +static uint8_t held_mods = 0; + +static bool handle_shift(uint16_t keycode, keyrecord_t* record) { +    switch (keycode) { +        case OSM(MOD_LSFT): +            keycode = KC_LSFT; +            break; +        case OSM(MOD_RSFT): +            keycode = KC_RSFT; +            break; + +#    ifndef NO_ACTION_TAPPING +        case QK_MOD_TAP ... QK_MOD_TAP_MAX: +            if (record->tap.count == 0) { // Mod-tap key is held. +                switch (QK_MOD_TAP_GET_MODS(keycode)) { +                    case MOD_LSFT: +                        keycode = KC_LSFT; +                        break; +                    case MOD_RSFT: +                        keycode = KC_RSFT; +                        break; +                } +            } +#    endif // NO_ACTION_TAPPING +    } + +    if (keycode == KC_LSFT || keycode == KC_RSFT) { +        const uint8_t mod = MOD_BIT(keycode); + +        if (is_caps_word_on()) { +            if (record->event.pressed) { +                held_mods |= mod; +            } else { +                held_mods &= ~mod; +            } +            return false; +        } else if ((held_mods & mod) != 0) { +            held_mods &= ~mod; +            del_mods(mod); +            return record->event.pressed; +        } +    } + +    return true; +} +#endif // CAPS_WORD_INVERT_ON_SHIFT +  bool process_caps_word(uint16_t keycode, keyrecord_t* record) {      if (keycode == QK_CAPS_WORD_TOGGLE) {          if (record->event.pressed) { @@ -21,6 +69,11 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {          }          return false;      } +#ifdef CAPS_WORD_INVERT_ON_SHIFT +    if (!handle_shift(keycode, record)) { +        return false; +    } +#endif // CAPS_WORD_INVERT_ON_SHIFT  #ifndef NO_ACTION_ONESHOT      const uint8_t mods = get_mods() | get_oneshot_mods(); @@ -111,12 +164,14 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {                  if (record->tap.count == 0) { // Mod-tap key is held.                      const uint8_t mods = QK_MOD_TAP_GET_MODS(keycode);                      switch (mods) { +#    ifndef CAPS_WORD_INVERT_ON_SHIFT                          case MOD_LSFT:                              keycode = KC_LSFT;                              break;                          case MOD_RSFT:                              keycode = KC_RSFT;                              break; +#    endif // CAPS_WORD_INVERT_ON_SHIFT                          case MOD_RSFT | MOD_RALT:                              keycode = RSFT(KC_RALT);                              break; @@ -124,6 +179,9 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {                              return true;                          default:                              caps_word_off(); +#    ifdef CAPS_WORD_INVERT_ON_SHIFT +                            add_mods(held_mods); +#    endif // CAPS_WORD_INVERT_ON_SHIFT                              return true;                      }                  } else { @@ -163,12 +221,20 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {          clear_weak_mods();  #endif // AUTO_SHIFT_ENABLE          if (caps_word_press_user(keycode)) { +#ifdef CAPS_WORD_INVERT_ON_SHIFT +            if (held_mods) { +                set_weak_mods(get_weak_mods() ^ MOD_BIT(KC_LSFT)); +            } +#endif // CAPS_WORD_INVERT_ON_SHIFT              send_keyboard_report();              return true;          }      }      caps_word_off(); +#ifdef CAPS_WORD_INVERT_ON_SHIFT +    add_mods(held_mods); +#endif // CAPS_WORD_INVERT_ON_SHIFT      return true;  } diff --git a/tests/caps_word/caps_word_invert_on_shift/config.h b/tests/caps_word/caps_word_invert_on_shift/config.h new file mode 100644 index 0000000000..7a3ec846f9 --- /dev/null +++ b/tests/caps_word/caps_word_invert_on_shift/config.h @@ -0,0 +1,21 @@ +// Copyright 2023 Google LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +#pragma once + +#include "test_common.h" + +#define CAPS_WORD_INVERT_ON_SHIFT +#define PERMISSIVE_HOLD diff --git a/tests/caps_word/caps_word_invert_on_shift/test.mk b/tests/caps_word/caps_word_invert_on_shift/test.mk new file mode 100644 index 0000000000..319c04d67a --- /dev/null +++ b/tests/caps_word/caps_word_invert_on_shift/test.mk @@ -0,0 +1,17 @@ +# Copyright 2023 Google LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +CAPS_WORD_ENABLE = yes + diff --git a/tests/caps_word/caps_word_invert_on_shift/test_caps_word_invert_on_shift.cpp b/tests/caps_word/caps_word_invert_on_shift/test_caps_word_invert_on_shift.cpp new file mode 100644 index 0000000000..d322448181 --- /dev/null +++ b/tests/caps_word/caps_word_invert_on_shift/test_caps_word_invert_on_shift.cpp @@ -0,0 +1,215 @@ +// Copyright 2023 Google LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +#include "keyboard_report_util.hpp" +#include "keycode.h" +#include "test_common.hpp" +#include "test_fixture.hpp" +#include "test_keymap_key.hpp" + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::AnyOf; +using ::testing::InSequence; +using ::testing::TestParamInfo; + +namespace { + +struct ShiftKeyParams { +    std::string name; +    uint16_t    keycode; +    uint16_t    report_shift_code; + +    static const std::string& GetName(const TestParamInfo<ShiftKeyParams>& info) { +        return info.param.name; +    } +}; + +class CapsWordInvertOnShift : public ::testing::WithParamInterface<ShiftKeyParams>, public TestFixture { +    void SetUp() override { +        caps_word_off(); +    } +}; + +// With Caps Word on, type "A, 4, Shift(A, 4, A), A, Shift(A), 4". +TEST_P(CapsWordInvertOnShift, ShiftWithinWord) { +    TestDriver driver; +    KeymapKey  key_shift(0, 0, 0, GetParam().keycode); +    KeymapKey  key_a(0, 1, 0, KC_A); +    KeymapKey  key_4(0, 2, 0, KC_4); +    set_keymap({key_shift, 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: "A4a$aAa4" +        InSequence s; +        EXPECT_REPORT(driver, (KC_LSFT, KC_A)); +        EXPECT_REPORT(driver, (KC_4)); +        EXPECT_REPORT(driver, (KC_A)); +        EXPECT_REPORT(driver, (KC_LSFT, KC_4)); +        EXPECT_REPORT(driver, (KC_A)); +        EXPECT_REPORT(driver, (KC_LSFT, KC_A)); +        EXPECT_REPORT(driver, (KC_A)); +        EXPECT_REPORT(driver, (KC_4)); +    } + +    caps_word_on(); +    tap_keys(key_a, key_4); // Type "A, 4". + +    key_shift.press(); // Type "Shift(A, 4, A)". +    run_one_scan_loop(); +    tap_keys(key_a, key_4, key_a); +    key_shift.release(); +    run_one_scan_loop(); + +    tap_key(key_a); // Type "A". + +    key_shift.press(); // Type "Shift(A)". +    run_one_scan_loop(); +    tap_key(key_a); +    key_shift.release(); +    run_one_scan_loop(); + +    tap_key(key_4); // Type "4". + +    VERIFY_AND_CLEAR(driver); +} + +TEST_P(CapsWordInvertOnShift, ShiftHeldAtWordEnd) { +    TestDriver driver; +    KeymapKey  key_shift(0, 0, 0, GetParam().keycode); +    KeymapKey  key_a(0, 1, 0, KC_A); +    KeymapKey  key_slsh(0, 2, 0, KC_SLSH); +    set_keymap({key_shift, key_a, key_slsh}); + +    // 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), +                KeyboardReport(KC_RSFT)))) +        .Times(AnyNumber()); +    // clang-format on + +    { // Expect: "Aa?A" +        InSequence s; +        EXPECT_REPORT(driver, (KC_LSFT, KC_A)); +        EXPECT_REPORT(driver, (KC_A)); +        EXPECT_REPORT(driver, (GetParam().report_shift_code, KC_SLSH)); +        EXPECT_REPORT(driver, (GetParam().report_shift_code, KC_A)); +    } + +    caps_word_on(); +    tap_key(key_a); + +    key_shift.press(); // Press Shift. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), 0); + +    tap_key(key_a); +    tap_key(key_slsh); // Tap '/' key, which is word breaking, ending Caps Word. + +    EXPECT_FALSE(is_caps_word_on()); +    EXPECT_EQ(get_mods(), MOD_BIT(GetParam().report_shift_code)); + +    tap_key(key_a); +    key_shift.release(); // Release Shift. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), 0); +    VERIFY_AND_CLEAR(driver); +} + +TEST_P(CapsWordInvertOnShift, TwoShiftsHeld) { +    TestDriver driver; +    KeymapKey  key_shift1(0, 0, 0, GetParam().keycode); +    KeymapKey  key_shift2(0, 1, 0, GetParam().report_shift_code); +    KeymapKey  key_a(0, 2, 0, KC_A); +    KeymapKey  key_slsh(0, 3, 0, KC_SLSH); +    set_keymap({key_shift1, key_shift2, key_a, key_slsh}); + +    // 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), +                KeyboardReport(KC_RSFT)))) +        .Times(AnyNumber()); +    // clang-format on + +    { // Expect: "Aa?a" +        InSequence s; +        EXPECT_REPORT(driver, (KC_LSFT, KC_A)); +        EXPECT_REPORT(driver, (KC_A)); +        EXPECT_REPORT(driver, (GetParam().report_shift_code, KC_SLSH)); +        EXPECT_REPORT(driver, (KC_A)); +    } + +    caps_word_on(); +    tap_key(key_a); + +    key_shift1.press(); // Press shift1. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), 0); + +    tap_key(key_a); +    tap_key(key_slsh); // Tap '/' key, which is word breaking, ending Caps Word. + +    EXPECT_FALSE(is_caps_word_on()); +    EXPECT_EQ(get_mods(), MOD_BIT(GetParam().report_shift_code)); + +    key_shift2.press(); // Press shift2. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), MOD_BIT(GetParam().report_shift_code)); + +    key_shift1.release(); // Release shift1. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), 0); +    tap_key(key_a); + +    key_shift2.release(); // Release shift2. +    run_one_scan_loop(); + +    EXPECT_EQ(get_mods(), 0); +    VERIFY_AND_CLEAR(driver); +} + +// clang-format off +INSTANTIATE_TEST_CASE_P( +    Shifts, +    CapsWordInvertOnShift, +    ::testing::Values( +        ShiftKeyParams{"KC_LSFT", KC_LSFT, KC_LSFT}, +        ShiftKeyParams{"KC_RSFT", KC_RSFT, KC_RSFT}, +        ShiftKeyParams{"LSFT_T", LSFT_T(KC_A), KC_LSFT}, +        ShiftKeyParams{"RSFT_T", RSFT_T(KC_A), KC_RSFT}, +        ShiftKeyParams{"OSM_LSFT", OSM(MOD_LSFT), KC_LSFT}, +        ShiftKeyParams{"OSM_RSFT", OSM(MOD_RSFT), KC_RSFT} +      ), +    ShiftKeyParams::GetName +    ); +// clang-format on + +} // namespace | 
