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

Achordion fix 2345 #74

Merged
merged 2 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 161 additions & 69 deletions keyboards/svalboard/keymaps/features/achordion.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022-2023 Google LLC
// Copyright 2022-2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,6 +40,7 @@ static bool pressed_another_key_before_release = false;

#ifdef ACHORDION_STREAK
// Timer for typing streak
static bool streaking = false;
static uint16_t streak_timer = 0;
#else
// When disabled, is_streak is never true
Expand All @@ -63,24 +64,88 @@ enum {
};
static uint8_t achordion_state = STATE_RELEASED;

#ifdef ACHORDION_STREAK
static void update_streak_timer(uint16_t keycode, keyrecord_t* record) {
if (achordion_streak_continue(keycode)) {
streaking = true;
streak_timer = record->event.time;
} else {
streaking = false;
streak_timer = 0;
}
}
#endif

// Presses or releases eager_mods through process_action(), which skips the
// usual event handling pipeline. The action is considered as a mod-tap hold or
// release, with Retro Tapping if enabled.
static void process_eager_mods_action(void) {
action_t action;
action.code = ACTION_MODS_TAP_KEY(
eager_mods, QK_MOD_TAP_GET_TAP_KEYCODE(tap_hold_keycode));
process_action(&tap_hold_record, action);
}

// Calls `process_record()` with state set to RECURSING.
static void recursively_process_record(keyrecord_t* record, uint8_t state) {
achordion_state = STATE_RECURSING;
#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
int8_t mouse_key_tracker = get_auto_mouse_key_tracker();
#endif
process_record(record);
#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
set_auto_mouse_key_tracker(mouse_key_tracker);
#endif
achordion_state = state;
}

// Clears eagerly-applied mods.
static void clear_eager_mods(void) {
unregister_mods(eager_mods);
eager_mods = 0;
}

// Sends hold press event and settles the active tap-hold key as held.
static void settle_as_hold(void) {
clear_eager_mods();
// Create hold press event.
recursively_process_record(&tap_hold_record, STATE_HOLDING);
if (eager_mods) {
// If eager mods are being applied, nothing needs to be done besides
// updating the state.
dprintln("Achordion: Settled eager mod as hold.");
achordion_state = STATE_HOLDING;
} else {
// Create hold press event.
dprintln("Achordion: Plumbing hold press.");
recursively_process_record(&tap_hold_record, STATE_HOLDING);
}
}

// Sends tap press and release and settles the active tap-hold key as tapped.
static void settle_as_tap(void) {
if (eager_mods) { // Clear eager mods if set.
#if defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
#ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE
neutralize_flashing_modifiers(get_mods());
#endif // DUMMY_MOD_NEUTRALIZER_KEYCODE
#endif // defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
tap_hold_record.event.pressed = false;
// To avoid falsely triggering Retro Tapping, process eager mods release as
// a regular mods release rather than a mod-tap release.
action_t action;
action.code = ACTION_MODS(eager_mods);
process_action(&tap_hold_record, action);
eager_mods = 0;
}

dprintln("Achordion: Plumbing tap press.");
tap_hold_record.event.pressed = true;
tap_hold_record.tap.count = 1; // Revise event as a tap.
tap_hold_record.tap.interrupted = true;
// Plumb tap press event.
recursively_process_record(&tap_hold_record, STATE_TAPPING);

send_keyboard_report();
#if TAP_CODE_DELAY > 0
wait_ms(TAP_CODE_DELAY);
#endif // TAP_CODE_DELAY > 0

dprintln("Achordion: Plumbing tap release.");
tap_hold_record.event.pressed = false;
// Plumb tap release event.
recursively_process_record(&tap_hold_record, STATE_TAPPING);
}

bool process_achordion(uint16_t keycode, keyrecord_t* record) {
Expand All @@ -89,25 +154,13 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {
return true;
}

// If this is a keypress and if the key is different than the tap-hold key,
// this information is saved to a flag to be processed later when the tap-hold
// key is released.
if (!pressed_another_key_before_release && record->event.pressed &&
tap_hold_keycode != KC_NO && tap_hold_keycode != keycode) {
pressed_another_key_before_release = true;
}

// Determine whether the current event is for a mod-tap or layer-tap key.
const bool is_mt = IS_QK_MOD_TAP(keycode);
const bool is_tap_hold = is_mt || IS_QK_LAYER_TAP(keycode);
// Check that this is a normal key event, don't act on combos.
#ifdef IS_KEYEVENT
const bool is_key_event = IS_KEYEVENT(record->event);
#else
const bool is_key_event =
(record->event.key.row < 254 && record->event.key.col < 254);
#endif

// Event while no tap-hold key is active.
if (achordion_state == STATE_RELEASED) {
if (is_tap_hold && record->tap.count == 0 && record->event.pressed &&
is_key_event) {
Expand All @@ -119,12 +172,21 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {
tap_hold_keycode = keycode;
tap_hold_record = *record;
hold_timer = record->event.time + timeout;
pressed_another_key_before_release = false;
eager_mods = 0;

if (is_mt) { // Apply mods immediately if they are "eager."
uint8_t mod = mod_config(QK_MOD_TAP_GET_MODS(tap_hold_keycode));
if (achordion_eager_mod(mod)) {
eager_mods = ((mod & 0x10) == 0) ? mod : (mod << 4);
register_mods(eager_mods);
const uint8_t mod = mod_config(QK_MOD_TAP_GET_MODS(keycode));
if (
#if defined(CAPS_WORD_ENABLE) && defined(CAPS_WORD_INVERT_ON_SHIFT)
// Since eager mods bypass normal event handling, eager Shift does
// not work with CAPS_WORD_INVERT_ON_SHIFT. So if this option is
// enabled, we don't apply Shift eagerly when Caps Word is on.
!(is_caps_word_on() && (mod & MOD_LSFT) != 0) &&
#endif // defined(CAPS_WORD_ENABLE) && defined(CAPS_WORD_INVERT_ON_SHIFT)
achordion_eager_mod(mod)) {
eager_mods = mod;
process_eager_mods_action();
}
}

Expand All @@ -135,46 +197,48 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {
}

#ifdef ACHORDION_STREAK
streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
update_streak_timer(keycode, record);
#endif
return true; // Otherwise, continue with default handling.
} else if (record->event.pressed && tap_hold_keycode != keycode) {
// Track whether another key was pressed while using a tap-hold key.
pressed_another_key_before_release = true;
}

// Release of the active tap-hold key.
if (keycode == tap_hold_keycode && !record->event.pressed) {
// The active tap-hold key is being released.
if (achordion_state == STATE_HOLDING) {
if (eager_mods) {
dprintln("Achordion: Key released. Clearing eager mods.");
tap_hold_record.event.pressed = false;
process_eager_mods_action();
} else if (achordion_state == STATE_HOLDING) {
dprintln("Achordion: Key released. Plumbing hold release.");
tap_hold_record.event.pressed = false;
// Plumb hold release event.
recursively_process_record(&tap_hold_record, STATE_RELEASED);
} else if (!pressed_another_key_before_release) {
// No other key was pressed between the press and release of the tap-hold
// key, simulate a hold and then a release without waiting for Achordion
// timeout to end.
dprintln("Achordion: Key released. Simulating hold and release.");
settle_as_hold();
// key, plumb a hold press and then a release.
dprintln("Achordion: Key released. Plumbing hold press and release.");
recursively_process_record(&tap_hold_record, STATE_HOLDING);
tap_hold_record.event.pressed = false;
// Plumb hold release event.
recursively_process_record(&tap_hold_record, STATE_RELEASED);
} else {
dprintf("Achordion: Key released.%s\n",
eager_mods ? " Clearing eager mods." : "");
if (is_mt) {
clear_eager_mods();
}
dprintln("Achordion: Key released.");
}

achordion_state = STATE_RELEASED;
// The tap-hold key is released, clear the related keycode and the flag.
tap_hold_keycode = KC_NO;
pressed_another_key_before_release = false;
return false;
}

if (achordion_state == STATE_UNSETTLED && record->event.pressed) {
#ifdef ACHORDION_STREAK
const bool is_streak = (streak_timer != 0);
streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
const uint16_t s_timeout =
achordion_streak_chord_timeout(tap_hold_keycode, keycode);
const bool is_streak =
streaking && s_timeout &&
!timer_expired(record->event.time, (streak_timer + s_timeout));
#endif

// Press event occurred on a key other than the active tap-hold key.
Expand All @@ -189,9 +253,10 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {
// events back into the handling pipeline so that QMK features and other
// user code can see them. This is done by calling `process_record()`, which
// in turn calls most handlers including `process_record_user()`.
if (!is_streak && (!is_key_event || (is_tap_hold && record->tap.count == 0) ||
achordion_chord(tap_hold_keycode, &tap_hold_record, keycode, record))) {
dprintln("Achordion: Plumbing hold press.");
if (!is_streak &&
(!is_key_event || (is_tap_hold && record->tap.count == 0) ||
achordion_chord(tap_hold_keycode, &tap_hold_record, keycode,
record))) {
settle_as_hold();

#ifdef REPEAT_KEY_ENABLE
Expand All @@ -205,23 +270,23 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {
}
#endif // REPEAT_KEY_ENABLE
} else {
clear_eager_mods(); // Clear in case eager mods were set.
settle_as_tap();

dprintln("Achordion: Plumbing tap press.");
tap_hold_record.tap.count = 1; // Revise event as a tap.
tap_hold_record.tap.interrupted = true;
// Plumb tap press event.
recursively_process_record(&tap_hold_record, STATE_TAPPING);

send_keyboard_report();
#if TAP_CODE_DELAY > 0
wait_ms(TAP_CODE_DELAY);
#endif // TAP_CODE_DELAY > 0

dprintln("Achordion: Plumbing tap release.");
tap_hold_record.event.pressed = false;
// Plumb tap release event.
recursively_process_record(&tap_hold_record, STATE_TAPPING);
#ifdef ACHORDION_STREAK
update_streak_timer(keycode, record);
if (is_streak && is_key_event && is_tap_hold && record->tap.count == 0) {
// If we are in a streak and resolved the current tap-hold key as a tap
// consider the next tap-hold key as active to be resolved next.
update_streak_timer(tap_hold_keycode, &tap_hold_record);
const uint16_t timeout = achordion_timeout(keycode);
tap_hold_keycode = keycode;
tap_hold_record = *record;
hold_timer = record->event.time + timeout;
achordion_state = STATE_UNSETTLED;
pressed_another_key_before_release = false;
return false;
}
#endif
}

recursively_process_record(record, achordion_state); // Re-process event.
Expand All @@ -230,21 +295,22 @@ bool process_achordion(uint16_t keycode, keyrecord_t* record) {

#ifdef ACHORDION_STREAK
// update idle timer on regular keys event
streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
update_streak_timer(keycode, record);
#endif
return true;
}

void achordion_task(void) {
if (achordion_state == STATE_UNSETTLED &&
timer_expired(timer_read(), hold_timer)) {
dprintln("Achordion: Timeout. Plumbing hold press.");
settle_as_hold(); // Timeout expired, settle the key as held.
}

#ifdef ACHORDION_STREAK
if (streak_timer && timer_expired(timer_read(), streak_timer)) {
streak_timer = 0; // Expired.
#define MAX_STREAK_TIMEOUT 800
if (streaking &&
timer_expired(timer_read(), (streak_timer + MAX_STREAK_TIMEOUT))) {
streaking = false; // Expired.
}
#endif
}
Expand Down Expand Up @@ -285,8 +351,34 @@ __attribute__((weak)) bool achordion_eager_mod(uint8_t mod) {
}

#ifdef ACHORDION_STREAK
__attribute__((weak)) uint16_t achordion_streak_timeout(uint16_t tap_hold_keycode) {
return 100; // Default of 100 ms.
__attribute__((weak)) bool achordion_streak_continue(uint16_t keycode) {
// If any mods other than shift or AltGr are held, don't continue the streak
if (get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) return false;
// This function doesn't get called for holds, so convert to tap version of
// keycodes
if (IS_QK_MOD_TAP(keycode)) keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
if (IS_QK_LAYER_TAP(keycode)) keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
// Regular letters and punctuation continue the streak.
if (keycode >= KC_A && keycode <= KC_Z) return true;
switch (keycode) {
case KC_DOT:
case KC_COMMA:
case KC_QUOTE:
case KC_SPACE:
return true;
}
// All other keys end the streak
return false;
}

__attribute__((weak)) uint16_t achordion_streak_chord_timeout(
uint16_t tap_hold_keycode, uint16_t next_keycode) {
return achordion_streak_timeout(tap_hold_keycode);
}

__attribute__((weak)) uint16_t
achordion_streak_timeout(uint16_t tap_hold_keycode) {
return 200;
}
#endif

Expand Down
Loading