From f129ffb593895646c9e09f5db4d1b547ddaa750d Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Mon, 29 Jan 2024 19:39:06 -0800 Subject: [PATCH 01/15] Initial add of Layer Lock feature --- builddefs/generic_features.mk | 1 + .../keycodes/keycodes_0.0.4_quantum.hjson | 11 ++ data/mappings/info_config.hjson | 3 + data/schemas/keyboard.jsonschema | 6 + keyboards/zsa/moonlander/config.h | 2 + quantum/keyboard.c | 7 + quantum/keycodes.h | 6 +- quantum/layer_lock.c | 81 +++++++++++ quantum/layer_lock.h | 129 ++++++++++++++++++ quantum/process_keycode/process_layer_lock.c | 95 +++++++++++++ quantum/process_keycode/process_layer_lock.h | 69 ++++++++++ quantum/quantum.c | 3 + quantum/quantum.h | 4 + tests/test_common/keycode_table.cpp | 1 + 14 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 data/constants/keycodes/keycodes_0.0.4_quantum.hjson create mode 100644 quantum/layer_lock.c create mode 100644 quantum/layer_lock.h create mode 100644 quantum/process_keycode/process_layer_lock.c create mode 100644 quantum/process_keycode/process_layer_lock.h diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk index dc34a642307d..f14f44087702 100644 --- a/builddefs/generic_features.mk +++ b/builddefs/generic_features.mk @@ -36,6 +36,7 @@ GENERIC_FEATURES = \ HAPTIC \ KEY_LOCK \ KEY_OVERRIDE \ + LAYER_LOCK \ LEADER \ MAGIC \ MOUSEKEY \ diff --git a/data/constants/keycodes/keycodes_0.0.4_quantum.hjson b/data/constants/keycodes/keycodes_0.0.4_quantum.hjson new file mode 100644 index 000000000000..63153daa73c8 --- /dev/null +++ b/data/constants/keycodes/keycodes_0.0.4_quantum.hjson @@ -0,0 +1,11 @@ +{ + "keycodes": { + "0x7C7B": { + "group": "quantum", + "key": "QK_LAYER_LOCK", + "aliases": [ + "QK_LLCK" + ] + } + } +} diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index b61ca0407145..f1f0deb8b25e 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -64,6 +64,9 @@ "WEAR_LEVELING_BACKING_SIZE": {"info_key": "eeprom.wear_leveling.backing_size", "value_type": "int", "to_json": false}, "WEAR_LEVELING_LOGICAL_SIZE": {"info_key": "eeprom.wear_leveling.logical_size", "value_type": "int", "to_json": false}, + // Layer locking + "LAYER_LOCK_IDLE_TIMEOUT": {"info_key": "layer_lock.timeout", "value_type": "int"}, + // Indicators "LED_CAPS_LOCK_PIN": {"info_key": "indicators.caps_lock"}, "LED_NUM_LOCK_PIN": {"info_key": "indicators.num_lock"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index b699f862770b..a5dc42281d39 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -343,6 +343,12 @@ } }, "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"}, + "layer_lock": { + "type": "object", + "properties": { + "timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"} + } + }, "layout_aliases": { "type": "object", "additionalProperties": {"$ref": "qmk.definitions.v1#/layout_macro"} diff --git a/keyboards/zsa/moonlander/config.h b/keyboards/zsa/moonlander/config.h index 08870fba9d5a..70f776c032fc 100644 --- a/keyboards/zsa/moonlander/config.h +++ b/keyboards/zsa/moonlander/config.h @@ -80,3 +80,5 @@ #define AUDIO_PIN A5 #define AUDIO_PIN_ALT A4 #define AUDIO_PIN_ALT_AS_NEGATIVE + +#define LAYER_LOCK_IDLE_TIMEOUT 60000 diff --git a/quantum/keyboard.c b/quantum/keyboard.c index df1dc1c3ee09..8db81a4b391b 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -140,6 +140,9 @@ along with this program. If not, see . #ifdef OS_DETECTION_ENABLE # include "os_detection.h" #endif +#if defined(LAYER_LOCK_ENABLE) && LAYER_LOCK_IDLE_TIMEOUT > 0 +# include "layer_lock.h" +#endif // LAYER_LOCK_ENABLE static uint32_t last_input_modification_time = 0; uint32_t last_input_activity_time(void) { @@ -655,6 +658,10 @@ void quantum_task(void) { #ifdef SECURE_ENABLE secure_task(); #endif + +#if defined(LAYER_LOCK_ENABLE) && LAYER_LOCK_IDLE_TIMEOUT > 0 + layer_lock_task(); +#endif } /** \brief Main task that is repeatedly called as fast as possible. */ diff --git a/quantum/keycodes.h b/quantum/keycodes.h index c92028ab4399..415ac614880f 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -745,6 +745,7 @@ enum qk_keycode_defines { QK_TRI_LAYER_UPPER = 0x7C78, QK_REPEAT_KEY = 0x7C79, QK_ALT_REPEAT_KEY = 0x7C7A, + QK_LAYER_LOCK = 0x7C7B, QK_KB_0 = 0x7E00, QK_KB_1 = 0x7E01, QK_KB_2 = 0x7E02, @@ -1419,6 +1420,7 @@ enum qk_keycode_defines { TL_UPPR = QK_TRI_LAYER_UPPER, QK_REP = QK_REPEAT_KEY, QK_AREP = QK_ALT_REPEAT_KEY, + QK_LLCK = QK_LAYER_LOCK, }; // Range Helpers @@ -1473,7 +1475,7 @@ enum qk_keycode_defines { #define IS_UNDERGLOW_KEYCODE(code) ((code) >= QK_UNDERGLOW_TOGGLE && (code) <= QK_UNDERGLOW_SPEED_DOWN) #define IS_RGB_KEYCODE(code) ((code) >= RGB_MODE_PLAIN && (code) <= RGB_MODE_TWINKLE) #define IS_RGB_MATRIX_KEYCODE(code) ((code) >= QK_RGB_MATRIX_ON && (code) <= QK_RGB_MATRIX_SPEED_DOWN) -#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY) +#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_LAYER_LOCK) #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31) #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31) @@ -1498,6 +1500,6 @@ enum qk_keycode_defines { #define UNDERGLOW_KEYCODE_RANGE QK_UNDERGLOW_TOGGLE ... QK_UNDERGLOW_SPEED_DOWN #define RGB_KEYCODE_RANGE RGB_MODE_PLAIN ... RGB_MODE_TWINKLE #define RGB_MATRIX_KEYCODE_RANGE QK_RGB_MATRIX_ON ... QK_RGB_MATRIX_SPEED_DOWN -#define QUANTUM_KEYCODE_RANGE QK_BOOTLOADER ... QK_ALT_REPEAT_KEY +#define QUANTUM_KEYCODE_RANGE QK_BOOTLOADER ... QK_LAYER_LOCK #define KB_KEYCODE_RANGE QK_KB_0 ... QK_KB_31 #define USER_KEYCODE_RANGE QK_USER_0 ... QK_USER_31 diff --git a/quantum/layer_lock.c b/quantum/layer_lock.c new file mode 100644 index 000000000000..3eaf0305b404 --- /dev/null +++ b/quantum/layer_lock.c @@ -0,0 +1,81 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "layer_lock.h" +#include "quantum_keycodes.h" + +#ifndef NO_ACTION_LAYER +// The current lock state. The kth bit is on if layer k is locked. +layer_state_t locked_layers = 0; + +// Layer Lock timer to disable layer lock after X seconds inactivity +# if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0 +uint32_t layer_lock_timer = 0; + +void layer_lock_task(void) { + if (locked_layers && timer_elapsed32(layer_lock_timer) > LAYER_LOCK_IDLE_TIMEOUT) { + layer_lock_all_off(); + layer_lock_timer = timer_read32(); + } +} +# endif // LAYER_LOCK_IDLE_TIMEOUT > 0 + +bool is_layer_locked(uint8_t layer) { + return locked_layers & ((layer_state_t)1 << layer); +} + +void layer_lock_invert(uint8_t layer) { + const layer_state_t mask = (layer_state_t)1 << layer; + if ((locked_layers & mask) == 0) { // Layer is being locked. +# ifndef NO_ACTION_ONESHOT + if (layer == get_oneshot_layer()) { + reset_oneshot_layer(); // Reset so that OSL doesn't turn layer off. + } +# endif // NO_ACTION_ONESHOT + layer_on(layer); +# if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0 + layer_lock_timer = timer_read32(); +# endif // LAYER_LOCK_IDLE_TIMEOUT > 0 + } else { // Layer is being unlocked. + layer_off(layer); + } + layer_lock_set_user(locked_layers ^= mask); +} + +// Implement layer_lock_on/off by deferring to layer_lock_invert. +void layer_lock_on(uint8_t layer) { + if (!is_layer_locked(layer)) { + layer_lock_invert(layer); + } +} + +void layer_lock_off(uint8_t layer) { + if (is_layer_locked(layer)) { + layer_lock_invert(layer); + } +} + +void layer_lock_all_off(void) { + layer_and(~locked_layers); + locked_layers = 0; + layer_lock_set_user(locked_layers); +} + +__attribute__((weak)) bool layer_lock_set_kb(layer_state_t locked_layers) { + return layer_lock_set_user(locked_layers); +} +__attribute__((weak)) bool layer_lock_set_user(layer_state_t locked_layers) { + return true; +} +#endif // NO_ACTION_LAYER diff --git a/quantum/layer_lock.h b/quantum/layer_lock.h new file mode 100644 index 000000000000..40abfa1ffd44 --- /dev/null +++ b/quantum/layer_lock.h @@ -0,0 +1,129 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file layer_lock.h + * @brief Layer Lock, a key to stay in the current layer. + * + * Overview + * -------- + * + * Layers are often accessed by holding a button, e.g. with a momentary layer + * switch `MO(layer)` or layer tap `LT(layer, key)` key. But you may sometimes + * want to "lock" or "toggle" the layer so that it stays on without having to + * hold down a button. One way to do that is with a tap-toggle `TT` layer key, + * but here is an alternative. + * + * This library implements a "Layer Lock key". When tapped, it "locks" the + * highest layer to stay active, assuming the layer was activated by one of the + * following keys: + * + * * `MO(layer)` momentary layer switch + * * `LT(layer, key)` layer tap + * * `OSL(layer)` one-shot layer + * * `TT(layer)` layer tap toggle + * * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods) + * + * Tapping the Layer Lock key again unlocks and turns off the layer. + * + * @note When a layer is "locked", other layer keys such as `TO(layer)` or + * manually calling `layer_off(layer)` will override and unlock the layer. + * + * Configuration + * ------------- + * + * Optionally, a timeout may be defined so that Layer Lock disables + * automatically if not keys are pressed for `LAYER_LOCK_IDLE_TIMEOUT` + * milliseconds. Define `LAYER_LOCK_IDLE_TIMEOUT` in your config.h, for instance + * + * #define LAYER_LOCK_IDLE_TIMEOUT 60000 // Turn off after 60 seconds. + * + * and call `layer_lock_task()` from your `matrix_scan_user()` in keymap.c: + * + * void matrix_scan_user(void) { + * layer_lock_task(); + * // Other tasks... + * } + * + * For full documentation, see + * + */ + +#pragma once + +#include +#include +#include "action_layer.h" +#include "action_util.h" + +/** + * Handler function for Layer Lock. + * + * In your keymap, define a custom keycode to use for Layer Lock. Then handle + * Layer Lock from your `process_record_user` function by calling + * `process_layer_lock`, passing your custom keycode for the `lock_keycode` arg: + * + * #include "features/layer_lock.h" + * + * bool process_record_user(uint16_t keycode, keyrecord_t* record) { + * if (!process_layer_lock(keycode, record, LLOCK)) { return false; } + * // Your macros ... + * + * return true; + * } + */ + +#ifndef NO_ACTION_LAYER +/** Returns true if `layer` is currently locked. */ +bool is_layer_locked(uint8_t layer); + +/** Locks and turns on `layer`. */ +void layer_lock_on(uint8_t layer); + +/** Unlocks and turns off `layer`. */ +void layer_lock_off(uint8_t layer); + +/** Unlocks and turns off all locked layers. */ +void layer_lock_all_off(void); + +/** Toggles whether `layer` is locked. */ +void layer_lock_invert(uint8_t layer); + +/** + * Optional callback that gets called when a layer is locked or unlocked. + * + * This is useful to represent the current lock state, e.g. by setting an LED or + * playing a sound. In your keymap, define + * + * void layer_lock_set_user(layer_state_t locked_layers) { + * // Do something like `set_led(is_layer_locked(NAV));` + * } + * + * @param locked_layers Bitfield in which the kth bit represents whether the + * kth layer is on. + */ +bool layer_lock_set_kb(layer_state_t locked_layers); +bool layer_lock_set_user(layer_state_t locked_layers); + +void layer_lock_task(void); +#else // NO_ACTION_LAYER +static inline bool is_layer_locked(uint8_t layer) { return false; } +static inline void layer_lock_on(uint8_t layer) {} +static inline void layer_lock_off(uint8_t layer) {} +static inline void layer_lock_all_off(void) {} +static inline void layer_lock_invert(uint8_t layer) {} +static inline bool layer_lock_set_kb(layer_state_t locked_layers) { return true; } +static inline bool layer_lock_set_user(layer_state_t locked_layers) { return true; } +static inline void layer_lock_task(void) {} +#endif // NO_ACTION_LAYER diff --git a/quantum/process_keycode/process_layer_lock.c b/quantum/process_keycode/process_layer_lock.c new file mode 100644 index 000000000000..1e36d8844e83 --- /dev/null +++ b/quantum/process_keycode/process_layer_lock.c @@ -0,0 +1,95 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file layer_lock.c + * @brief Layer Lock implementation + * + * For full documentation, see + * + */ + +#include "layer_lock.h" +#include "process_layer_lock.h" +#include "quantum_keycodes.h" +#include "action_util.h" + +// The current lock state. The kth bit is on if layer k is locked. +extern layer_state_t locked_layers; +#if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0 +extern uint32_t layer_lock_timer; +#endif + +// Handles an event on an `MO` or `TT` layer switch key. +static bool handle_mo_or_tt(uint8_t layer, keyrecord_t* record) { + if (is_layer_locked(layer)) { + if (record->event.pressed) { // On press, unlock the layer. + layer_lock_invert(layer); + } + return false; // Skip default handling. + } + return true; +} + +bool process_layer_lock(uint16_t keycode, keyrecord_t* record) { +#ifndef NO_ACTION_LAYER +# if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0 + layer_lock_timer = timer_read32(); +# endif // LAYER_LOCK_IDLE_TIMEOUT > 0 + + // The intention is that locked layers remain on. If something outside of + // this feature turned any locked layers off, unlock them. + if ((locked_layers & ~layer_state) != 0) { + layer_lock_set_kb(locked_layers &= layer_state); + } + + if (keycode == QK_LAYER_LOCK) { + if (record->event.pressed) { // The layer lock key was pressed. + layer_lock_invert(get_highest_layer(layer_state)); + } + return false; + } + + switch (keycode) { + case QK_MOMENTARY ... QK_MOMENTARY_MAX: // `MO(layer)` keys. + return handle_mo_or_tt(QK_MOMENTARY_GET_LAYER(keycode), record); + + case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: // `TT(layer)`. + return handle_mo_or_tt(QK_LAYER_TAP_TOGGLE_GET_LAYER(keycode), record); + + case QK_LAYER_MOD ... QK_LAYER_MOD_MAX: { // `LM(layer, mod)`. + uint8_t layer = QK_LAYER_MOD_GET_LAYER(keycode); + if (is_layer_locked(layer)) { + if (record->event.pressed) { // On press, unlock the layer. + layer_lock_invert(layer); + } else { // On release, clear the mods. + clear_mods(); + send_keyboard_report(); + } + return false; // Skip default handling. + } + } break; + +# ifndef NO_ACTION_TAPPING + case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: // `LT(layer, key)` keys. + if (record->tap.count == 0 && !record->event.pressed && is_layer_locked(QK_LAYER_TAP_GET_LAYER(keycode))) { + // Release event on a held layer-tap key where the layer is locked. + return false; // Skip default handling so that layer stays on. + } + break; +# endif // NO_ACTION_TAPPING + } +#endif // NO_ACTION_LAYER + return true; +} diff --git a/quantum/process_keycode/process_layer_lock.h b/quantum/process_keycode/process_layer_lock.h new file mode 100644 index 000000000000..b54c0f6f1062 --- /dev/null +++ b/quantum/process_keycode/process_layer_lock.h @@ -0,0 +1,69 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file layer_lock.h + * @brief Layer Lock, a key to stay in the current layer. + * + * Overview + * -------- + * + * Layers are often accessed by holding a button, e.g. with a momentary layer + * switch `MO(layer)` or layer tap `LT(layer, key)` key. But you may sometimes + * want to "lock" or "toggle" the layer so that it stays on without having to + * hold down a button. One way to do that is with a tap-toggle `TT` layer key, + * but here is an alternative. + * + * This library implements a "Layer Lock key". When tapped, it "locks" the + * highest layer to stay active, assuming the layer was activated by one of the + * following keys: + * + * * `MO(layer)` momentary layer switch + * * `LT(layer, key)` layer tap + * * `OSL(layer)` one-shot layer + * * `TT(layer)` layer tap toggle + * * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods) + * + * Tapping the Layer Lock key again unlocks and turns off the layer. + * + * @note When a layer is "locked", other layer keys such as `TO(layer)` or + * manually calling `layer_off(layer)` will override and unlock the layer. + * + * Configuration + * ------------- + * + * Optionally, a timeout may be defined so that Layer Lock disables + * automatically if not keys are pressed for `LAYER_LOCK_IDLE_TIMEOUT` + * milliseconds. Define `LAYER_LOCK_IDLE_TIMEOUT` in your config.h, for instance + * + * #define LAYER_LOCK_IDLE_TIMEOUT 60000 // Turn off after 60 seconds. + * + * and call `layer_lock_task()` from your `matrix_scan_user()` in keymap.c: + * + * void matrix_scan_user(void) { + * layer_lock_task(); + * // Other tasks... + * } + * + * For full documentation, see + * + */ + +#pragma once + +#include +#include +#include "action.h" + +bool process_layer_lock(uint16_t keycode, keyrecord_t* record); diff --git a/quantum/quantum.c b/quantum/quantum.c index 011f9d73e4af..21e9df6f35d4 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -393,6 +393,9 @@ bool process_record_quantum(keyrecord_t *record) { #endif #ifdef TRI_LAYER_ENABLE process_tri_layer(keycode, record) && +#endif +#ifdef LAYER_LOCK_ENABLE + process_layer_lock(keycode, record) && #endif true)) { return false; diff --git a/quantum/quantum.h b/quantum/quantum.h index 5446ab1ad719..2b775510b453 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -240,6 +240,10 @@ extern layer_state_t layer_state; # include "os_detection.h" #endif +#ifdef LAYER_LOCK_ENABLE +# include "process_layer_lock.h" +#endif // LAYER_LOCK_ENABLE + void set_single_persistent_default_layer(uint8_t default_layer); #define IS_LAYER_ON(layer) layer_state_is(layer) diff --git a/tests/test_common/keycode_table.cpp b/tests/test_common/keycode_table.cpp index 18dd5360277b..477ccf840014 100644 --- a/tests/test_common/keycode_table.cpp +++ b/tests/test_common/keycode_table.cpp @@ -687,6 +687,7 @@ std::map KEYCODE_ID_TABLE = { {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"}, {QK_REPEAT_KEY, "QK_REPEAT_KEY"}, {QK_ALT_REPEAT_KEY, "QK_ALT_REPEAT_KEY"}, + {QK_LAYER_LOCK, "QK_LAYER_LOCK"}, {QK_KB_0, "QK_KB_0"}, {QK_KB_1, "QK_KB_1"}, {QK_KB_2, "QK_KB_2"}, From 8370f4cd559cffcc3088af24aeba0b950048c55c Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Tue, 6 Feb 2024 21:26:06 -0800 Subject: [PATCH 02/15] Add tests for layer lock --- tests/layer_lock/config.h | 8 ++ tests/layer_lock/test.mk | 8 ++ tests/layer_lock/test_layer_lock.cpp | 148 +++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 tests/layer_lock/config.h create mode 100644 tests/layer_lock/test.mk create mode 100644 tests/layer_lock/test_layer_lock.cpp diff --git a/tests/layer_lock/config.h b/tests/layer_lock/config.h new file mode 100644 index 000000000000..25d0b20c0ead --- /dev/null +++ b/tests/layer_lock/config.h @@ -0,0 +1,8 @@ +// Copyright 2021 Christopher Courtney, aka Drashna Jael're (@drashna) +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "test_common.h" + +#define LAYER_LOCK_IDLE_TIMEOUT 1000 diff --git a/tests/layer_lock/test.mk b/tests/layer_lock/test.mk new file mode 100644 index 000000000000..05771e4dbf89 --- /dev/null +++ b/tests/layer_lock/test.mk @@ -0,0 +1,8 @@ +# Copyright 2021 Christopher Courtney, aka Drashna Jael're (@drashna) +# SPDX-License-Identifier: GPL-2.0-or-later + +# -------------------------------------------------------------------------------- +# Keep this file, even if it is empty, as a marker that this folder contains tests +# -------------------------------------------------------------------------------- + +LAYER_LOCK_ENABLE = yes diff --git a/tests/layer_lock/test_layer_lock.cpp b/tests/layer_lock/test_layer_lock.cpp new file mode 100644 index 000000000000..76185fdb2d7a --- /dev/null +++ b/tests/layer_lock/test_layer_lock.cpp @@ -0,0 +1,148 @@ +// Copyright 2021 Christopher Courtney, aka Drashna Jael're (@drashna) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "keycodes.h" +#include "test_common.hpp" + +using testing::_; + +class LayerLock : public TestFixture {}; + +TEST_F(LayerLock, LayerLockMomentaryTest) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_trns = KeymapKey(1, 0, 0, KC_TRNS); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_trns, key_ll}); + + /* Press Lower. */ + EXPECT_NO_REPORT(driver); + key_layer.press(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_ll); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + key_layer.release(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(LayerLock, LayerLockLayerTapTest) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, LT(1, KC_B)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_trns = KeymapKey(1, 0, 0, KC_TRNS); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_trns, key_ll}); + + /* Press Lower. */ + EXPECT_NO_REPORT(driver); + key_layer.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_ll); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(LayerLock, LayerLockOneshotTapTest) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, OSL(1)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_trns = KeymapKey(1, 0, 0, KC_TRNS); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_trns, key_ll}); + + /* Press Lower. */ + EXPECT_NO_REPORT(driver); + tap_key(key_layer); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_ll); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(LayerLock, LayerLockOneshotHoldTest) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, OSL(1)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_trns = KeymapKey(1, 0, 0, KC_TRNS); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_trns, key_ll}); + + /* Press Lower. */ + EXPECT_NO_REPORT(driver); + key_layer.press(); + idle_for(TAPPING_TERM); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_ll); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + key_layer.release(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); +} + + +TEST_F(LayerLock, LayerLockTimeoutTest) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_trns = KeymapKey(1, 0, 0, KC_TRNS); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_trns, key_ll}); + + /* Press Lower. */ + EXPECT_NO_REPORT(driver); + key_layer.press(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_ll); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + key_layer.release(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + idle_for(LAYER_LOCK_IDLE_TIMEOUT); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); +} From 9f2cc29bba3c4a9e27a6a0c8f5d762214b9e765d Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Sun, 9 Jun 2024 12:29:03 -0700 Subject: [PATCH 03/15] Update for newer version --- data/constants/keycodes/keycodes_0.0.5.hjson | 0 ...{keycodes_0.0.4_quantum.hjson => keycodes_0.0.5_quantum.hjson} | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/constants/keycodes/keycodes_0.0.5.hjson rename data/constants/keycodes/{keycodes_0.0.4_quantum.hjson => keycodes_0.0.5_quantum.hjson} (100%) diff --git a/data/constants/keycodes/keycodes_0.0.5.hjson b/data/constants/keycodes/keycodes_0.0.5.hjson new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/data/constants/keycodes/keycodes_0.0.4_quantum.hjson b/data/constants/keycodes/keycodes_0.0.5_quantum.hjson similarity index 100% rename from data/constants/keycodes/keycodes_0.0.4_quantum.hjson rename to data/constants/keycodes/keycodes_0.0.5_quantum.hjson From 2c176197938ec4b7ddb6ecb9436947b1173affdd Mon Sep 17 00:00:00 2001 From: Pascal Getreuer Date: Thu, 13 Jun 2024 20:35:36 -0600 Subject: [PATCH 04/15] Add documentation to Layer Lock. This commit builds on @drashna's https://github.com/qmk/qmk_firmware/pull/23430 to add documentation: * New page docs/features/layer_lock.md added describing Layer Lock. * Added link to this page in docs/_sidebar.json * And in docs/keycodes.md * And a cross-reference in docs/feature_layers.md NOTE: Beware, I haven't checked this documentation locally with `qmk docs` (the command failed with a problem involving yarn). So there might be formatting bugs and broken links in this. --- docs/_sidebar.json | 1 + docs/feature_layers.md | 3 + docs/features/layer_lock.md | 138 ++++++++++++++++++++++++++++++++++++ docs/keycodes.md | 8 +++ 4 files changed, 150 insertions(+) create mode 100644 docs/features/layer_lock.md diff --git a/docs/_sidebar.json b/docs/_sidebar.json index b41719e4b327..c21aa49c0b61 100644 --- a/docs/_sidebar.json +++ b/docs/_sidebar.json @@ -129,6 +129,7 @@ { "text": "Key Lock", "link": "/features/key_lock" }, { "text": "Key Overrides", "link": "/features/key_overrides" }, { "text": "Layers", "link": "/feature_layers" }, + { "text": "Layer Lock", "link": "/features/layer_lock" }, { "text": "One Shot Keys", "link": "/one_shot_keys" }, { "text": "OS Detection", "link": "/features/os_detection" }, { "text": "Raw HID", "link": "/features/rawhid" }, diff --git a/docs/feature_layers.md b/docs/feature_layers.md index fe9932fadbe6..7b758e42aa9a 100644 --- a/docs/feature_layers.md +++ b/docs/feature_layers.md @@ -17,6 +17,9 @@ These functions allow you to activate layers in various ways. Note that layers a * `TO(layer)` - activates *layer* and de-activates all other layers (except your default layer). This function is special, because instead of just adding/removing one layer to your active layer stack, it will completely replace your current active layers, uniquely allowing you to replace higher layers with a lower one. This is activated on keydown (as soon as the key is pressed). * `TT(layer)` - Layer Tap-Toggle. If you hold the key down, *layer* is activated, and then is de-activated when you let go (like `MO`). If you repeatedly tap it, the layer will be toggled on or off (like `TG`). It needs 5 taps by default, but you can change this by defining `TAPPING_TOGGLE` -- for example, `#define TAPPING_TOGGLE 2` to toggle on just two taps. +See also the [Layer Lock key](features/layer_lock), which locks the highest +active layer until pressed again. + ### Caveats {#caveats} Currently, the `layer` argument of `LT()` is limited to layers 0-15, and the `kc` argument to the [Basic Keycode set](keycodes_basic), meaning you can't use keycodes like `LCTL()`, `KC_TILD`, or anything greater than `0xFF`. This is because QMK uses 16-bit keycodes, of which 4 bits are used for the function identifier and 4 bits for the layer, leaving only 8 bits for the keycode. diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md new file mode 100644 index 000000000000..72ad1d95d439 --- /dev/null +++ b/docs/features/layer_lock.md @@ -0,0 +1,138 @@ +# Layer Lock + +Some [layer switches](../feature_layers#switching-and-toggling-layers) access +the layer by holding the key, including momentary layer `MO(layer)` and layer +tap `LT(layer, key)` keys. You may sometimes need to stay on the layer for a +long period of time. Layer Lock "locks" the current layer to stay on, supposing +it was accessed by one of: + + * `MO(layer)` momentary layer switch + * `LT(layer, key)` layer tap + * `OSL(layer)` one-shot layer + * `TT(layer)` layer tap toggle + * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods) + +Press the Layer Lock key again to unlock the layer. Additionally, when a layer +is locked, layer switch keys that turn off the layer such as `TO(other_layer)` +will unlock it. + + +## How do I enable Layer Lock + +In your rules.mk, add: + +```make +LAYER_LOCK_ENABLE = yes +``` + +Pick a key in your keymap on a layer you intend to lock, and assign it the +keycode `QK_LAYER_LOCK` (short alias `QK_LLCK`). Note that locking the base +layer has no effect, so typically, this key is used on layers above the base +layer. + + +## Example use + +Consider a keymap with the following base layer. + +![Base layer with a MO(NAV) key.](https://imgur.com/DkEhj9x) + +The highlighted key is a momentary layer switch `MO(NAV)`. Holding it accesses a +navigation layer. + +![Nav layer wite a Layer Lock key.](https://imgur.com/2wUZNWk) + + +Holding the NAV key is fine for brief use, but awkward to continue holding when +using navigation functions continuously. The Layer Lock key comes to the rescue: + +1. Hold the NAV key, activating the navigation layer. +2. Tap Layer Lock. +3. Release NAV. The navigation layer stays on. +4. Make use of the arrow keys, etc. +5. Tap Layer Lock or NAV again to turn the navigation layer back off. + +A variation that would also work is to put the Layer Lock key on the base layer +and make other layers transparent (`KC_TRNS`) in that position. Pressing the +Layer Lock key locks (or unlocks) the highest active layer, regardless of which +layer the Layer Lock key is on. + + +## Idle timeout + +Optionally, Layer Lock may be configured to unlock if the keyboard is idle +for some time. In config.h, define `LAYER_LOCK_IDLE_TIMEOUT` in units of +milliseconds: + +```c +#define LAYER_LOCK_IDLE_TIMEOUT 60000 // Turn off after 60 seconds. +``` + + +## Functions + +Use the following functions to query and manipulate the layer lock state. + +| Function | Description | +|----------------------------|------------------------------------| +| `is_layer_locked(layer)` | Checks whether `layer` is locked. | +| `layer_lock_on(layer)` | Locks and turns on `layer`. | +| `layer_lock_off(layer)` | Unlocks and turns off `layer`. | +| `layer_lock_invert(layer)` | Toggles whether `layer` is locked. | + + +## Representing the current Layer Lock state + +There is an optional callback `layer_lock_set_user()` that gets called when a +layer is locked or unlocked. This is useful to represent the current lock state +for instance by setting an LED. In keymap.c, define + +```c +void layer_lock_set_user(layer_state_t locked_layers) { + // Do something like `set_led(is_layer_locked(NAV));` +} +``` + +The argument `locked_layers` is a bitfield in which the kth bit is on if the kth +layer is locked. Alternatively, you can use `is_layer_locked(layer)` to check if +a given layer is locked. + + +## Combine Layer Lock with a mod-tap + +It is possible to create a [mod-tap MT key](../mod_tap) that acts as a modifier +on hold and Layer Lock on tap. Since Layer Lock is not a [basic +keycode](../keycodes_basic), attempting `MT(mod, QK_LLCK)` is invalid does not +work directly, yet this effect can be achieved through [changing the tap +function](../mod_tap#changing-tap-function). For example, the following +implements a `SFTLLCK` key that acts as Shift on hold and Layer Lock on tap: + +```c +#define SFTLLCK LSFT_T(KC_0) + +// Use SFTLLCK in your keymap... + +bool process_record_user(uint16_t keycode, keyrecord_t *record) { + switch (keycode) { + case SFTLLCK: + if (record->tap.count) { + if (record->event.pressed) { + // Toggle the lock on the highest layer. + layer_lock_invert(get_highest_layer(layer_state)); + } + return false; + } + break; + + // Other macros... + } + return true; +} +``` + +In the above, `KC_0` is an arbitrary placeholder for the tapping keycode. This +keycode will never be sent, so any basic keycode will do. In +`process_record_user()`, the tap press event is changed to to toggle the lock on +the highest layer. Layer Lock can be combined with a [layer-tap LT +key](../feature_layers#switching-and-toggling-layers) similarly. + diff --git a/docs/keycodes.md b/docs/keycodes.md index 4c91d98fa7a4..b99ae78ed400 100644 --- a/docs/keycodes.md +++ b/docs/keycodes.md @@ -375,6 +375,14 @@ See also: [Key Lock](features/key_lock) |---------|--------------------------------------------------------------| |`QK_LOCK`|Hold down the next key pressed, until the key is pressed again| +## Layer Lock {#layer-lock} + +See also: [Layer Lock](features/layer_lock) + +|Key |Aliases |Description | +|---------------|---------|----------------------------------| +|`QK_LAYER_LOCK`|`QK_LLCK`|Locks or unlocks the highest layer| + ## Layer Switching {#layer-switching} See also: [Layer Switching](feature_layers#switching-and-toggling-layers) From 09b0a72333a67f01e9a0f8655394d9bfc36afec9 Mon Sep 17 00:00:00 2001 From: Pascal Getreuer Date: Thu, 13 Jun 2024 21:36:40 -0600 Subject: [PATCH 05/15] Fix imgur links. --- docs/features/layer_lock.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md index 72ad1d95d439..c2af5413a4c4 100644 --- a/docs/features/layer_lock.md +++ b/docs/features/layer_lock.md @@ -35,12 +35,12 @@ layer. Consider a keymap with the following base layer. -![Base layer with a MO(NAV) key.](https://imgur.com/DkEhj9x) +![Base layer with a MO(NAV) key.](https://i.imgur.com/DkEhj9x.png) The highlighted key is a momentary layer switch `MO(NAV)`. Holding it accesses a navigation layer. -![Nav layer wite a Layer Lock key.](https://imgur.com/2wUZNWk) +![Nav layer wite a Layer Lock key.](https://i.imgur.com/2wUZNWk.png) Holding the NAV key is fine for brief use, but awkward to continue holding when From 98c42f98935e84bfc1e3648c5d5d794ad427211c Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Thu, 13 Jun 2024 20:54:50 -0700 Subject: [PATCH 06/15] Use correct callback function --- quantum/layer_lock.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quantum/layer_lock.c b/quantum/layer_lock.c index 3eaf0305b404..9ee3c307dcaa 100644 --- a/quantum/layer_lock.c +++ b/quantum/layer_lock.c @@ -50,7 +50,7 @@ void layer_lock_invert(uint8_t layer) { } else { // Layer is being unlocked. layer_off(layer); } - layer_lock_set_user(locked_layers ^= mask); + layer_lock_set_kb(locked_layers ^= mask); } // Implement layer_lock_on/off by deferring to layer_lock_invert. @@ -69,7 +69,7 @@ void layer_lock_off(uint8_t layer) { void layer_lock_all_off(void) { layer_and(~locked_layers); locked_layers = 0; - layer_lock_set_user(locked_layers); + layer_lock_set_kb(locked_layers); } __attribute__((weak)) bool layer_lock_set_kb(layer_state_t locked_layers) { From 763ed201b2107049fd769bcd1dae3ae7d0ca03e8 Mon Sep 17 00:00:00 2001 From: Drashna Jaelre Date: Fri, 14 Jun 2024 17:22:25 -0700 Subject: [PATCH 07/15] Update include --- quantum/quantum.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantum/quantum.h b/quantum/quantum.h index 2b775510b453..92acc7fff12e 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -241,7 +241,7 @@ extern layer_state_t layer_state; #endif #ifdef LAYER_LOCK_ENABLE -# include "process_layer_lock.h" +# include "layer_lock.h" #endif // LAYER_LOCK_ENABLE void set_single_persistent_default_layer(uint8_t default_layer); From 17ce2c206605cce18cd947d27928e92e86a8aaac Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Tue, 18 Jun 2024 22:37:08 -0700 Subject: [PATCH 08/15] remove moonlander config change --- keyboards/zsa/moonlander/config.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/keyboards/zsa/moonlander/config.h b/keyboards/zsa/moonlander/config.h index 70f776c032fc..08870fba9d5a 100644 --- a/keyboards/zsa/moonlander/config.h +++ b/keyboards/zsa/moonlander/config.h @@ -80,5 +80,3 @@ #define AUDIO_PIN A5 #define AUDIO_PIN_ALT A4 #define AUDIO_PIN_ALT_AS_NEGATIVE - -#define LAYER_LOCK_IDLE_TIMEOUT 60000 From f3dfc4bcaf6f5284e82736bd1557a139d1572012 Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Tue, 18 Jun 2024 22:39:58 -0700 Subject: [PATCH 09/15] Include process layer lock --- quantum/quantum.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quantum/quantum.c b/quantum/quantum.c index 21e9df6f35d4..4a28060b6a3b 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -72,6 +72,10 @@ # include "process_unicode_common.h" #endif +#ifdef LAYER_LOCK_ENABLE +#include "process_layer_lock.h" +#endif // LAYER_LOCK_ENABLE + #ifdef AUDIO_ENABLE # ifndef GOODBYE_SONG # define GOODBYE_SONG SONG(GOODBYE_SOUND) From acaa02da1677008802cd6b767cb79d8d8c04cdff Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Tue, 18 Jun 2024 22:48:12 -0700 Subject: [PATCH 10/15] Appease the lint gods --- quantum/layer_lock.h | 14 ++++++++++---- tests/layer_lock/test_layer_lock.cpp | 1 - 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/quantum/layer_lock.h b/quantum/layer_lock.h index 40abfa1ffd44..6d7285da2a61 100644 --- a/quantum/layer_lock.h +++ b/quantum/layer_lock.h @@ -117,13 +117,19 @@ bool layer_lock_set_kb(layer_state_t locked_layers); bool layer_lock_set_user(layer_state_t locked_layers); void layer_lock_task(void); -#else // NO_ACTION_LAYER -static inline bool is_layer_locked(uint8_t layer) { return false; } +#else // NO_ACTION_LAYER +static inline bool is_layer_locked(uint8_t layer) { + return false; +} static inline void layer_lock_on(uint8_t layer) {} static inline void layer_lock_off(uint8_t layer) {} static inline void layer_lock_all_off(void) {} static inline void layer_lock_invert(uint8_t layer) {} -static inline bool layer_lock_set_kb(layer_state_t locked_layers) { return true; } -static inline bool layer_lock_set_user(layer_state_t locked_layers) { return true; } +static inline bool layer_lock_set_kb(layer_state_t locked_layers) { + return true; +} +static inline bool layer_lock_set_user(layer_state_t locked_layers) { + return true; +} static inline void layer_lock_task(void) {} #endif // NO_ACTION_LAYER diff --git a/tests/layer_lock/test_layer_lock.cpp b/tests/layer_lock/test_layer_lock.cpp index 76185fdb2d7a..f6f47dff206a 100644 --- a/tests/layer_lock/test_layer_lock.cpp +++ b/tests/layer_lock/test_layer_lock.cpp @@ -112,7 +112,6 @@ TEST_F(LayerLock, LayerLockOneshotHoldTest) { VERIFY_AND_CLEAR(driver); } - TEST_F(LayerLock, LayerLockTimeoutTest) { TestDriver driver; KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); From 61f0048fabddbc958b6e14a35fa55b62fa884a04 Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Tue, 18 Jun 2024 22:49:06 -0700 Subject: [PATCH 11/15] More appeasement --- quantum/quantum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantum/quantum.c b/quantum/quantum.c index 4a28060b6a3b..fd69fa81e907 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -73,7 +73,7 @@ #endif #ifdef LAYER_LOCK_ENABLE -#include "process_layer_lock.h" +# include "process_layer_lock.h" #endif // LAYER_LOCK_ENABLE #ifdef AUDIO_ENABLE From 351f16d81100ea78dff160a961cb0a2104344c63 Mon Sep 17 00:00:00 2001 From: Pascal Getreuer <50221757+getreuer@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:10:36 -0700 Subject: [PATCH 12/15] Elaborate Layer Lock tests. (#90) --- docs/features/layer_lock.md | 3 +- tests/layer_lock/test_layer_lock.cpp | 147 ++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md index c2af5413a4c4..b6c1091ca55a 100644 --- a/docs/features/layer_lock.md +++ b/docs/features/layer_lock.md @@ -88,8 +88,9 @@ layer is locked or unlocked. This is useful to represent the current lock state for instance by setting an LED. In keymap.c, define ```c -void layer_lock_set_user(layer_state_t locked_layers) { +bool layer_lock_set_user(layer_state_t locked_layers) { // Do something like `set_led(is_layer_locked(NAV));` + return true; } ``` diff --git a/tests/layer_lock/test_layer_lock.cpp b/tests/layer_lock/test_layer_lock.cpp index f6f47dff206a..00742c3b4361 100644 --- a/tests/layer_lock/test_layer_lock.cpp +++ b/tests/layer_lock/test_layer_lock.cpp @@ -8,6 +8,62 @@ using testing::_; class LayerLock : public TestFixture {}; +TEST_F(LayerLock, LayerLockState) { + TestDriver driver; + KeymapKey key_a = KeymapKey(0, 0, 0, KC_A); + KeymapKey key_b = KeymapKey(1, 0, 0, KC_B); + KeymapKey key_c = KeymapKey(2, 0, 0, KC_C); + KeymapKey key_d = KeymapKey(3, 0, 0, KC_C); + + set_keymap({key_a, key_b, key_c, key_d}); + + EXPECT_FALSE(is_layer_locked(1)); + EXPECT_FALSE(is_layer_locked(2)); + EXPECT_FALSE(is_layer_locked(3)); + + layer_lock_invert(1); // Layer 1: unlocked -> locked + layer_lock_on(2); // Layer 2: unlocked -> locked + layer_lock_off(3); // Layer 3: stays unlocked + + // Layers 1 and 2 are now on. + EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(layer_state_is(2)); + // Layers 1 and 2 are now locked. + EXPECT_TRUE(is_layer_locked(1)); + EXPECT_TRUE(is_layer_locked(2)); + EXPECT_FALSE(is_layer_locked(3)); + + layer_lock_invert(1); // Layer 1: locked -> unlocked + layer_lock_on(2); // Layer 2: stays locked + layer_lock_on(3); // Layer 3: unlocked -> locked + + EXPECT_FALSE(layer_state_is(1)); + EXPECT_TRUE(layer_state_is(2)); + EXPECT_TRUE(layer_state_is(3)); + EXPECT_FALSE(is_layer_locked(1)); + EXPECT_TRUE(is_layer_locked(2)); + EXPECT_TRUE(is_layer_locked(3)); + + layer_lock_invert(1); // Layer 1: unlocked -> locked + layer_lock_off(2); // Layer 2: locked -> unlocked + + EXPECT_TRUE(layer_state_is(1)); + EXPECT_FALSE(layer_state_is(2)); + EXPECT_TRUE(layer_state_is(3)); + EXPECT_TRUE(is_layer_locked(1)); + EXPECT_FALSE(is_layer_locked(2)); + EXPECT_TRUE(is_layer_locked(3)); + + layer_lock_all_off(); // Layers 1 and 3: locked -> unlocked + + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(layer_state_is(2)); + EXPECT_FALSE(layer_state_is(3)); + EXPECT_FALSE(is_layer_locked(1)); + EXPECT_FALSE(is_layer_locked(2)); + EXPECT_FALSE(is_layer_locked(3)); +} + TEST_F(LayerLock, LayerLockMomentaryTest) { TestDriver driver; KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); @@ -17,22 +73,32 @@ TEST_F(LayerLock, LayerLockMomentaryTest) { set_keymap({key_layer, key_a, key_trns, key_ll}); - /* Press Lower. */ EXPECT_NO_REPORT(driver); key_layer.press(); run_one_scan_loop(); EXPECT_TRUE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); EXPECT_NO_REPORT(driver); tap_key(key_ll); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); EXPECT_NO_REPORT(driver); key_layer.release(); run_one_scan_loop(); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + // Pressing Layer Lock again unlocks the lock. + EXPECT_NO_REPORT(driver); + key_ll.press(); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); } @@ -45,7 +111,6 @@ TEST_F(LayerLock, LayerLockLayerTapTest) { set_keymap({key_layer, key_a, key_trns, key_ll}); - /* Press Lower. */ EXPECT_NO_REPORT(driver); key_layer.press(); idle_for(TAPPING_TERM); @@ -56,6 +121,15 @@ TEST_F(LayerLock, LayerLockLayerTapTest) { EXPECT_NO_REPORT(driver); tap_key(key_ll); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + // Pressing Layer Lock again unlocks the lock. + EXPECT_NO_REPORT(driver); + key_ll.press(); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); } @@ -68,7 +142,6 @@ TEST_F(LayerLock, LayerLockOneshotTapTest) { set_keymap({key_layer, key_a, key_trns, key_ll}); - /* Press Lower. */ EXPECT_NO_REPORT(driver); tap_key(key_layer); run_one_scan_loop(); @@ -79,6 +152,15 @@ TEST_F(LayerLock, LayerLockOneshotTapTest) { tap_key(key_ll); run_one_scan_loop(); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + // Pressing Layer Lock again unlocks the lock. + EXPECT_NO_REPORT(driver); + key_ll.press(); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); } @@ -91,7 +173,6 @@ TEST_F(LayerLock, LayerLockOneshotHoldTest) { set_keymap({key_layer, key_a, key_trns, key_ll}); - /* Press Lower. */ EXPECT_NO_REPORT(driver); key_layer.press(); idle_for(TAPPING_TERM); @@ -109,6 +190,15 @@ TEST_F(LayerLock, LayerLockOneshotHoldTest) { key_layer.release(); run_one_scan_loop(); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + // Pressing Layer Lock again unlocks the lock. + EXPECT_NO_REPORT(driver); + key_ll.press(); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); } @@ -121,7 +211,6 @@ TEST_F(LayerLock, LayerLockTimeoutTest) { set_keymap({key_layer, key_a, key_trns, key_ll}); - /* Press Lower. */ EXPECT_NO_REPORT(driver); key_layer.press(); run_one_scan_loop(); @@ -137,11 +226,59 @@ TEST_F(LayerLock, LayerLockTimeoutTest) { key_layer.release(); run_one_scan_loop(); EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); EXPECT_NO_REPORT(driver); idle_for(LAYER_LOCK_IDLE_TIMEOUT); run_one_scan_loop(); EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(LayerLock, ToKeyOverridesLayerLock) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); + KeymapKey key_to0 = KeymapKey(1, 0, 0, TO(0)); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_to0, key_ll}); + + EXPECT_NO_REPORT(driver); + layer_lock_on(1); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + tap_key(key_to0); // TO(0) overrides Layer Lock and unlocks layer 1. + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(LayerLock, LayerClearOverridesLayerLock) { + TestDriver driver; + KeymapKey key_layer = KeymapKey(0, 0, 0, MO(1)); + KeymapKey key_a = KeymapKey(0, 1, 0, KC_A); + KeymapKey key_ll = KeymapKey(1, 1, 0, QK_LAYER_LOCK); + + set_keymap({key_layer, key_a, key_ll}); + + EXPECT_NO_REPORT(driver); + layer_lock_on(1); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + EXPECT_TRUE(is_layer_locked(1)); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_A)); + layer_clear(); // layer_clear() overrides Layer Lock and unlocks layer 1. + key_a.press(); + run_one_scan_loop(); + EXPECT_FALSE(layer_state_is(1)); + EXPECT_FALSE(is_layer_locked(1)); VERIFY_AND_CLEAR(driver); } From 04217d717a147ad0f96c92e67d70f8e5161595ce Mon Sep 17 00:00:00 2001 From: Drashna Jaelre Date: Sun, 18 Aug 2024 12:15:26 -0700 Subject: [PATCH 13/15] Fix typo Co-authored-by: Daniel <1767914+iamdanielv@users.noreply.github.com> --- docs/features/layer_lock.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md index b6c1091ca55a..c1f90544c847 100644 --- a/docs/features/layer_lock.md +++ b/docs/features/layer_lock.md @@ -40,7 +40,7 @@ Consider a keymap with the following base layer. The highlighted key is a momentary layer switch `MO(NAV)`. Holding it accesses a navigation layer. -![Nav layer wite a Layer Lock key.](https://i.imgur.com/2wUZNWk.png) +![Nav layer with a Layer Lock key.](https://i.imgur.com/2wUZNWk.png) Holding the NAV key is fine for brief use, but awkward to continue holding when From 4d57b5204dc5f5db608860634b0140f83a4f82ef Mon Sep 17 00:00:00 2001 From: Drashna Jael're Date: Thu, 29 Aug 2024 20:34:18 -0700 Subject: [PATCH 14/15] Update keycodes --- data/constants/keycodes/keycodes_0.0.5_quantum.hjson | 11 ----------- data/constants/keycodes/keycodes_0.0.6_quantum.hjson | 7 +++++++ 2 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 data/constants/keycodes/keycodes_0.0.5_quantum.hjson diff --git a/data/constants/keycodes/keycodes_0.0.5_quantum.hjson b/data/constants/keycodes/keycodes_0.0.5_quantum.hjson deleted file mode 100644 index 63153daa73c8..000000000000 --- a/data/constants/keycodes/keycodes_0.0.5_quantum.hjson +++ /dev/null @@ -1,11 +0,0 @@ -{ - "keycodes": { - "0x7C7B": { - "group": "quantum", - "key": "QK_LAYER_LOCK", - "aliases": [ - "QK_LLCK" - ] - } - } -} diff --git a/data/constants/keycodes/keycodes_0.0.6_quantum.hjson b/data/constants/keycodes/keycodes_0.0.6_quantum.hjson index 814cd66a5b12..be3285f9e437 100644 --- a/data/constants/keycodes/keycodes_0.0.6_quantum.hjson +++ b/data/constants/keycodes/keycodes_0.0.6_quantum.hjson @@ -3,5 +3,12 @@ "0x7C20": "!delete!", // old QK_OUTPUT_AUTO "0x7C21": "!delete!", // old QK_OUTPUT_USB "0x7C22": "!delete!", // old QK_OUTPUT_BLUETOOTH + "0x7C7B": { + "group": "quantum", + "key": "QK_LAYER_LOCK", + "aliases": [ + "QK_LLCK" + ] + } } } From 3114c9f3768bcfe07a2db54152892c12d00e0140 Mon Sep 17 00:00:00 2001 From: Drashna Jaelre Date: Thu, 29 Aug 2024 21:57:46 -0700 Subject: [PATCH 15/15] Fix typo in docs because drashna never makes spelling mistakes! Co-authored-by: Daniel <1767914+iamdanielv@users.noreply.github.com> --- docs/features/layer_lock.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md index c1f90544c847..aaf323acccde 100644 --- a/docs/features/layer_lock.md +++ b/docs/features/layer_lock.md @@ -133,7 +133,7 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { In the above, `KC_0` is an arbitrary placeholder for the tapping keycode. This keycode will never be sent, so any basic keycode will do. In -`process_record_user()`, the tap press event is changed to to toggle the lock on +`process_record_user()`, the tap press event is changed to toggle the lock on the highest layer. Layer Lock can be combined with a [layer-tap LT key](../feature_layers#switching-and-toggling-layers) similarly.