diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index b699f862770..f2543939655 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -384,7 +384,11 @@ "h": {"$ref": "qmk.definitions.v1#/key_unit"}, "w": {"$ref": "qmk.definitions.v1#/key_unit"}, "x": {"$ref": "qmk.definitions.v1#/key_unit"}, - "y": {"$ref": "qmk.definitions.v1#/key_unit"} + "y": {"$ref": "qmk.definitions.v1#/key_unit"}, + "hand": { + "type": "string", + "pattern": "[LR*]", + } } } } diff --git a/docs/tap_hold.md b/docs/tap_hold.md index 0ade6a86737..49f798157bd 100644 --- a/docs/tap_hold.md +++ b/docs/tap_hold.md @@ -491,17 +491,10 @@ moment that `KC_C` is released. Determining whether keys are on the same or opposite hands involves defining the "handedness" of each key position. By default, if nothing is specified, -handedness is guessed based on keyboard matrix dimensions. If this is -inaccurate, handedness may be specified in several ways. +handedness is guessed based on keyboard geometry. -The easiest way to specify handedness is by `chordal_hold_layout`. Define in -config.h: - -```c -#define CHORDAL_HOLD_LAYOUT -``` - -Then in keymap.c, define `chordal_hold_layout` in the following form: +Handedness may be specified with `chordal_hold_layout`. In keymap.c, define +`chordal_hold_layout` in the following form: ```c const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = @@ -514,37 +507,21 @@ const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = ``` Use the same `LAYOUT` macro as used to define your keymap layers. Each entry is -a character, either `'L'` for left, `'R'` for right, or `'*'` to exempt keys -from the "opposite hands rule." When a key has `'*'` handedness, pressing it -with either hand results in a hold. This could be used perhaps on thumb keys or -other places where you want to allow same-hand chords. +a character indicating the handedness of one key, either `'L'` for left, `'R'` +for right, or `'*'` to exempt keys from the "opposite hands rule." When a key +has `'*'` handedness, pressing it with either hand results in a hold. This could +be used perhaps on thumb keys or other places where you want to allow same-hand +chords. -Alternatively, handedness may be defined functionally with -`chordal_hold_handedness_user()`. For example, in keymap.c define: +Keyboard makers may specify handedness in keyboard.json. Under `"layouts"`, +specify the handedness of a key by adding a `"hand"` field with a value of +either `"L"`, `"R"`, or `"*"`. Note that if `"layouts"` contains multiple +layouts, only the first one is read. For example: -```c -char chordal_hold_handedness_user(keypos_t key) { - if (key.col == 0 || key.col == MATRIX_COLS - 1) { - return '*'; // Exempt the outer columns. - } - // On split keyboards, typically, the first half of the rows are on the - // left, and the other half are on the right. - return key.row < MATRIX_ROWS / 2 ? 'L' : 'R'; -} +```json +{"matrix": [5, 6], "x": 0, "y": 5.5, "w": 1.25, "hand", "*"}, ``` -Given the matrix position of a key, the function should return `'L'`, `'R'`, or -`'*'`. Adapt the logic in this function according to the keyboard's matrix. -Similarly at the keyboard level, `chordal_hold_handedness_kb()` may be defined -to specify handedness. - -::: warning -Note the matrix may have irregularities around larger keys, around the edges of -the board, and around thumb clusters. You may find it helpful to use [this -debugging example](faq_debug#which-matrix-position-is-this-keypress) to -correspond physical keys to matrix positions. -::: - ### Per-chord customization diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index 5a6c9674860..5eb210b77fa 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -1,5 +1,7 @@ """Used by the make system to generate keyboard.c from info.json. """ +import statistics + from milc import cli from qmk.info import info_json @@ -87,6 +89,55 @@ def _gen_matrix_mask(info_data): lines.append(f' 0b{"".join(reversed(mask[i]))},') lines.append('};') lines.append('#endif') + lines.append('') + + return lines + + +def _gen_chordal_hold_layout(info_data): + keys_x = [] + keys_hand = [] + + # Get x-coordinate for the center of each key. + # NOTE: If there are multiple layouts, only the first is read. + for layout_name, layout_data in info_data['layouts'].items(): + for key_data in layout_data['layout']: + keys_x.append(key_data['x'] + key_data.get('w', 1.0) / 2) + keys_hand.append(key_data.get('hand', '')) + break + + x_midline = statistics.median(keys_x) + x_prev = None + + layout_handedness = [[]] + + for x, hand in zip(keys_x, keys_hand): + if x_prev is not None and x < x_prev: + layout_handedness.append([]) + + if not hand: + # Where unspecified, assume handedness based on the key's location + # relative to the midline. + if abs(x - x_midline) > 0.25: + hand = 'L' if x < x_midline else 'R' + else: + hand = '*' + + layout_handedness[-1].append(hand) + x_prev = x + + lines = [] + lines.append('#ifdef CHORDAL_HOLD') + lines.append('__attribute__((weak)) const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = ' + layout_name + '(') + + for i, row in enumerate(layout_handedness): + line = ' ' + ', '.join(f"'{c}'" for c in row) + if i < len(layout_handedness) - 1: + line += ',' + lines.append(line) + + lines.append(');') + lines.append('#endif') return lines @@ -101,10 +152,11 @@ def generate_keyboard_c(cli): kb_info_json = info_json(cli.args.keyboard) # Build the layouts.h file. - keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] + keyboard_c_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] - keyboard_h_lines.extend(_gen_led_configs(kb_info_json)) - keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json)) + keyboard_c_lines.extend(_gen_led_configs(kb_info_json)) + keyboard_c_lines.extend(_gen_matrix_mask(kb_info_json)) + keyboard_c_lines.extend(_gen_chordal_hold_layout(kb_info_json)) # Show the results - dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet) + dump_lines(cli.args.output, keyboard_c_lines, cli.args.quiet) diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c index 53a3413c385..984bf098a8b 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -50,6 +50,8 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re # endif # ifdef CHORDAL_HOLD +extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; + # define REGISTERED_TAPS_SIZE 8 // Array of tap-hold keys that have been settled as tapped but not yet released. static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {}; @@ -609,32 +611,16 @@ bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_r return true; // Return true on combos or other non-key events. } - char tap_hold_hand = chordal_hold_handedness_user(tap_hold_record->event.key); + char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key); if (tap_hold_hand == '*') { return true; } - char other_hand = chordal_hold_handedness_user(other_record->event.key); + char other_hand = chordal_hold_handedness(other_record->event.key); return other_hand == '*' || tap_hold_hand != other_hand; } -__attribute__((weak)) char chordal_hold_handedness_kb(keypos_t key) { -# if defined(SPLIT_KEYBOARD) || ((MATRIX_ROWS) > (MATRIX_COLS)) - // 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*/ 'L' : /*right*/ 'R'; -# else - // Otherwise, assume the first half of the cols are left, others are right. - return (key.col < (MATRIX_COLS) / 2) ? /*left*/ 'L' : /*right*/ 'R'; -# endif -} - -__attribute__((weak)) char chordal_hold_handedness_user(keypos_t key) { -# if defined(CHORDAL_HOLD_LAYOUT) - // If given, read handedness from `chordal_hold_layout` array. +__attribute__((weak)) char chordal_hold_handedness(keypos_t key) { return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]); -# else - return chordal_hold_handedness_kb(key); -# endif } static void registered_taps_add(keypos_t key) { diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h index 6227a394d6d..77c45f504fa 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -88,12 +88,6 @@ bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, u * returns true, indicating that it may be held. Otherwise, it returns false, * in which case the tap-hold key is immediately settled at 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 may be considered held; false if tapped. @@ -101,9 +95,9 @@ bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t *tap_hold_record, u bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record); /** - * Keyboard-level callback to determine handedness of a key. + * Gets the handedness of a key. * - * This function should return: + * This function returns: * 'L' for keys pressed by the left hand, * 'R' for keys on the right hand, * '*' for keys exempt from the "opposite hands rule." This could be used @@ -112,9 +106,7 @@ bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_r * @param key A key matrix position. * @return Handedness value. */ -char chordal_hold_handedness_kb(keypos_t key); -/** User callback to determine handedness of a key. */ -char chordal_hold_handedness_user(keypos_t key); +char chordal_hold_handedness(keypos_t key); # ifdef CHORDAL_HOLD_LAYOUT extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; diff --git a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk index 6b5968df16f..86e45bc5bce 100644 --- a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk @@ -13,6 +13,4 @@ # 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 -# -------------------------------------------------------------------------------- +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c new file mode 100644 index 00000000000..ffc914b645e --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c @@ -0,0 +1,8 @@ +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +}; diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h b/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h index 910a753c270..f8787f08338 100644 --- a/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h @@ -18,5 +18,4 @@ #include "test_common.h" #define CHORDAL_HOLD -#define CHORDAL_HOLD_LAYOUT #define PERMISSIVE_HOLD diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp index 3a79dbf35aa..61cab76b8c3 100644 --- a/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp @@ -26,9 +26,9 @@ using testing::InSequence; class ChordalHoldPermissiveHold : public TestFixture {}; TEST_F(ChordalHoldPermissiveHold, chordal_hold_handedness) { - EXPECT_EQ(chordal_hold_handedness_user({.col = 0, .row = 0}), 'L'); - EXPECT_EQ(chordal_hold_handedness_user({.col = MATRIX_COLS - 1, .row = 0}), 'R'); - EXPECT_EQ(chordal_hold_handedness_user({.col = 0, .row = 2}), '*'); + EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 0}), 'L'); + EXPECT_EQ(chordal_hold_handedness({.col = MATRIX_COLS - 1, .row = 0}), 'R'); + EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 2}), '*'); } TEST_F(ChordalHoldPermissiveHold, get_chordal_hold_default) { diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk index 0660eb3e6b8..d59be407c5c 100644 --- a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk @@ -14,3 +14,4 @@ # along with this program. If not, see . AUTO_SHIFT_ENABLE = yes +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c new file mode 100644 index 00000000000..ffc914b645e --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c @@ -0,0 +1,8 @@ +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +};