diff --git a/kmk/kmk_keyboard.py b/kmk/kmk_keyboard.py index 60159eb..e223d0e 100644 --- a/kmk/kmk_keyboard.py +++ b/kmk/kmk_keyboard.py @@ -270,7 +270,7 @@ class KMKKeyboard: self._timeouts[timeout_key] = callback return timeout_key - def _cancel_timeout(self, timeout_key): + def cancel_timeout(self, timeout_key): if timeout_key in self._timeouts: del self._timeouts[timeout_key] diff --git a/kmk/modules/holdtap.py b/kmk/modules/holdtap.py new file mode 100644 index 0000000..77dd4e9 --- /dev/null +++ b/kmk/modules/holdtap.py @@ -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) diff --git a/kmk/modules/layers.py b/kmk/modules/layers.py index 1d7a281..c369476 100644 --- a/kmk/modules/layers.py +++ b/kmk/modules/layers.py @@ -1,35 +1,34 @@ '''One layer isn't enough. Adds keys to get to more of them''' from micropython import const -from supervisor import ticks_ms from kmk.key_validators import layer_key_validator from kmk.keys import make_argumented_key -from kmk.kmktime import check_deadline -from kmk.modules import Module +from kmk.modules.holdtap import HoldTap 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) - DF = const(1) - LM = const(2) - LT = const(3) - TG = const(4) - TT = const(5) + LT = const(0) + TT = const(1) -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''' def __init__(self): # Layers - self.start_time = { - LayerType.LT: None, - LayerType.TG: None, - LayerType.TT: None, - LayerType.LM: None, - } + super().__init__() make_argumented_key( validator=layer_key_validator, names=('MO',), @@ -48,8 +47,8 @@ class Layers(Module): make_argumented_key( validator=layer_key_validator, names=('LT',), - on_press=self._lt_pressed, - on_release=self._lt_released, + on_press=curry(self.ht_pressed, key_type=LayerType.LT), + on_release=curry(self.ht_released, key_type=LayerType.LT), ) make_argumented_key( validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed @@ -60,31 +59,10 @@ class Layers(Module): make_argumented_key( validator=layer_key_validator, names=('TT',), - on_press=self._tt_pressed, - on_release=self._tt_released, + on_press=curry(self.ht_pressed, key_type=LayerType.TT), + 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): ''' Switches the default layer @@ -130,22 +108,6 @@ class Layers(Module): keyboard.keys_pressed.discard(key.meta.kc) 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): ''' 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.insert(0, key.meta.layer) - def _tt_pressed(self, key, keyboard, *args, **kwargs): - ''' - Momentarily activates layer if held, toggles it if tapped repeatedly - ''' - 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 + def ht_activate_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_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) diff --git a/kmk/modules/modtap.py b/kmk/modules/modtap.py index 76e9755..6b31a73 100644 --- a/kmk/modules/modtap.py +++ b/kmk/modules/modtap.py @@ -1,57 +1,30 @@ -from supervisor import ticks_ms - from kmk.key_validators import mod_tap_validator from kmk.keys import make_argumented_key -from kmk.kmktime import check_deadline -from kmk.modules import Module +from kmk.modules.holdtap import HoldTap -class ModTap(Module): +class ModTap(HoldTap): def __init__(self): - self._mod_tap_timer = None + super().__init__() make_argumented_key( validator=mod_tap_validator, names=('MT',), - on_press=self.mt_pressed, - on_release=self.mt_released, + 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 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''' + def ht_activate_hold(self, key, keyboard, *args, **kwargs): + keyboard.hid_pending = True keyboard.keys_pressed.add(key.meta.mods) - self._mod_tap_timer = ticks_ms() - return keyboard - - def mt_released(self, key, keyboard, *args, **kwargs): - '''On keyup, check timer, and press key if needed.''' + def ht_deactivate_hold(self, key, keyboard, *args, **kwargs): + keyboard.hid_pending = True 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 - return keyboard + def ht_activate_tap(self, key, keyboard, *args, **kwargs): + 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)