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.
123 lines
4.2 KiB
Python
123 lines
4.2 KiB
Python
from kmk.keys import KC, make_argumented_key
|
|
from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta
|
|
|
|
|
|
class TapDanceKeyMeta:
|
|
def __init__(self, *keys, tap_time=None):
|
|
'''
|
|
Any key in the tapdance sequence that is not already a holdtap
|
|
key gets converted to a holdtap key with identical tap and hold
|
|
meta attributes.
|
|
'''
|
|
self.tap_time = tap_time
|
|
self.keys = []
|
|
|
|
for key in keys:
|
|
if not isinstance(key.meta, HoldTapKeyMeta):
|
|
ht_key = KC.HT(
|
|
tap=key,
|
|
hold=key,
|
|
prefer_hold=False,
|
|
tap_interrupted=False,
|
|
tap_time=self.tap_time,
|
|
)
|
|
self.keys.append(ht_key)
|
|
else:
|
|
self.keys.append(key)
|
|
self.keys = tuple(self.keys)
|
|
|
|
|
|
class TapDance(HoldTap):
|
|
def __init__(self):
|
|
super().__init__()
|
|
make_argumented_key(
|
|
validator=TapDanceKeyMeta,
|
|
names=('TD',),
|
|
on_press=self.td_pressed,
|
|
on_release=self.td_released,
|
|
)
|
|
|
|
self.td_counts = {}
|
|
|
|
def process_key(self, keyboard, key, is_pressed, int_coord):
|
|
if isinstance(key.meta, TapDanceKeyMeta):
|
|
if key in self.td_counts:
|
|
return key
|
|
|
|
for _key, state in self.key_states.copy().items():
|
|
if state.activated == ActivationType.RELEASED:
|
|
keyboard.cancel_timeout(state.timeout_key)
|
|
self.ht_activate_tap(_key, keyboard)
|
|
self.send_key_buffer(keyboard)
|
|
self.ht_deactivate_tap(_key, keyboard)
|
|
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
|
key = None
|
|
|
|
del self.key_states[_key]
|
|
del self.td_counts[state.tap_dance]
|
|
|
|
key = super().process_key(keyboard, key, is_pressed, int_coord)
|
|
|
|
return key
|
|
|
|
def td_pressed(self, key, keyboard, *args, **kwargs):
|
|
# active tap dance
|
|
if key in self.td_counts:
|
|
count = self.td_counts[key]
|
|
kc = key.meta.keys[count]
|
|
keyboard.cancel_timeout(self.key_states[kc].timeout_key)
|
|
|
|
count += 1
|
|
|
|
# Tap dance reached the end of the list: send last tap in sequence
|
|
# and start from the beginning.
|
|
if count >= len(key.meta.keys):
|
|
self.key_states[kc].activated = ActivationType.RELEASED
|
|
self.on_tap_time_expired(kc, keyboard)
|
|
count = 0
|
|
else:
|
|
del self.key_states[kc]
|
|
|
|
# new tap dance
|
|
else:
|
|
count = 0
|
|
|
|
current_key = key.meta.keys[count]
|
|
|
|
self.ht_pressed(current_key, keyboard, *args, **kwargs)
|
|
self.td_counts[key] = count
|
|
|
|
# Add the active tap dance to key_states; `on_tap_time_expired` needs
|
|
# the back-reference.
|
|
self.key_states[current_key].tap_dance = key
|
|
|
|
def td_released(self, key, keyboard, *args, **kwargs):
|
|
try:
|
|
kc = key.meta.keys[self.td_counts[key]]
|
|
except KeyError:
|
|
return
|
|
state = self.key_states[kc]
|
|
if state.activated == ActivationType.HOLD_TIMEOUT:
|
|
# release hold
|
|
self.ht_deactivate_hold(kc, keyboard, *args, **kwargs)
|
|
del self.key_states[kc]
|
|
del self.td_counts[key]
|
|
elif state.activated == ActivationType.INTERRUPTED:
|
|
# release tap
|
|
self.ht_deactivate_on_interrupt(kc, keyboard, *args, **kwargs)
|
|
del self.key_states[kc]
|
|
del self.td_counts[key]
|
|
else:
|
|
# keep counting
|
|
state.activated = ActivationType.RELEASED
|
|
|
|
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
|
|
# Note: the `key` argument is the current holdtap key in the sequence,
|
|
# not the tapdance key.
|
|
state = self.key_states[key]
|
|
if state.activated == ActivationType.RELEASED:
|
|
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
|
self.send_key_buffer(keyboard)
|
|
del self.td_counts[state.tap_dance]
|
|
super().on_tap_time_expired(key, keyboard, *args, **kwargs)
|