Adding ComboLayers to Layers Module (See #658) (#666)

---------

Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com>
This commit is contained in:
Alex Miller 2023-03-10 15:40:57 -05:00 committed by GitHub
parent adff02e88a
commit 878fe0deca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 180 additions and 41 deletions

98
docs/en/combo_layers.md Normal file
View File

@ -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()
```

View File

@ -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 - 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 - 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 ### Using Multiple Base Layers
In some cases, you may want to have more than one base layer (for instance you want to use 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 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 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()` layers with `KC.DF()` the same way that you would treat using `KC.TO()`
## Example Code ## Example Code
For our example, let's take a simple 3x3 macropad with two layers as follows: For our example, let's take a simple 3x3 macropad with two layers as follows:

View File

@ -36,9 +36,15 @@ class LayerKeyMeta:
class Layers(HoldTap): 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): _active_combo = None
def __init__(
self,
combo_layers=None,
):
# Layers # Layers
super().__init__() super().__init__()
self.combo_layers = combo_layers
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
names=('MO',), names=('MO',),
@ -46,9 +52,7 @@ class Layers(HoldTap):
on_release=self._mo_released, on_release=self._mo_released,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator, names=('DF',), on_press=self._df_pressed
names=('DF',),
on_press=self._df_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
@ -57,14 +61,10 @@ class Layers(HoldTap):
on_release=self._lm_released, on_release=self._lm_released,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed
names=('TG',),
on_press=self._tg_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator, names=('TO',), on_press=self._to_pressed
names=('TO',),
on_press=self._to_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator_lt, validator=layer_key_validator_lt,
@ -83,67 +83,102 @@ class Layers(HoldTap):
''' '''
Switches the default layer Switches the default layer
''' '''
keyboard.active_layers[-1] = key.meta.layer self.activate_layer(keyboard, key.meta.layer, as_default=True)
self._print_debug(keyboard)
def _mo_pressed(self, key, keyboard, *args, **kwargs): def _mo_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Momentarily activates layer, switches off when you let go Momentarily activates layer, switches off when you let go
''' '''
keyboard.active_layers.insert(0, key.meta.layer) self.activate_layer(keyboard, key.meta.layer)
self._print_debug(keyboard)
@staticmethod def _mo_released(self, key, keyboard, *args, **kwargs):
def _mo_released(key, keyboard, *args, **kwargs): self.deactivate_layer(keyboard, key.meta.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(key.meta.layer)
del keyboard.active_layers[del_idx]
except ValueError:
pass
__class__._print_debug(__class__, keyboard)
def _lm_pressed(self, key, keyboard, *args, **kwargs): def _lm_pressed(self, key, keyboard, *args, **kwargs):
''' '''
As MO(layer) but with mod active As MO(layer) but with mod active
''' '''
# Sets the timer start and acts like MO otherwise keyboard.hid_pending = True
keyboard.add_key(key.meta.kc) keyboard.keys_pressed.add(key.meta.kc)
self._mo_pressed(key, keyboard, *args, **kwargs) self.activate_layer(keyboard, key.meta.layer)
def _lm_released(self, key, keyboard, *args, **kwargs): def _lm_released(self, key, keyboard, *args, **kwargs):
''' '''
As MO(layer) but with mod active As MO(layer) but with mod active
''' '''
keyboard.remove_key(key.meta.kc) keyboard.hid_pending = True
self._mo_released(key, keyboard, *args, **kwargs) keyboard.keys_pressed.discard(key.meta.kc)
self.deactivate_layer(keyboard, key.meta.layer)
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)
''' '''
# See mo_released for implementation details around this # See mo_released for implementation details around this
try: if key.meta.layer in keyboard.active_layers:
del_idx = keyboard.active_layers.index(key.meta.layer) self.deactivate_layer(keyboard, key.meta.layer)
del keyboard.active_layers[del_idx] else:
except ValueError: self.activate_layer(keyboard, key.meta.layer)
keyboard.active_layers.insert(0, key.meta.layer)
def _to_pressed(self, key, keyboard, *args, **kwargs): def _to_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Activates layer and deactivates all other layers Activates layer and deactivates all other layers
''' '''
self._active_combo = None
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 _print_debug(self, keyboard): def _print_debug(self, keyboard):
# debug(f'__getitem__ {key}')
if debug.enabled: if debug.enabled:
debug(f'active_layers={keyboard.active_layers}') 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