Merge pull request #246 from honboubao/hold-tap

Improve hold tap (MT, LT, TT) behaviour
This commit is contained in:
Josh Klar 2021-09-25 22:13:23 +00:00 committed by GitHub
commit 8650a6ea7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 195 additions and 125 deletions

View File

@ -270,7 +270,7 @@ class KMKKeyboard:
self._timeouts[timeout_key] = callback self._timeouts[timeout_key] = callback
return timeout_key return timeout_key
def _cancel_timeout(self, timeout_key): def cancel_timeout(self, timeout_key):
if timeout_key in self._timeouts: if timeout_key in self._timeouts:
del self._timeouts[timeout_key] del self._timeouts[timeout_key]

118
kmk/modules/holdtap.py Normal file
View File

@ -0,0 +1,118 @@
from micropython import const
from kmk.modules import Module
class ActivationType:
NOT_ACTIVATED = const(0)
HOLD_TIMEOUT = const(1)
INTERRUPTED = const(2)
class HoldTapKeyState:
def __init__(self, timeout_key, *args, **kwargs):
self.timeout_key = timeout_key
self.args = args
self.kwargs = kwargs
self.activated = ActivationType.NOT_ACTIVATED
class HoldTap(Module):
def __init__(self):
self.key_states = {}
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
'''Before other key down decide to send tap kc down.'''
if self.matrix_detected_press(keyboard):
for key, state in self.key_states.items():
if state.activated == ActivationType.NOT_ACTIVATED:
# press tap because interrupted by other key
self.key_states[key].activated = ActivationType.INTERRUPTED
self.ht_activate_on_interrupt(
key, keyboard, *state.args, **state.kwargs
)
if keyboard.hid_pending:
keyboard._send_hid()
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 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,
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 in self.key_states:
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)
else:
# press and release tap because key released within tap time
self.ht_activate_tap(key, keyboard, *args, **kwargs)
keyboard.set_timeout(
False,
lambda: self.ht_deactivate_tap(key, keyboard, *args, **kwargs),
)
del self.key_states[key]
return keyboard
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
'''When tap time expires activate mod if key is still being pressed.'''
if (
key in self.key_states
and self.key_states[key].activated == ActivationType.NOT_ACTIVATED
):
# press hold because timer expired after tap time
self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
self.ht_activate_hold(key, keyboard, *args, **kwargs)
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
pass
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
pass
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
pass
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
pass
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
self.ht_activate_tap(key, keyboard, *args, **kwargs)
def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs):
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)

View File

@ -1,35 +1,34 @@
'''One layer isn't enough. Adds keys to get to more of them''' '''One layer isn't enough. Adds keys to get to more of them'''
from micropython import const from micropython import const
from supervisor import ticks_ms
from kmk.key_validators import layer_key_validator from kmk.key_validators import layer_key_validator
from kmk.keys import make_argumented_key from kmk.keys import make_argumented_key
from kmk.kmktime import check_deadline from kmk.modules.holdtap import HoldTap
from kmk.modules import Module
class LayerType: class LayerType:
'''Defines layer type values for readability''' '''Defines layer types to be passed on as on_press and on_release kwargs where needed'''
MO = const(0) LT = const(0)
DF = const(1) TT = const(1)
LM = const(2)
LT = const(3)
TG = const(4)
TT = const(5)
class Layers(Module): def curry(fn, *args, **kwargs):
def curried(*fn_args, **fn_kwargs):
merged_args = args + fn_args
merged_kwargs = kwargs.copy()
merged_kwargs.update(fn_kwargs)
return fn(*merged_args, **merged_kwargs)
return curried
class Layers(HoldTap):
'''Gives access to the keys used to enable the layer system''' '''Gives access to the keys used to enable the layer system'''
def __init__(self): def __init__(self):
# Layers # Layers
self.start_time = { super().__init__()
LayerType.LT: None,
LayerType.TG: None,
LayerType.TT: None,
LayerType.LM: None,
}
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
names=('MO',), names=('MO',),
@ -48,8 +47,8 @@ class Layers(Module):
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
names=('LT',), names=('LT',),
on_press=self._lt_pressed, on_press=curry(self.ht_pressed, key_type=LayerType.LT),
on_release=self._lt_released, on_release=curry(self.ht_released, key_type=LayerType.LT),
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed
@ -60,31 +59,10 @@ class Layers(Module):
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
names=('TT',), names=('TT',),
on_press=self._tt_pressed, on_press=curry(self.ht_pressed, key_type=LayerType.TT),
on_release=self._tt_released, on_release=curry(self.ht_released, key_type=LayerType.TT),
) )
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 _df_pressed(self, key, keyboard, *args, **kwargs): def _df_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Switches the default layer Switches the default layer
@ -130,22 +108,6 @@ class Layers(Module):
keyboard.keys_pressed.discard(key.meta.kc) keyboard.keys_pressed.discard(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs) self._mo_released(key, keyboard, *args, **kwargs)
def _lt_pressed(self, key, keyboard, *args, **kwargs):
# Sets the timer start and acts like MO otherwise
self.start_time[LayerType.LT] = ticks_ms()
self._mo_pressed(key, keyboard, *args, **kwargs)
def _lt_released(self, key, keyboard, *args, **kwargs):
# On keyup, check timer, and press key if needed.
if self.start_time[LayerType.LT] and (
check_deadline(ticks_ms(), self.start_time[LayerType.LT], keyboard.tap_time)
):
keyboard.hid_pending = True
keyboard.tap_key(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs)
self.start_time[LayerType.LT] = None
def _tg_pressed(self, key, keyboard, *args, **kwargs): def _tg_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Toggles the layer (enables it if not active, and vise versa) Toggles the layer (enables it if not active, and vise versa)
@ -164,27 +126,44 @@ class Layers(Module):
keyboard.active_layers.clear() keyboard.active_layers.clear()
keyboard.active_layers.insert(0, key.meta.layer) keyboard.active_layers.insert(0, key.meta.layer)
def _tt_pressed(self, key, keyboard, *args, **kwargs): def ht_activate_hold(self, key, keyboard, *args, **kwargs):
''' key_type = kwargs['key_type']
Momentarily activates layer if held, toggles it if tapped repeatedly if key_type == LayerType.LT:
'''
if self.start_time[LayerType.TT] is None:
# Sets the timer start and acts like MO otherwise
self.start_time[LayerType.TT] = ticks_ms()
self._mo_pressed(key, keyboard, *args, **kwargs)
elif check_deadline(
ticks_ms(), self.start_time[LayerType.TT], keyboard.tap_time
):
self.start_time[LayerType.TT] = None
self._tg_pressed(key, keyboard, *args, **kwargs)
return
return
def _tt_released(self, key, keyboard, *args, **kwargs):
if self.start_time[LayerType.TT] is None or not check_deadline(
ticks_ms(), self.start_time[LayerType.TT], keyboard.tap_time
):
# On first press, works like MO. On second press, does nothing unless let up within
# time window, then acts like TG.
self.start_time[LayerType.TT] = None
self._mo_released(key, keyboard, *args, **kwargs) self._mo_released(key, keyboard, *args, **kwargs)
elif key_type == LayerType.TT:
self._tg_pressed(key, keyboard, *args, **kwargs)
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
key_type = kwargs['key_type']
if key_type == LayerType.LT:
self._mo_released(key, keyboard, *args, **kwargs)
elif key_type == LayerType.TT:
self._tg_pressed(key, keyboard, *args, **kwargs)
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
key_type = kwargs['key_type']
if key_type == LayerType.LT:
keyboard.hid_pending = True
keyboard.keys_pressed.add(key.meta.kc)
elif key_type == LayerType.TT:
self._tg_pressed(key, keyboard, *args, **kwargs)
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
key_type = kwargs['key_type']
if key_type == LayerType.LT:
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key.meta.kc)
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
key_type = kwargs['key_type']
if key_type == LayerType.LT:
self.ht_activate_tap(key, keyboard, *args, **kwargs)
elif key_type == LayerType.TT:
self.ht_activate_hold(key, keyboard, *args, **kwargs)
def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs):
key_type = kwargs['key_type']
if key_type == LayerType.LT:
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
elif key_type == LayerType.TT:
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)

View File

@ -1,57 +1,30 @@
from supervisor import ticks_ms
from kmk.key_validators import mod_tap_validator from kmk.key_validators import mod_tap_validator
from kmk.keys import make_argumented_key from kmk.keys import make_argumented_key
from kmk.kmktime import check_deadline from kmk.modules.holdtap import HoldTap
from kmk.modules import Module
class ModTap(Module): class ModTap(HoldTap):
def __init__(self): def __init__(self):
self._mod_tap_timer = None super().__init__()
make_argumented_key( make_argumented_key(
validator=mod_tap_validator, validator=mod_tap_validator,
names=('MT',), names=('MT',),
on_press=self.mt_pressed, on_press=self.ht_pressed,
on_release=self.mt_released, on_release=self.ht_released,
) )
def during_bootup(self, keyboard): def ht_activate_hold(self, key, keyboard, *args, **kwargs):
return keyboard.hid_pending = True
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 mt_pressed(self, key, keyboard, *args, **kwargs):
'''Sets the timer start and acts like a modifier otherwise'''
keyboard.keys_pressed.add(key.meta.mods) keyboard.keys_pressed.add(key.meta.mods)
self._mod_tap_timer = ticks_ms() def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
return keyboard keyboard.hid_pending = True
def mt_released(self, key, keyboard, *args, **kwargs):
'''On keyup, check timer, and press key if needed.'''
keyboard.keys_pressed.discard(key.meta.mods) keyboard.keys_pressed.discard(key.meta.mods)
if self._mod_tap_timer and (
check_deadline(ticks_ms(), self._mod_tap_timer, keyboard.tap_time)
):
keyboard.hid_pending = True
keyboard.tap_key(key.meta.kc)
self._mod_tap_timer = None def ht_activate_tap(self, key, keyboard, *args, **kwargs):
return keyboard keyboard.hid_pending = True
keyboard.keys_pressed.add(key.meta.kc)
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key.meta.kc)