From 73de490e53300fa39a52663124f425ac2f1763df Mon Sep 17 00:00:00 2001 From: rootiest Date: Wed, 22 Apr 2026 22:36:16 -0400 Subject: [PATCH] feat(q5_max): Add speculative hold for caps_mod key --- .../q5_max/ansi_encoder/keymaps/via/config.h | 9 ++ .../q5_max/ansi_encoder/keymaps/via/keymap.c | 82 ++++++++----------- keyboards/keychron/q5_max/info.json | 3 +- 3 files changed, 43 insertions(+), 51 deletions(-) diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h index e341c556f3..6bdfb9054d 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h @@ -17,7 +17,16 @@ // COMM+DOT+SLSH fallback combo fires regardless of the active layer. #define COMBO_ONLY_FROM_LAYER 0 +// Pressing the Shift key continues Caps Word and inverts the shift state +#define CAPS_WORD_INVERT_ON_SHIFT + // Default tapping term for mod-tap, layer-tap, and tap-dance keys. #define TAPPING_TERM 200 // Allow per-key overrides via get_tapping_term() in keymap.c. #define TAPPING_TERM_PER_KEY + +// Use right CTRL key to neutralize modifier taps when cancelled. +#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_RIGHT_CTRL + +// Neutralize left ALT and left GUI (Default value) +#define MODS_TO_NEUTRALIZE {MOD_BIT(KC_LEFT_ALT), MOD_BIT(KC_LEFT_GUI)} diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c index bd2e736ad2..fd051663f8 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c @@ -41,10 +41,11 @@ enum custom_keycodes { LCK_FN4, // Lock/unlock FN4 LCK_CTL, // Lock/unlock KEEB_CTL LCK_BASE, // Clear all locks and return to BASE - CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect BSP_DEL, // Tap=Backspace, Shift+Tap=Delete }; +#define CAPS_MOD MT(MOD_LCTL, KC_ESC) + // Declare layers early so the HID functions below can reference KEEB_CTL. enum layers { BASE, @@ -99,12 +100,12 @@ static void hid_send_battery(void) { if (!(get_transport() & TRANSPORT_WIRELESS)) return; uint8_t level = battery_get_percentage(); if (level == g_last_sent_bat) return; - g_last_sent_bat = level; - uint8_t data[HID_PACKET_SIZE] = {0}; - data[HID_OFF_CMD] = HID_CMD_BATTERY; - data[HID_OFF_SRC] = HID_DEV_Q5MAX; - data[HID_OFF_FLAGS] = 0; - data[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = level; + g_last_sent_bat = level; + uint8_t data[HID_PACKET_SIZE] = {0}; + data[HID_OFF_CMD] = HID_CMD_BATTERY; + data[HID_OFF_SRC] = HID_DEV_Q5MAX; + data[HID_OFF_FLAGS] = 0; + data[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = level; raw_hid_send(data, HID_PACKET_SIZE); } #endif @@ -187,15 +188,12 @@ bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) { // Host is querying the current battery level. // Reply with the current percentage when in wireless mode, or // HID_BATT_UNAVAILABLE when wired (USB transport / not meaningful). - uint8_t resp[HID_PACKET_SIZE] = {0}; - resp[HID_OFF_CMD] = HID_CMD_BATTERY; - resp[HID_OFF_SRC] = HID_DEV_Q5MAX; - resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE; + uint8_t resp[HID_PACKET_SIZE] = {0}; + resp[HID_OFF_CMD] = HID_CMD_BATTERY; + resp[HID_OFF_SRC] = HID_DEV_Q5MAX; + resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE; #ifdef LK_WIRELESS_ENABLE - resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = - (get_transport() & TRANSPORT_WIRELESS) - ? battery_get_percentage() - : HID_BATT_UNAVAILABLE; + resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = (get_transport() & TRANSPORT_WIRELESS) ? battery_get_percentage() : HID_BATT_UNAVAILABLE; #else resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = HID_BATT_UNAVAILABLE; #endif @@ -211,9 +209,7 @@ bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) { } // CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect -static bool caps_mod_held = false; -static bool caps_mod_ctrl_registered = false; -static uint16_t caps_mod_timer = 0; +// (Refactored to use MT(MOD_LCTL, KC_ESC) with custom tap logic) // clang-format off const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { @@ -350,38 +346,28 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { case CAPS_MOD: - if (record->event.pressed) { - caps_mod_held = true; - caps_mod_timer = timer_read(); - // If a real modifier is held, send a dummy key so the OS sees - // modifier+key rather than a bare modifier hold/tap. Without - // this, the OS never receives any keycode while the modifier is - // down and treats the eventual modifier release as a tap (e.g. - // GUI opening the app menu). KC_F24 is harmless and universally - // ignored by applications. - if (get_mods() & (MOD_MASK_GUI | MOD_MASK_ALT | MOD_MASK_SHIFT)) { - register_code(KC_F24); - unregister_code(KC_F24); - } - } else { - if (caps_mod_ctrl_registered) { - unregister_code(KC_LCTL); - caps_mod_ctrl_registered = false; - } else { - uint8_t mods = get_mods(); + // Custom tap logic: only intercept if it's a TAP AND a modifier is held. + // If it's a pure hold (Ctrl) or a pure tap (Esc), return true to let + // the MT() core handle it. + if (record->tap.count > 0 && record->event.pressed) { + uint8_t mods = get_mods(); + if (mods & (MOD_MASK_GUI | MOD_MASK_ALT)) { + // Neutralize the modifier hold so releasing GUI/Alt doesn't + // trigger an OS "tap" action (like opening the Start menu). + tap_code(DUMMY_MOD_NEUTRALIZER_KEYCODE); + if (mods & MOD_MASK_GUI) { autocorrect_toggle(); - } else if (mods & MOD_MASK_ALT) { - caps_word_toggle(); - } else if (mods & MOD_MASK_SHIFT) { - tap_code(KC_CAPS); // Shift still held → host sees Shift+CapsLock (toggles on most OSes) } else { - tap_code(KC_ESC); + caps_word_toggle(); } + return false; // suppress default Esc tap + } else if (mods & MOD_MASK_SHIFT) { + tap_code(KC_CAPS); + return false; // suppress default Esc tap } - caps_mod_held = false; // cleared in both hold and tap paths } - return false; + return true; // let core handle Esc tap or Ctrl hold case LCK_FN1: case LCK_FN2: @@ -466,10 +452,6 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { } void matrix_scan_user(void) { - if (caps_mod_held && !caps_mod_ctrl_registered && timer_elapsed(caps_mod_timer) > TAPPING_TERM) { - caps_mod_ctrl_registered = true; - register_code(KC_LCTL); - } if (alt_tab_active && timer_elapsed(alt_tab_timer) > ALT_TAB_TIMEOUT) { unregister_code(KC_LALT); alt_tab_active = false; @@ -493,7 +475,7 @@ void matrix_scan_user(void) { // BASE stays dark; each FN/control layer gets a distinct colour. #if defined(RGB_MATRIX_ENABLE) bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) { -#ifdef DIP_SWITCH_ENABLE +# ifdef DIP_SWITCH_ENABLE // Win-side override: paint all LEDs white so the user gets a clean white // backlight regardless of which RGB effect is active. The effect keeps // ticking internally and resumes the moment the switch returns to Mac side. @@ -504,7 +486,7 @@ bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) { rgb_matrix_set_color(i, 255, 255, 255); } } -#endif +# endif switch (get_highest_layer(layer_state)) { case FN1: diff --git a/keyboards/keychron/q5_max/info.json b/keyboards/keychron/q5_max/info.json index e589981b7c..68a48dc949 100644 --- a/keyboards/keychron/q5_max/info.json +++ b/keyboards/keychron/q5_max/info.json @@ -18,7 +18,8 @@ "nkro" : true, "rgb_matrix": true, "raw" : true, - "send_string": true + "send_string": true, + "speculative_hold": true }, "matrix_pins": { "cols": ["C6", "C7", "C8", "A14", "A15", "C10", "C11", "C13", "C14", "C15", "C0", "C1", "C2", "C3", "A0", "A1", "A2", "A3", "B10"],