make TapDance a module (#281)
* extract tapdance logic into a module * clean out old tapdance code * canonicalize key variable names * split _process_tap_dance into td_pressed and td_released * implement consistent argument order * update documentation * implement Module.process_key for key interception and modification * fix tapdance realesing instead of pressing * fix: default parameters in key handler * cleanup holdtap * add error handling to modules process_key * fix: key released too late Tapped keys didn't release on a "key released" event, but waited for a timeout. Resulted in, for example, modifiers applying to keys after the modifier was released. * fix lint/formatting * fix tap_time reference in modtap + minimal documentation * fix lint
This commit is contained in:
@@ -38,3 +38,6 @@ class Module:
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed):
|
||||
return key
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from micropython import const
|
||||
|
||||
from kmk.modules import Module
|
||||
from kmk.types import ModTapKeyMeta
|
||||
|
||||
|
||||
class ActivationType:
|
||||
@@ -18,6 +19,8 @@ class HoldTapKeyState:
|
||||
|
||||
|
||||
class HoldTap(Module):
|
||||
tap_time = 300
|
||||
|
||||
def __init__(self):
|
||||
self.key_states = {}
|
||||
|
||||
@@ -28,8 +31,12 @@ class HoldTap(Module):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed):
|
||||
'''Before other key down decide to send tap kc down.'''
|
||||
if self.matrix_detected_press(keyboard):
|
||||
current_key = key
|
||||
if is_pressed and not isinstance(key.meta, ModTapKeyMeta):
|
||||
for key, state in self.key_states.items():
|
||||
if state.activated == ActivationType.NOT_ACTIVATED:
|
||||
# press tap because interrupted by other key
|
||||
@@ -39,7 +46,7 @@ class HoldTap(Module):
|
||||
)
|
||||
if keyboard.hid_pending:
|
||||
keyboard._send_hid()
|
||||
return
|
||||
return current_key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
@@ -53,16 +60,10 @@ class HoldTap(Module):
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def matrix_detected_press(self, keyboard):
|
||||
return (keyboard.matrix_update is not None and keyboard.matrix_update[2]) or (
|
||||
keyboard.secondary_matrix_update is not None
|
||||
and keyboard.secondary_matrix_update[2]
|
||||
)
|
||||
|
||||
def ht_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''Do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
|
||||
timeout_key = keyboard.set_timeout(
|
||||
keyboard.tap_time,
|
||||
self.tap_time,
|
||||
lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs),
|
||||
)
|
||||
self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs)
|
||||
|
124
kmk/modules/tapdance.py
Normal file
124
kmk/modules/tapdance.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from kmk.key_validators import tap_dance_key_validator
|
||||
from kmk.keys import make_argumented_key
|
||||
from kmk.modules import Module
|
||||
from kmk.types import TapDanceKeyMeta
|
||||
|
||||
|
||||
class TapDance(Module):
|
||||
# User-configurable
|
||||
tap_time = 300
|
||||
|
||||
# Internal State
|
||||
_tapping = False
|
||||
_tap_dance_counts = {}
|
||||
_tap_timeout = None
|
||||
_tap_side_effects = {}
|
||||
|
||||
def __init__(self):
|
||||
make_argumented_key(
|
||||
validator=tap_dance_key_validator,
|
||||
names=('TAP_DANCE', 'TD'),
|
||||
on_press=self.td_pressed,
|
||||
on_release=self.td_released,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
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 process_key(self, keyboard, key, is_pressed):
|
||||
if self._tapping and is_pressed and not isinstance(key.meta, TapDanceKeyMeta):
|
||||
for k, v in self._tap_dance_counts.items():
|
||||
if v:
|
||||
self._end_tap_dance(k, keyboard, hold=True)
|
||||
keyboard.hid_pending = True
|
||||
keyboard._send_hid()
|
||||
keyboard.set_timeout(
|
||||
False, lambda: keyboard.process_key(key, is_pressed, None, None)
|
||||
)
|
||||
return None
|
||||
|
||||
return key
|
||||
|
||||
def td_pressed(self, key, keyboard, *args, **kwargs):
|
||||
if key not in self._tap_dance_counts or not self._tap_dance_counts[key]:
|
||||
self._tap_dance_counts[key] = 1
|
||||
self._tapping = True
|
||||
else:
|
||||
keyboard.cancel_timeout(self._tap_timeout)
|
||||
self._tap_dance_counts[key] += 1
|
||||
|
||||
if key not in self._tap_side_effects:
|
||||
self._tap_side_effects[key] = None
|
||||
|
||||
self._tap_timeout = keyboard.set_timeout(
|
||||
self.tap_time, lambda: self._end_tap_dance(key, keyboard, hold=True)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def td_released(self, key, keyboard, *args, **kwargs):
|
||||
has_side_effects = self._tap_side_effects[key] is not None
|
||||
hit_max_defined_taps = self._tap_dance_counts[key] == len(key.meta.codes)
|
||||
|
||||
keyboard.cancel_timeout(self._tap_timeout)
|
||||
if has_side_effects or hit_max_defined_taps:
|
||||
self._end_tap_dance(key, keyboard)
|
||||
else:
|
||||
self._tap_timeout = keyboard.set_timeout(
|
||||
self.tap_time, lambda: self._end_tap_dance(key, keyboard)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _end_tap_dance(self, key, keyboard, hold=False):
|
||||
v = self._tap_dance_counts[key] - 1
|
||||
|
||||
if v < 0:
|
||||
return self
|
||||
|
||||
if key in keyboard.keys_pressed:
|
||||
key_to_press = key.meta.codes[v]
|
||||
keyboard.add_key(key_to_press)
|
||||
self._tap_side_effects[key] = key_to_press
|
||||
elif self._tap_side_effects[key]:
|
||||
keyboard.remove_key(self._tap_side_effects[key])
|
||||
self._tap_side_effects[key] = None
|
||||
self._cleanup_tap_dance(key)
|
||||
elif hold is False:
|
||||
if key.meta.codes[v] in keyboard.keys_pressed:
|
||||
keyboard.remove_key(key.meta.codes[v])
|
||||
else:
|
||||
keyboard.tap_key(key.meta.codes[v])
|
||||
self._cleanup_tap_dance(key)
|
||||
else:
|
||||
key_to_press = key.meta.codes[v]
|
||||
keyboard.add_key(key_to_press)
|
||||
self._tap_side_effects[key] = key_to_press
|
||||
self._tapping = 0
|
||||
|
||||
keyboard.hid_pending = True
|
||||
|
||||
return self
|
||||
|
||||
def _cleanup_tap_dance(self, key):
|
||||
self._tap_dance_counts[key] = 0
|
||||
self._tapping = any(count > 0 for count in self._tap_dance_counts.values())
|
||||
return self
|
Reference in New Issue
Block a user