summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Rascher <jon@bcat.name>2021-06-09 18:51:28 -0500
committerPete Johanson <peter@peterjohanson.com>2021-11-15 05:49:23 -0500
commite9140b2da914ee121e7f40eaeb8c6cf827d03622 (patch)
tree483b9f93035a4a7f393d753c63bd1c2b215598e1
parentf8018b22d0bcba97e54652a01e8bdefb15f83c4c (diff)
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.
-rw-r--r--app/CMakeLists.txt1
-rw-r--r--app/dts/bindings/zmk,conditional-layers.yaml17
-rw-r--r--app/src/conditional_layer.c91
3 files changed, 109 insertions, 0 deletions
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 5092def..9c7befe 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 0000000..7e79038
--- /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 0000000..4beb87e
--- /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 <stdint.h>
+
+#include <devicetree.h>
+#include <logging/log.h>
+
+#include <zmk/event_manager.h>
+#include <zmk/keymap.h>
+#include <zmk/events/layer_state_changed.h>
+
+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