From e9140b2da914ee121e7f40eaeb8c6cf827d03622 Mon Sep 17 00:00:00 2001 From: Jonathan Rascher Date: Wed, 9 Jun 2021 18:51:28 -0500 Subject: [PATCH] feat(conditional-layers): Implement feature This is a generalization of the existing concept of tri-layer support that's already well known. Essentially, a conditional-layer configuration activates a particular layer (the then-layer) when one or more other layers (the if-layers) are activated. This is commonly used on ortho keyboards to activate a third "adjust" layer while the primary two layers ("lower" and "raise") are active. --- app/CMakeLists.txt | 1 + app/dts/bindings/zmk,conditional-layers.yaml | 17 ++++ app/src/conditional_layer.c | 91 ++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 app/dts/bindings/zmk,conditional-layers.yaml create mode 100644 app/src/conditional_layer.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5092deff..9c7befec 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -57,6 +57,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_sensor_rotate_key_press.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) target_sources(app PRIVATE src/combo.c) + target_sources(app PRIVATE src/conditional_layer.c) target_sources(app PRIVATE src/keymap.c) endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) diff --git a/app/dts/bindings/zmk,conditional-layers.yaml b/app/dts/bindings/zmk,conditional-layers.yaml new file mode 100644 index 00000000..7e79038e --- /dev/null +++ b/app/dts/bindings/zmk,conditional-layers.yaml @@ -0,0 +1,17 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Conditional layers allow layer combinations to trigger additional layers + +compatible: "zmk,conditional-layers" + +child-binding: + description: "Single conditional layer that activates then-layer when if-layers are active" + + properties: + if-layers: + type: array + required: true + then-layer: + type: int + required: true diff --git a/app/src/conditional_layer.c b/app/src/conditional_layer.c new file mode 100644 index 00000000..4beb87e3 --- /dev/null +++ b/app/src/conditional_layer.c @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_conditional_layers + +#include + +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +// Conditional layer configuration that activates the specified then-layer when all if-layers are +// active. With two if-layers, this is referred to as "tri-layer", and is commonly used to activate +// a third "adjust" layer if and only if the "lower" and "raise" layers are both active. +struct conditional_layer_cfg { + // A bitmask of each layer that must be pressed for this conditional layer config to activate. + zmk_keymap_layers_state_t if_layers_state_mask; + + // The layer number that should be active while all layers in the if-layers mask are active. + int8_t then_layer; +}; + +#define IF_LAYER_BIT(i, n) BIT(DT_PROP_BY_IDX(n, if_layers, i)) | + +// Evaluates to conditional_layer_cfg struct initializer. +#define CONDITIONAL_LAYER_DECL(n) \ + { \ + /* TODO: Replace UTIL_LISTIFY with DT_FOREACH_PROP_ELEM after Zepyhr 2.6.0 upgrade. */ \ + .if_layers_state_mask = UTIL_LISTIFY(DT_PROP_LEN(n, if_layers), IF_LAYER_BIT, n) 0, \ + .then_layer = DT_PROP(n, then_layer), \ + }, + +// All conditional layer configurations in the keymap. +static const struct conditional_layer_cfg CONDITIONAL_LAYER_CFGS[] = { + DT_INST_FOREACH_CHILD(0, CONDITIONAL_LAYER_DECL)}; + +static const int32_t NUM_CONDITIONAL_LAYER_CFGS = + sizeof(CONDITIONAL_LAYER_CFGS) / sizeof(*CONDITIONAL_LAYER_CFGS); + +static void conditional_layer_activate(int8_t layer) { + // This may trigger another event that could, in turn, activate additional then-layers. However, + // the process will eventually terminate (at worst, when every layer is active). + if (!zmk_keymap_layer_active(layer)) { + LOG_DBG("layer %d", layer); + zmk_keymap_layer_activate(layer); + } +} + +static void conditional_layer_deactivate(int8_t layer) { + // This may deactivate a then-layer that's already active via another mechanism (e.g., a + // momentary layer behavior). However, the same problem arises when multiple keys with the same + // &mo binding are held and then one is released, so it's probably not an issue in practice. + if (zmk_keymap_layer_active(layer)) { + LOG_DBG("layer %d", layer); + zmk_keymap_layer_deactivate(layer); + } +} + +// On layer state changes, examines each conditional layer config to determine if then-layer in the +// config should activate based on the currently active set of if-layers. +static int layer_state_changed_listener(const zmk_event_t *ev) { + for (int i = 0; i < NUM_CONDITIONAL_LAYER_CFGS; i++) { + const struct conditional_layer_cfg *cfg = CONDITIONAL_LAYER_CFGS + i; + zmk_keymap_layers_state_t mask = cfg->if_layers_state_mask; + + // Activate then-layer if and only if all if-layers are already active. Note that we + // reevaluate the current layer state for each config since activation of one layer can also + // trigger activation of another. + if ((zmk_keymap_layer_state() & mask) == mask) { + conditional_layer_activate(cfg->then_layer); + } else { + conditional_layer_deactivate(cfg->then_layer); + } + } + return 0; +} + +ZMK_LISTENER(conditional_layer, layer_state_changed_listener); +ZMK_SUBSCRIPTION(conditional_layer, zmk_layer_state_changed); + +#endif