Skip to content

Commit

Permalink
Adding ComboLayers to Layers Module (See #658) (#666)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: xs5871 <[email protected]>
  • Loading branch information
doesntfazer and xs5871 committed Oct 16, 2023
1 parent 591edc5 commit d88f554
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 41 deletions.
98 changes: 98 additions & 0 deletions docs/en/combo_layers.md
Original file line number Diff line number Diff line change
@@ -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()
```
6 changes: 6 additions & 0 deletions docs/en/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@ 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
different games). In this case, best practice is to have these layers be the lowest, i.e.
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:

Expand Down
117 changes: 76 additions & 41 deletions kmk/modules/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,23 @@ 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',),
on_press=self._mo_pressed,
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,
Expand All @@ -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,
Expand All @@ -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

0 comments on commit d88f554

Please sign in to comment.