diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c index 8f238490f2a..af7b8b5ce1c 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -49,6 +49,47 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re } # endif +# ifdef CHORDAL_HOLD +__attribute__((weak)) bool get_chordal_hold( + uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record, + uint16_t other_keycode, keyrecord_t* other_record) { + return get_chordal_hold_default(tap_hold_record, other_record); +} + +bool get_chordal_hold_default( + keyrecord_t* tap_hold_record, keyrecord_t* other_record) { + uint8_t tap_hold_hand = chordal_hold_handedness_user(tap_hold_record->event.key); + if (tap_hold_hand == 0) { + return true; + } + uint8_t other_hand = chordal_hold_handedness_user(other_record->event.key); + return other_hand == 0 || tap_hold_hand != other_hand; +} + +__attribute__((weak)) uint8_t chordal_hold_handedness_kb(keypos_t key) { +# if defined(SPLIT_KEYBOARD) || ((MATRIX_ROWS) > (MATRIX_COLS)) +#pragma message "Inferred handedness rows" + // If the keyboard is split or if MATRIX_ROWS > MATRIX_COLS, assume that the + // first half of the rows are left and the latter half are right. + return (key.row < (MATRIX_ROWS) / 2) ? /*left*/ 1 : /*right*/ 2; +# else +#pragma message "Inferred handedness cols" + // Otherwise, assume the first half of the cols are left, others are right. + return (key.col < (MATRIX_COLS) / 2) ? /*left*/ 1 : /*right*/ 2; +# endif +} + +__attribute__((weak)) uint8_t chordal_hold_handedness_user(keypos_t key) { +# if defined(CHORDAL_HOLD_LAYOUT) +# pragma message "Using chordal_hold_layout" + // If given, read handedness from `chordal_hold_layout` array. + return pgm_read_byte(&chordal_hold_layout[key.row][key.col]); +# else + return chordal_hold_handedness_kb(key); +# endif +} +# endif // CHORDAL_HOLD + # ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) { return false; @@ -188,7 +229,7 @@ bool process_tapping(keyrecord_t *keyp) { return true; } -# if (defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT)) || defined(PERMISSIVE_HOLD_PER_KEY) || defined(HOLD_ON_OTHER_KEY_PRESS_PER_KEY) +# if (defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT)) || defined(PERMISSIVE_HOLD_PER_KEY) || defined(CHORDAL_HOLD) || defined(HOLD_ON_OTHER_KEY_PRESS_PER_KEY) TAP_DEFINE_KEYCODE; # endif @@ -271,6 +312,22 @@ bool process_tapping(keyrecord_t *keyp) { // set interrupted flag when other key pressed during tapping if (event.pressed) { tapping_key.tap.interrupted = true; + +# if defined(CHORDAL_HOLD) + if (!is_tap_record(keyp) && + !get_chordal_hold(tapping_keycode, &tapping_key, + get_record_keycode(keyp, true), keyp)) { + // Settle the tapping key as *tapped*, since it is + // not considered a held chord with keyp. + ac_dprintf("Tapping: End. Tap in non-chord\n"); + tapping_key.tap.count = 1; + // In process_action(), HOLD_ON_OTHER_KEY_PRESS will + // revert interrupted events to holds, so this needs + // to be set false. + tapping_key.tap.interrupted = false; + process_record(&tapping_key); + } else +# endif if (TAP_GET_HOLD_ON_OTHER_KEY_PRESS # if defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT) // Auto Shift cannot evaluate this early @@ -278,6 +335,8 @@ bool process_tapping(keyrecord_t *keyp) { && !(MAYBE_RETRO_SHIFTING(event, keyp) && get_auto_shifted_key(get_record_keycode(keyp, false), keyp)) # endif ) { + // Settle the tapping key as *held*, since + // HOLD_ON_OTHER_KEY_PRESS is enabled for this key. ac_dprintf("Tapping: End. No tap. Interfered by pressed key\n"); process_record(&tapping_key); tapping_key = (keyrecord_t){0}; diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h index 6b518b82988..0e7721b5008 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -46,6 +46,82 @@ bool get_permissive_hold(uint16_t keycode, keyrecord_t *record); bool get_retro_tapping(uint16_t keycode, keyrecord_t *record); bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record); +#ifdef CHORDAL_HOLD +/** + * Callback to say when a key chord before the tapping term is considered held. + * + * In keymap.c, define the callback + * + * bool get_chordal_hold(uint16_t tap_hold_keycode, + * keyrecord_t* tap_hold_record, + * uint16_t other_keycode, + * keyrecord_t* other_record) { + * // Conditions... + * } + * + * This callback is called when: + * + * 1. `tap_hold_keycode` is pressed. + * 2. `other_keycode` is pressed while `tap_hold_keycode` is still held, + * provided `other_keycode` is *not* also a tap-hold key and it is pressed + * before the tapping term. + * + * Returning true indicates that the tap-hold key should be considered held, or + * false to consider it tapped. + * + * @param tap_hold_keycode Keycode of the tap-hold key. + * @param tap_hold_record Record from the tap-hold press event. + * @param other_keycode Keycode of the other key. + * @param other_record Record from the other key's press event. + * @return True if the tap-hold key is considered held; false if tapped. + */ +bool get_chordal_hold( + uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record, + uint16_t other_keycode, keyrecord_t* other_record); + +/** + * Default "opposite hands rule" for whether a key chord should settle as held. + * + * This function returns true when the tap-hold key and other key are on + * "opposite hands." In detail, handedness of the two keys are compared. If + * handedness values differ, or if either handedness is zero, the function + * returns true, indicating a hold. Otherwise, it returns false, indicating that + * the tap-hold key should settle as tapped. + * + * "Handedness" is determined as follows, in order of decending precedence: + * 1. `chordal_hold_handedness_user()`, if defined. + * 2. `chordal_hold_layout`, if CHORDAL_HOLD_LAYOUT is defined. + * 3. `chordal_hold_handedness_kb()`, if defined. + * 4. fallback assumption based on keyboard matrix dimensions. + * + * @param tap_hold_record Record of the active tap-hold key press. + * @param other_record Record of the other, interrupting key press. + * @return True if the tap-hold key is considered held; false if tapped. + */ +bool get_chordal_hold_default( + keyrecord_t* tap_hold_record, keyrecord_t* other_record); + +/** + * Keyboard-level callback to determine handedness of a key. + * + * This function should return: + * 1 for keys pressed by the left hand, + * 2 for keys on the right hand, + * 0 for keys exempt from the "opposite hands rule." This could be used + * perhaps on thumb keys or keys that might be pressed by either hand. + * + * @param key A key matrix position. + * @return Handedness value. + */ +uint8_t chordal_hold_handedness_kb(keypos_t key); +/** User callback to determine handedness of a key. */ +uint8_t chordal_hold_handedness_user(keypos_t key); + +# ifdef CHORDAL_HOLD_LAYOUT +extern const uint8_t chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; +# endif +#endif + #ifdef DYNAMIC_TAPPING_TERM_ENABLE extern uint16_t g_tapping_term; #endif diff --git a/tests/tap_hold_configurations/chordal_hold/config.h b/tests/tap_hold_configurations/chordal_hold/config.h new file mode 100644 index 00000000000..510b6a035f2 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/config.h @@ -0,0 +1,21 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#pragma once + +#include "test_common.h" +#define CHORDAL_HOLD + diff --git a/tests/tap_hold_configurations/chordal_hold/test.mk b/tests/tap_hold_configurations/chordal_hold/test.mk new file mode 100644 index 00000000000..6b5968df16f --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/test.mk @@ -0,0 +1,18 @@ +# Copyright 2022 Vladislav Kucheriavykh +# +# 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 . + +# -------------------------------------------------------------------------------- +# Keep this file, even if it is empty, as a marker that this folder contains tests +# -------------------------------------------------------------------------------- diff --git a/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp new file mode 100644 index 00000000000..cbe8e4e1c20 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp @@ -0,0 +1,167 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#include "keyboard_report_util.hpp" +#include "keycode.h" +#include "test_common.hpp" +#include "action_tapping.h" +#include "test_fixture.hpp" +#include "test_keymap_key.hpp" + +using testing::_; +using testing::InSequence; + +class ChordalHold : public TestFixture {}; + +TEST_F(ChordalHold, chord_with_mod_tap_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + // Regular key on the right hand. + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A)); + regular_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHold, non_chord_with_mod_tap_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key and regular key both on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_P)); + regular_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHold, tap_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + idle_for(TAPPING_TERM - 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHold, hold_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHold, chordal_hold_ignores_multiple_mod_taps) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press second mod-tap key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT)); + mod_tap_key2.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + /* Release keys. */ + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h new file mode 100644 index 00000000000..b1d37b1e3f9 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h @@ -0,0 +1,22 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#pragma once + +#include "test_common.h" +#define CHORDAL_HOLD +#define HOLD_ON_OTHER_KEY_PRESS + diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk new file mode 100644 index 00000000000..6b5968df16f --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk @@ -0,0 +1,18 @@ +# Copyright 2022 Vladislav Kucheriavykh +# +# 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 . + +# -------------------------------------------------------------------------------- +# Keep this file, even if it is empty, as a marker that this folder contains tests +# -------------------------------------------------------------------------------- diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp new file mode 100644 index 00000000000..68ee607ac8f --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp @@ -0,0 +1,167 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#include "keyboard_report_util.hpp" +#include "keycode.h" +#include "test_common.hpp" +#include "action_tapping.h" +#include "test_fixture.hpp" +#include "test_keymap_key.hpp" + +using testing::_; +using testing::InSequence; + +class ChordalHoldAndHoldOnOtherKeypress : public TestFixture {}; + +TEST_F(ChordalHoldAndHoldOnOtherKeypress, chord_with_mod_tap_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + // Regular key on the right hand. + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A)); + regular_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndHoldOnOtherKeypress, non_chord_with_mod_tap_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key and regular key both on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_P)); + regular_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndHoldOnOtherKeypress, tap_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + idle_for(TAPPING_TERM - 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndHoldOnOtherKeypress, hold_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndHoldOnOtherKeypress, chordal_hold_ignores_multiple_mod_taps) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press second mod-tap key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT)); + mod_tap_key2.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + /* Release keys. */ + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h new file mode 100644 index 00000000000..7f06b679a85 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h @@ -0,0 +1,22 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#pragma once + +#include "test_common.h" +#define CHORDAL_HOLD +#define PERMISSIVE_HOLD + diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk new file mode 100644 index 00000000000..6b5968df16f --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk @@ -0,0 +1,18 @@ +# Copyright 2022 Vladislav Kucheriavykh +# +# 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 . + +# -------------------------------------------------------------------------------- +# Keep this file, even if it is empty, as a marker that this folder contains tests +# -------------------------------------------------------------------------------- diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp new file mode 100644 index 00000000000..fdb053a5b21 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp @@ -0,0 +1,167 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * + * 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 . + */ + +#include "keyboard_report_util.hpp" +#include "keycode.h" +#include "test_common.hpp" +#include "action_tapping.h" +#include "test_fixture.hpp" +#include "test_keymap_key.hpp" + +using testing::_; +using testing::InSequence; + +class ChordalHoldAndPermissiveHold : public TestFixture {}; + +TEST_F(ChordalHoldAndPermissiveHold, chord_with_mod_tap_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + // Regular key on the right hand. + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A)); + regular_key.press(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndPermissiveHold, non_chord_with_mod_tap_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key and regular key both on the left hand. + auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_hold_key, regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press regular key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_P)); + regular_key.release(); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndPermissiveHold, tap_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + idle_for(TAPPING_TERM - 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndPermissiveHold, hold_mod_tap_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + + set_keymap({mod_tap_key}); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldAndPermissiveHold, chordal_hold_ignores_multiple_mod_taps) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + /* Press second mod-tap key. */ + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT)); + mod_tap_key2.press(); + idle_for(TAPPING_TERM + 1); + VERIFY_AND_CLEAR(driver); + + /* Release keys. */ + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} +