diff --git a/kmk/kmk_keyboard.py b/kmk/kmk_keyboard.py index acb199c..b292d0c 100644 --- a/kmk/kmk_keyboard.py +++ b/kmk/kmk_keyboard.py @@ -258,7 +258,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/layers.py b/kmk/modules/layers.py index 416bc27..3586299 100644 --- a/kmk/modules/layers.py +++ b/kmk/modules/layers.py @@ -1,34 +1,25 @@ '''One layer isn't enough. Adds keys to get to more of them''' -from micropython import const - from kmk.key_validators import layer_key_validator from kmk.keys import make_argumented_key -from kmk.kmktime import accurate_ticks, accurate_ticks_diff -from kmk.modules import Module +from kmk.modules.modtap import HoldTap -class LayerType: - '''Defines layer type values for readability''' +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) - MO = const(0) - DF = const(1) - LM = const(2) - LT = const(3) - TG = const(4) - TT = const(5) + return curried -class Layers(Module): +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',), @@ -47,8 +38,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='LT'), + on_release=curry(self.ht_released, key_type='LT'), ) make_argumented_key( validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed @@ -59,31 +50,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='TT'), + on_release=curry(self.ht_released, key_type='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 @@ -129,24 +99,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] = accurate_ticks() - 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 ( - accurate_ticks_diff( - accurate_ticks(), 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) @@ -165,27 +117,36 @@ 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] = accurate_ticks() - self._mo_pressed(key, keyboard, *args, **kwargs) - elif accurate_ticks_diff( - accurate_ticks(), self.start_time[LayerType.TT], keyboard.tap_time - ): - self.start_time[LayerType.TT] = None - self._tg_pressed(key, keyboard, *args, **kwargs) - return - return + def ht_activate_hold(self, key, keyboard, *args, **kwargs): + self._mo_pressed(key, keyboard, *args, **kwargs) - def _tt_released(self, key, keyboard, *args, **kwargs): - if self.start_time[LayerType.TT] is None or not accurate_ticks_diff( - accurate_ticks(), 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) + def ht_deactivate_hold(self, key, keyboard, *args, **kwargs): + self._mo_released(key, keyboard, *args, **kwargs) + + def ht_activate_tap(self, key, keyboard, *args, **kwargs): + key_type = kwargs['key_type'] + if key_type == 'LT': + keyboard.hid_pending = True + keyboard.keys_pressed.add(key.meta.kc) + elif key_type == '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 == '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 == 'LT': + self.ht_activate_tap(key, keyboard, *args, **kwargs) + elif key_type == '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 == 'LT': + self.ht_deactivate_tap(key, keyboard, *args, **kwargs) + elif key_type == 'TT': + self.ht_deactivate_hold(key, keyboard, *args, **kwargs) diff --git a/kmk/modules/modtap.py b/kmk/modules/modtap.py index 5402899..eaa83c2 100644 --- a/kmk/modules/modtap.py +++ b/kmk/modules/modtap.py @@ -1,18 +1,19 @@ from kmk.key_validators import mod_tap_validator from kmk.keys import make_argumented_key -from kmk.kmktime import accurate_ticks, accurate_ticks_diff from kmk.modules import Module -class ModTap(Module): +class HoldTapKeyState: + def __init__(self, timeout_key, *args, **kwargs): + self.timeout_key = timeout_key + self.args = args + self.kwargs = kwargs + self.activated = False + + +class HoldTap(Module): def __init__(self): - self._mod_tap_timer = None - make_argumented_key( - validator=mod_tap_validator, - names=('MT',), - on_press=self.mt_pressed, - on_release=self.mt_released, - ) + self.key_states = {} def during_bootup(self, keyboard): return @@ -21,6 +22,17 @@ class ModTap(Module): 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 not state.activated: + # press tap because interrupted by other key + self.key_states[key].activated = 'interrupt' + 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): @@ -35,23 +47,90 @@ class ModTap(Module): 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 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 == 'hold': + # release hold + self.ht_deactivate_hold(key, keyboard, *args, **kwargs) + elif state.activated == 'interrupt': + # 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 not self.key_states[key].activated: + # press hold because timer expired after tap time + self.key_states[key].activated = 'hold' + 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) + + +class ModTap(HoldTap): + def __init__(self): + super().__init__() + make_argumented_key( + validator=mod_tap_validator, + names=('MT',), + on_press=self.ht_pressed, + on_release=self.ht_released, + ) + + def ht_activate_hold(self, key, keyboard, *args, **kwargs): + keyboard.hid_pending = True keyboard.keys_pressed.add(key.meta.mods) - self._mod_tap_timer = accurate_ticks() - 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 ( - accurate_ticks_diff( - accurate_ticks(), 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)