Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Caps Word capitalization when used with Combos + Auto Shift. #17549

Merged
merged 12 commits into from
Aug 13, 2022
7 changes: 6 additions & 1 deletion quantum/process_keycode/process_auto_shift.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ bool get_autoshift_shift_state(uint16_t keycode) {
/** \brief Restores the shift key if it was cancelled by Auto Shift */
static void autoshift_flush_shift(void) {
autoshift_flags.holding_shift = false;
del_weak_mods(MOD_BIT(KC_LSFT));
# ifdef CAPS_WORD_ENABLE
if (!is_caps_word_on())
# endif // CAPS_WORD_ENABLE
{
del_weak_mods(MOD_BIT(KC_LSFT));
}
if (autoshift_flags.cancelling_lshift) {
autoshift_flags.cancelling_lshift = false;
add_mods(MOD_BIT(KC_LSFT));
Expand Down
4 changes: 4 additions & 0 deletions quantum/process_keycode/process_caps_word.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ bool process_caps_word(uint16_t keycode, keyrecord_t* record) {
#endif // SWAP_HANDS_ENABLE
}

#ifdef AUTO_SHIFT_ENABLE
del_weak_mods(get_autoshift_state() ? ~MOD_BIT(KC_LSFT) : 0xff);
#else
clear_weak_mods();
#endif // AUTO_SHIFT_ENABLE
if (caps_word_press_user(keycode)) {
send_keyboard_report();
return true;
Expand Down
9 changes: 9 additions & 0 deletions quantum/process_keycode/process_combo.c
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,16 @@ static inline void dump_key_buffer(void) {
#endif
}
record->event.time = 0;

#if defined(CAPS_WORD_ENABLE) && defined(AUTO_SHIFT_ENABLE)
// Edge case: preserve the weak Left Shift mod if both Caps Word and
// Auto Shift are on. Caps Word capitalizes by setting the weak Left
// Shift mod during the press event, but Auto Shift doesn't send the
// key until it receives the release event.
del_weak_mods((is_caps_word_on() && get_autoshift_state()) ? ~MOD_BIT(KC_LSFT) : 0xff);
#else
clear_weak_mods();
#endif // defined(CAPS_WORD_ENABLE) && defined(AUTO_SHIFT_ENABLE)

#if TAP_CODE_DELAY > 0
// only delay once and for a non-tapping key
Expand Down
64 changes: 50 additions & 14 deletions tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"

// Allow reports with no keys or only KC_LSFT.
// clang-format off
#define EXPECT_EMPTY_OR_LSFT(driver) \
EXPECT_CALL(driver, send_keyboard_mock(AnyOf( \
KeyboardReport(), \
KeyboardReport(KC_LSFT))))
// clang-format on

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AnyOf;
Expand All @@ -39,13 +47,7 @@ TEST_F(CapsWord, AutoShiftKeys) {
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_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "A, A, space, a".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
Expand All @@ -65,6 +67,46 @@ TEST_F(CapsWord, AutoShiftKeys) {
testing::Mock::VerifyAndClearExpectations(&driver);
}

// Test Caps Word + Auto Shift where keys A and B are rolled.
TEST_F(CapsWord, AutoShiftRolledShiftedKeys) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 0, 1, KC_B);
set_keymap({key_a, key_b});

EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "A, B, A, B".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
}

caps_word_on();

key_a.press(); // Overlapping taps: A down, B down, A up, B up.
run_one_scan_loop();
key_b.press();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();
key_b.release();
run_one_scan_loop();

key_a.press(); // Nested taps: A down, B down, B up, A up.
run_one_scan_loop();
key_b.press();
run_one_scan_loop();
key_b.release();
run_one_scan_loop();
key_a.release();
run_one_scan_loop();

caps_word_off();
testing::Mock::VerifyAndClearExpectations(&driver);
}

// Tests that with tap-hold keys with Retro Shift, letter keys are shifted by
// Caps Word regardless of whether they are retroshifted.
TEST_F(CapsWord, RetroShiftKeys) {
Expand All @@ -73,13 +115,7 @@ TEST_F(CapsWord, RetroShiftKeys) {
KeymapKey key_layertap_b(0, 1, 0, LT(1, KC_B));
set_keymap({key_modtap_a, key_layertap_b});

// 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_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "B, A, B, A".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
Expand Down
20 changes: 20 additions & 0 deletions tests/caps_word/caps_word_combo/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 TAPPING_TERM 200
19 changes: 19 additions & 0 deletions tests/caps_word/caps_word_combo/test.mk
Original file line number Diff line number Diff line change
@@ -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
COMBO_ENABLE = yes
AUTO_SHIFT_ENABLE = yes

212 changes: 212 additions & 0 deletions tests/caps_word/caps_word_combo/test_caps_word_combo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// 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/>.

// Test Caps Word + Combos, with and without Auto Shift.

#include <algorithm>
#include <numeric>
#include <vector>

#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"

// Allow reports with no keys or only KC_LSFT.
// clang-format off
#define EXPECT_EMPTY_OR_LSFT(driver) \
EXPECT_CALL(driver, send_keyboard_mock(AnyOf( \
KeyboardReport(), \
KeyboardReport(KC_LSFT))))
// clang-format on

using ::testing::AnyNumber;
using ::testing::AnyOf;
using ::testing::InSequence;
using ::testing::TestParamInfo;

extern "C" {
// Define some combos to use for the test, including overlapping combos and
// combos that chord tap-hold keys.
enum combo_events { AB_COMBO, BC_COMBO, AD_COMBO, DE_COMBO, FGHI_COMBO, COMBO_LENGTH };
uint16_t COMBO_LEN = COMBO_LENGTH;

const uint16_t ab_combo[] PROGMEM = {KC_A, KC_B, COMBO_END};
const uint16_t bc_combo[] PROGMEM = {KC_B, KC_C, COMBO_END};
const uint16_t ad_combo[] PROGMEM = {KC_A, LCTL_T(KC_D), COMBO_END};
const uint16_t de_combo[] PROGMEM = {LCTL_T(KC_D), LT(1, KC_E), COMBO_END};
const uint16_t fghi_combo[] PROGMEM = {KC_F, KC_G, KC_H, KC_I, COMBO_END};

// clang-format off
combo_t key_combos[] = {
[AB_COMBO] = COMBO(ab_combo, KC_SPC), // KC_A + KC_B = KC_SPC
[BC_COMBO] = COMBO(bc_combo, KC_X), // KC_B + KC_C = KC_X
[AD_COMBO] = COMBO(ad_combo, KC_Y), // KC_A + LCTL_T(KC_D) = KC_Y
[DE_COMBO] = COMBO(de_combo, KC_Z), // LCTL_T(KC_D) + LT(1, KC_E) = KC_Z
[FGHI_COMBO] = COMBO(fghi_combo, KC_W) // KC_F + KC_G + KC_H + KC_I = KC_W
};
// clang-format on
} // extern "C"

namespace {

// To test combos thorougly, we test them with pressing the chord keys with
// a few different orders and timings.
struct TestParams {
std::string name;
bool autoshift_on;

static const std::string& GetName(const TestParamInfo<TestParams>& info) {
return info.param.name;
}
};

class CapsWord : public ::testing::WithParamInterface<TestParams>, public TestFixture {
public:
void SetUp() override {
caps_word_off();
if (GetParam().autoshift_on) {
autoshift_enable();
} else {
autoshift_disable();
}
}
};

// Test pressing the keys in a combo with different orders and timings.
TEST_P(CapsWord, SingleCombo) {
TestDriver driver;
KeymapKey key_b(0, 0, 1, KC_B);
KeymapKey key_c(0, 0, 2, KC_C);
set_keymap({key_b, key_c});

EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT, KC_X));

caps_word_on();
tap_combo({key_b, key_c});

EXPECT_TRUE(is_caps_word_on());
caps_word_off();

testing::Mock::VerifyAndClearExpectations(&driver);
}

// Test a longer 4-key combo.
TEST_P(CapsWord, LongerCombo) {
TestDriver driver;
KeymapKey key_f(0, 0, 0, KC_F);
KeymapKey key_g(0, 0, 1, KC_G);
KeymapKey key_h(0, 0, 2, KC_H);
KeymapKey key_i(0, 0, 3, KC_I);
set_keymap({key_f, key_g, key_h, key_i});

EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT, KC_W));

caps_word_on();
tap_combo({key_f, key_g, key_h, key_i});

EXPECT_TRUE(is_caps_word_on());
caps_word_off();

testing::Mock::VerifyAndClearExpectations(&driver);
}

// Test with two overlapping combos on regular keys:
// KC_A + KC_B = KC_SPC,
// KC_B + KC_C = KC_X.
TEST_P(CapsWord, ComboRegularKeys) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_b(0, 0, 1, KC_B);
KeymapKey key_c(0, 0, 2, KC_C);
KeymapKey key_1(0, 0, 3, KC_1);
set_keymap({key_a, key_b, key_c, key_1});

EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "A, B, 1, X, 1, C, space, a".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_LSFT, KC_X));
EXPECT_REPORT(driver, (KC_1));
EXPECT_REPORT(driver, (KC_LSFT, KC_C));
EXPECT_REPORT(driver, (KC_SPC));
EXPECT_REPORT(driver, (KC_A));
}

caps_word_on();
tap_key(key_a);
tap_key(key_b);
tap_key(key_1);
tap_combo({key_b, key_c}); // BC combo types "x".
tap_key(key_1);
tap_key(key_c);
tap_combo({key_a, key_b}); // AB combo types space.
tap_key(key_a);

EXPECT_FALSE(is_caps_word_on());
testing::Mock::VerifyAndClearExpectations(&driver);
}

// Test where combo chords involve tap-hold keys:
// KC_A + LCTL_T(KC_D) = KC_Y,
// LCTL_T(KC_D) + LT(1, KC_E) = KC_Z,
TEST_P(CapsWord, ComboModTapKey) {
TestDriver driver;
KeymapKey key_a(0, 0, 0, KC_A);
KeymapKey key_modtap_d(0, 0, 1, LCTL_T(KC_D));
KeymapKey key_layertap_e(0, 0, 2, LT(1, KC_E));
set_keymap({key_a, key_modtap_d, key_layertap_e});

EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "A, D, E, Y, Z".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT, KC_D));
EXPECT_REPORT(driver, (KC_LSFT, KC_E));
EXPECT_REPORT(driver, (KC_LSFT, KC_Y));
EXPECT_REPORT(driver, (KC_LSFT, KC_Z));
}

caps_word_on();
tap_key(key_a);
tap_key(key_modtap_d);
tap_key(key_layertap_e);
tap_combo({key_a, key_modtap_d}); // AD combo types "y".
tap_combo({key_modtap_d, key_layertap_e}); // DE combo types "z".

EXPECT_TRUE(is_caps_word_on());
caps_word_off();

testing::Mock::VerifyAndClearExpectations(&driver);
}

// clang-format off
INSTANTIATE_TEST_CASE_P(
Combos,
CapsWord,
::testing::Values(
TestParams{"AutoshiftDisabled", false},
TestParams{"AutoshiftEnabled", true}
),
TestParams::GetName
);
// clang-format on

} // namespace
Loading