17f2961c0b
Instead of handling resumed key events in a deep stack, buffer them until the next main loop iteration. New resume events that may be emitted during handling of old resumes are prepended to that buffer, i.e. take precedence over events that happen deeper into the buffer/event stack. Logical replay order is thus preserved.
252 lines
8.5 KiB
Python
252 lines
8.5 KiB
Python
from micropython import const
|
|
|
|
from kmk.keys import KC, make_argumented_key
|
|
from kmk.modules import Module
|
|
from kmk.utils import Debug
|
|
|
|
debug = Debug(__name__)
|
|
|
|
|
|
class ActivationType:
|
|
PRESSED = const(0)
|
|
RELEASED = const(1)
|
|
HOLD_TIMEOUT = const(2)
|
|
INTERRUPTED = const(3)
|
|
REPEAT = const(4)
|
|
|
|
|
|
class HoldTapKeyState:
|
|
def __init__(self, timeout_key, *args, **kwargs):
|
|
self.timeout_key = timeout_key
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
self.activated = ActivationType.PRESSED
|
|
|
|
|
|
class HoldTapKeyMeta:
|
|
def __init__(
|
|
self,
|
|
tap,
|
|
hold,
|
|
prefer_hold=True,
|
|
tap_interrupted=False,
|
|
tap_time=None,
|
|
repeat=False,
|
|
):
|
|
self.tap = tap
|
|
self.hold = hold
|
|
self.prefer_hold = prefer_hold
|
|
self.tap_interrupted = tap_interrupted
|
|
self.tap_time = tap_time
|
|
self.repeat = repeat
|
|
|
|
|
|
class HoldTap(Module):
|
|
tap_time = 300
|
|
|
|
def __init__(self):
|
|
self.key_buffer = []
|
|
self.key_states = {}
|
|
if not KC.get('HT'):
|
|
make_argumented_key(
|
|
validator=HoldTapKeyMeta,
|
|
names=('HT',),
|
|
on_press=self.ht_pressed,
|
|
on_release=self.ht_released,
|
|
)
|
|
|
|
def during_bootup(self, keyboard):
|
|
return
|
|
|
|
def before_matrix_scan(self, keyboard):
|
|
return
|
|
|
|
def after_matrix_scan(self, keyboard):
|
|
return
|
|
|
|
def process_key(self, keyboard, key, is_pressed, int_coord):
|
|
'''Handle holdtap being interrupted by another key press/release.'''
|
|
current_key = key
|
|
send_buffer = False
|
|
append_buffer = False
|
|
|
|
for key, state in self.key_states.items():
|
|
if key == current_key:
|
|
continue
|
|
if state.activated != ActivationType.PRESSED:
|
|
continue
|
|
|
|
# holdtap is interrupted by another key event.
|
|
if (is_pressed and not key.meta.tap_interrupted) or (
|
|
not is_pressed and key.meta.tap_interrupted and self.key_buffer
|
|
):
|
|
|
|
keyboard.cancel_timeout(state.timeout_key)
|
|
self.key_states[key].activated = ActivationType.INTERRUPTED
|
|
self.ht_activate_on_interrupt(
|
|
key, keyboard, *state.args, **state.kwargs
|
|
)
|
|
send_buffer = True
|
|
|
|
# if interrupt on release: store interrupting keys until one of them
|
|
# is released.
|
|
if (
|
|
key.meta.tap_interrupted
|
|
and is_pressed
|
|
and not isinstance(current_key.meta, HoldTapKeyMeta)
|
|
):
|
|
append_buffer = True
|
|
|
|
# apply changes with 'side-effects' on key_states or the loop behaviour
|
|
# outside the loop.
|
|
if append_buffer:
|
|
self.key_buffer.append((int_coord, current_key, is_pressed))
|
|
current_key = None
|
|
|
|
elif send_buffer:
|
|
self.send_key_buffer(keyboard)
|
|
keyboard.resume_process_key(self, current_key, is_pressed, int_coord)
|
|
current_key = None
|
|
|
|
return current_key
|
|
|
|
def before_hid_send(self, keyboard):
|
|
return
|
|
|
|
def after_hid_send(self, keyboard):
|
|
return
|
|
|
|
def on_powersave_enable(self, keyboard):
|
|
return
|
|
|
|
def on_powersave_disable(self, keyboard):
|
|
return
|
|
|
|
def ht_pressed(self, key, keyboard, *args, **kwargs):
|
|
'''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
|
|
if key in self.key_states:
|
|
state = self.key_states[key]
|
|
keyboard.cancel_timeout(self.key_states[key].timeout_key)
|
|
|
|
if state.activated == ActivationType.RELEASED:
|
|
state.activated = ActivationType.REPEAT
|
|
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
|
elif state.activated == ActivationType.HOLD_TIMEOUT:
|
|
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
|
elif state.activated == ActivationType.INTERRUPTED:
|
|
self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs)
|
|
return
|
|
|
|
if key.meta.tap_time is None:
|
|
tap_time = self.tap_time
|
|
else:
|
|
tap_time = key.meta.tap_time
|
|
timeout_key = keyboard.set_timeout(
|
|
tap_time,
|
|
lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs),
|
|
)
|
|
self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs)
|
|
return keyboard
|
|
|
|
def ht_released(self, key, keyboard, *args, **kwargs):
|
|
'''On keyup, release mod or tap key.'''
|
|
if key not in self.key_states:
|
|
return keyboard
|
|
|
|
state = self.key_states[key]
|
|
keyboard.cancel_timeout(state.timeout_key)
|
|
|
|
if state.activated == ActivationType.HOLD_TIMEOUT:
|
|
# release hold
|
|
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
|
elif state.activated == ActivationType.INTERRUPTED:
|
|
# release tap
|
|
self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs)
|
|
elif state.activated == ActivationType.PRESSED:
|
|
# press and release tap because key released within tap time
|
|
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
|
self.send_key_buffer(keyboard)
|
|
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
|
state.activated = ActivationType.RELEASED
|
|
self.send_key_buffer(keyboard)
|
|
elif state.activated == ActivationType.REPEAT:
|
|
state.activated = ActivationType.RELEASED
|
|
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
|
|
|
# don't delete the key state right now in this case
|
|
tap_time = 0
|
|
if key.meta.repeat:
|
|
if key.meta.tap_time is None:
|
|
tap_time = self.tap_time
|
|
else:
|
|
tap_time = key.meta.tap_time
|
|
state.timeout_key = keyboard.set_timeout(
|
|
tap_time, lambda: self.key_states.pop(key)
|
|
)
|
|
|
|
return keyboard
|
|
|
|
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
|
|
'''When tap time expires activate hold if key is still being pressed.
|
|
Remove key if ActivationType is RELEASED.'''
|
|
try:
|
|
state = self.key_states[key]
|
|
except KeyError:
|
|
if debug.enabled:
|
|
debug(f'on_tap_time_expired: no such key {key}')
|
|
return
|
|
|
|
if self.key_states[key].activated == ActivationType.PRESSED:
|
|
# press hold because timer expired after tap time
|
|
self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
|
|
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
|
self.send_key_buffer(keyboard)
|
|
elif state.activated == ActivationType.RELEASED:
|
|
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
|
del self.key_states[key]
|
|
|
|
def send_key_buffer(self, keyboard):
|
|
if not self.key_buffer:
|
|
return
|
|
|
|
for (int_coord, key, is_pressed) in self.key_buffer:
|
|
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
|
|
|
self.key_buffer.clear()
|
|
|
|
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_activate_hold')
|
|
keyboard.resume_process_key(self, key.meta.hold, True)
|
|
|
|
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_deactivate_hold')
|
|
keyboard.resume_process_key(self, key.meta.hold, False)
|
|
|
|
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_activate_tap')
|
|
keyboard.resume_process_key(self, key.meta.tap, True)
|
|
|
|
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_deactivate_tap')
|
|
keyboard.resume_process_key(self, key.meta.tap, False)
|
|
|
|
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_activate_on_interrupt')
|
|
if key.meta.prefer_hold:
|
|
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
|
else:
|
|
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
|
|
|
def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs):
|
|
if debug.enabled:
|
|
debug('ht_deactivate_on_interrupt')
|
|
if key.meta.prefer_hold:
|
|
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
|
else:
|
|
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|