From d88f554f19af698751a8a82832ce11a3ef286747 Mon Sep 17 00:00:00 2001 From: Alex Miller <39163867+doesntfazer@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:40:57 -0500 Subject: [PATCH] Adding ComboLayers to Layers Module (See #658) (#666) --------- Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com> --- docs/en/combo_layers.md | 98 +++++++++++++++++++++++++++++++++ docs/en/layers.md | 6 +++ kmk/modules/layers.py | 117 ++++++++++++++++++++++++++-------------- 3 files changed, 180 insertions(+), 41 deletions(-) create mode 100644 docs/en/combo_layers.md diff --git a/docs/en/combo_layers.md b/docs/en/combo_layers.md new file mode 100644 index 000000000..e809855ba --- /dev/null +++ b/docs/en/combo_layers.md @@ -0,0 +1,98 @@ +## Combo Layers + +Combo Layers is when you hold down 2 or more KC.MO() or KC.LM() keys at a time, and it goes to a defined layer. + +By default combo layers is not activated. You can activate combo layers by adding this to your `main.py` file. +The combolayers NEEDS to be above the `keyboard.modules.append(Layers(combolayers))` + +```python +combo_layers = { + (1, 2): 3, + } +keyboard.modules.append(Layers(combo_layers)) +``` + +In the above code, when layer 1 and 2 are held, layer 3 will activate. If you release 1 or 2 it will go to whatever key is still being held, if both are released it goes to the default (0) layer. +You should also notice that if you already have the layers Module activated, you can just add combolayers into `(Layers())` + +You can add more, and even add more than 2 layers at a time. + +```python +combo_layers = { + (1, 2): 3, + (1, 2, 3): 4, + } +``` + +## Limitations + +There can only be one combo layer active at a time and for overlapping matches +the first matching combo in `combo_layers` takes precedence. +Example: +```python +layers = Layers() +layers.combo_layers = { + (1, 2, 3): 8, + (1, 2): 9, + } +keyboard.modules.append(Layers(combo_layers)) +``` +* If you activate layers 1 then 2, your active layer will be layer number 9. +* If you activate layers 1 then 2, then 3, your active layer will be layer + number 3 (because the layer combo `(1,2)` has been activated, but layer 3 + stacks on top). + * deactivate 1: you're on layer 3 + * deactivate 2: you're on layer 3 + * deactivate 3: you're on layer 8 +* If you activate layers 3 then 1, then 2, your active layer will be layer + number 8. Deativate layer + * deactivate any of 1/2/3: you're on layer 0 + + +## Fully Working Example code + +Below is an example of a fully working keypad that uses combo layers. + +```python +print("Starting") + +import board + +from kmk.kmk_keyboard import KMKKeyboard +from kmk.keys import KC + +combo_layers = { + (1, 2): 3, +keyboard.modules.append(Layers(combo_layers)) + + +keyboard = KMKKeyboard() + + +keyboard.keymap = [ + [ #Default + KC.A, KC.B KC.C KC.D, + KC.E, KC.F KC.G KC.H, + KC.MO(1), KC.J, KC.K, KC.MO(2), + ], + [ #Layer 1 + KC.N1, KC.N2, KC.N3, KC.N4, + KC.N5, KC.N6, KC.N7, KC.8, + KC.MO(1), KC.N9, KC.N0, KC.MO(2), + ], + [ #Layer 2 + KC.EXLM, KC.AT, KC.HASH, KC.DLR, + KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR, + KC.MO(1), KC.LPRN, KC.RPRN, KC.MO(2), + ], + [ #Layer 3 + KC.F1, KC.F2, KC.F3, KC.F4, + KC.F5, KC.F6, KC.F7, KC.F8, + KC.MO(1) KC.F9, KC.F10, KC.MO(2) + ] + +] + +if __name__ == '__main__': + keyboard.go() +``` \ No newline at end of file diff --git a/docs/en/layers.md b/docs/en/layers.md index e5a02e71b..64065085e 100644 --- a/docs/en/layers.md +++ b/docs/en/layers.md @@ -33,6 +33,11 @@ Some helpful guidelines to keep in mind as you design your layers: - Only reference higher-numbered layers from a given layer - Leave keys as `KC.TRNS` in higher layers when they would overlap with a layer-switch +## Using Combo Layers +Combo Layers allow you to activate a corresponding layer based on the activation of 2 or more other layers. +The advantage of using Combo layers is that when you release one of the layer keys, it stays on whatever layer is still being held. +See [combo layers documentation](combolayers.md) for more information on it's function and to see examples. + ### Using Multiple Base Layers In some cases, you may want to have more than one base layer (for instance you want to use both QWERTY and Dvorak layouts, or you have a custom gamepad that can switch between @@ -40,6 +45,7 @@ different games). In this case, best practice is to have these layers be the low defined first in your keymap. These layers are mutually-exclusive, so treat changing default layers with `KC.DF()` the same way that you would treat using `KC.TO()` + ## Example Code For our example, let's take a simple 3x3 macropad with two layers as follows: diff --git a/kmk/modules/layers.py b/kmk/modules/layers.py index 8f3812e2b..89a4cf248 100644 --- a/kmk/modules/layers.py +++ b/kmk/modules/layers.py @@ -36,9 +36,15 @@ def __init__(self, layer, kc=None): class Layers(HoldTap): '''Gives access to the keys used to enable the layer system''' - def __init__(self): + _active_combo = None + + def __init__( + self, + combo_layers=None, + ): # Layers super().__init__() + self.combo_layers = combo_layers make_argumented_key( validator=layer_key_validator, names=('MO',), @@ -46,9 +52,7 @@ def __init__(self): on_release=self._mo_released, ) make_argumented_key( - validator=layer_key_validator, - names=('DF',), - on_press=self._df_pressed, + validator=layer_key_validator, names=('DF',), on_press=self._df_pressed ) make_argumented_key( validator=layer_key_validator, @@ -57,14 +61,10 @@ def __init__(self): on_release=self._lm_released, ) 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 ) make_argumented_key( - validator=layer_key_validator, - names=('TO',), - on_press=self._to_pressed, + validator=layer_key_validator, names=('TO',), on_press=self._to_pressed ) make_argumented_key( validator=layer_key_validator_lt, @@ -83,67 +83,102 @@ def _df_pressed(self, key, keyboard, *args, **kwargs): ''' Switches the default layer ''' - keyboard.active_layers[-1] = key.meta.layer - self._print_debug(keyboard) + self.activate_layer(keyboard, key.meta.layer, as_default=True) def _mo_pressed(self, key, keyboard, *args, **kwargs): ''' Momentarily activates layer, switches off when you let go ''' - keyboard.active_layers.insert(0, key.meta.layer) - self._print_debug(keyboard) + self.activate_layer(keyboard, key.meta.layer) - @staticmethod - def _mo_released(key, keyboard, *args, **kwargs): - # remove the first instance of the target layer - # from the active list - # under almost all normal use cases, this will - # disable the layer (but preserve it if it was triggered - # as a default layer, etc.) - # this also resolves an issue where using DF() on a layer - # triggered by MO() and then defaulting to the MO()'s layer - # would result in no layers active - try: - del_idx = keyboard.active_layers.index(key.meta.layer) - del keyboard.active_layers[del_idx] - except ValueError: - pass - __class__._print_debug(__class__, keyboard) + def _mo_released(self, key, keyboard, *args, **kwargs): + self.deactivate_layer(keyboard, key.meta.layer) def _lm_pressed(self, key, keyboard, *args, **kwargs): ''' As MO(layer) but with mod active ''' - # Sets the timer start and acts like MO otherwise - keyboard.add_key(key.meta.kc) - self._mo_pressed(key, keyboard, *args, **kwargs) + keyboard.hid_pending = True + keyboard.keys_pressed.add(key.meta.kc) + self.activate_layer(keyboard, key.meta.layer) def _lm_released(self, key, keyboard, *args, **kwargs): ''' As MO(layer) but with mod active ''' - keyboard.remove_key(key.meta.kc) - self._mo_released(key, keyboard, *args, **kwargs) + keyboard.hid_pending = True + keyboard.keys_pressed.discard(key.meta.kc) + self.deactivate_layer(keyboard, key.meta.layer) def _tg_pressed(self, key, keyboard, *args, **kwargs): ''' Toggles the layer (enables it if not active, and vise versa) ''' # See mo_released for implementation details around this - try: - del_idx = keyboard.active_layers.index(key.meta.layer) - del keyboard.active_layers[del_idx] - except ValueError: - keyboard.active_layers.insert(0, key.meta.layer) + if key.meta.layer in keyboard.active_layers: + self.deactivate_layer(keyboard, key.meta.layer) + else: + self.activate_layer(keyboard, key.meta.layer) def _to_pressed(self, key, keyboard, *args, **kwargs): ''' Activates layer and deactivates all other layers ''' + self._active_combo = None keyboard.active_layers.clear() keyboard.active_layers.insert(0, key.meta.layer) def _print_debug(self, keyboard): - # debug(f'__getitem__ {key}') if debug.enabled: debug(f'active_layers={keyboard.active_layers}') + + def activate_layer(self, keyboard, layer, as_default=False): + if as_default: + keyboard.active_layers[-1] = layer + else: + keyboard.active_layers.insert(0, layer) + + if self.combo_layers: + self._activate_combo_layer(keyboard) + + self._print_debug(keyboard) + + def deactivate_layer(self, keyboard, layer): + # Remove the first instance of the target layer from the active list + # under almost all normal use cases, this will disable the layer (but + # preserve it if it was triggered as a default layer, etc.). + # This also resolves an issue where using DF() on a layer + # triggered by MO() and then defaulting to the MO()'s layer + # would result in no layers active. + try: + del_idx = keyboard.active_layers.index(layer) + del keyboard.active_layers[del_idx] + except ValueError: + if debug.enabled: + debug(f'_mo_released: layer {layer} not active') + + if self.combo_layers: + self._deactivate_combo_layer(keyboard, layer) + + self._print_debug(keyboard) + + def _activate_combo_layer(self, keyboard): + if self._active_combo: + return + + for combo, result in self.combo_layers.items(): + matching = True + for layer in combo: + if layer not in keyboard.active_layers: + matching = False + break + + if matching: + self._active_combo = combo + keyboard.active_layers.insert(0, result) + break + + def _deactivate_combo_layer(self, keyboard, layer): + if self._active_combo and layer in self._active_combo: + keyboard.active_layers.remove(self.combo_layers[self._active_combo]) + self._active_combo = None