diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk index 4e058dcd2659..00649f734888 100644 --- a/builddefs/generic_features.mk +++ b/builddefs/generic_features.mk @@ -30,6 +30,7 @@ GENERIC_FEATURES = \ HAPTIC \ KEY_LOCK \ KEY_OVERRIDE \ + LAYER_LOCK \ LEADER \ PROGRAMMABLE_BUTTON \ REPEAT_KEY \ diff --git a/data/constants/keycodes/keycodes_0.0.4.hjson b/data/constants/keycodes/keycodes_0.0.4.hjson new file mode 100644 index 000000000000..63153daa73c8 --- /dev/null +++ b/data/constants/keycodes/keycodes_0.0.4.hjson @@ -0,0 +1,11 @@ +{ + "keycodes": { + "0x7C7B": { + "group": "quantum", + "key": "QK_LAYER_LOCK", + "aliases": [ + "QK_LLCK" + ] + } + } +} 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..e69de29bb2d1 diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index ab9a4a0e4527..cdd14e33fbdc 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -49,6 +49,9 @@ "DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"}, "DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"}, + // 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 e32b88133d1a..b79586ad092b 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -271,6 +271,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/quantum/keyboard.c b/quantum/keyboard.c index bb44576ba1b5..912087f02437 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -141,6 +141,9 @@ along with this program. If not, see . #ifdef ACHORDION_ENABLE # include "process_achordion.h" #endif +#if defined(LAYER_LOCK_ENABLE) && LAYER_LOCK_IDLE_TIMEOUT > 0 +# include "layer_lock.h" +#endif static uint32_t last_input_modification_time = 0; uint32_t last_input_activity_time(void) { @@ -643,6 +646,10 @@ void quantum_task(void) { #ifdef ACHORDION_ENABLE achordion_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 bbf10da36d97..d6011d594ecf 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -1,4 +1,4 @@ -// Copyright 2023 QMK +// Copyright 2024 QMK // SPDX-License-Identifier: GPL-2.0-or-later /******************************************************************************* @@ -723,6 +723,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, @@ -1366,6 +1367,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 @@ -1417,6 +1419,6 @@ enum qk_keycode_defines { #define IS_MACRO_KEYCODE(code) ((code) >= QK_MACRO_0 && (code) <= QK_MACRO_31) #define IS_BACKLIGHT_KEYCODE(code) ((code) >= QK_BACKLIGHT_ON && (code) <= QK_BACKLIGHT_TOGGLE_BREATHING) #define IS_RGB_KEYCODE(code) ((code) >= RGB_TOG && (code) <= RGB_MODE_TWINKLE) -#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) 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 00b4de6b1e58..973fe57045d8 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -72,6 +72,9 @@ #ifdef VELOCIKEY_ENABLE # include "velocikey.h" #endif +#ifdef LAYER_LOCK_ENABLE +# include "process_layer_lock.h" +#endif #ifdef AUDIO_ENABLE # ifndef GOODBYE_SONG @@ -397,6 +400,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 30e86b43007f..4503f356494b 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -234,7 +234,9 @@ extern layer_state_t layer_state; # include "repeat_key.h" # include "process_repeat_key.h" #endif - +#ifdef LAYER_LOCK_ENABLE +# include "layer_lock.h" +#endif 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 9ed80cdbcf66..ac4530bb8867 100644 --- a/tests/test_common/keycode_table.cpp +++ b/tests/test_common/keycode_table.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 QMK +// Copyright 2024 QMK // SPDX-License-Identifier: GPL-2.0-or-later /******************************************************************************* @@ -665,6 +665,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"},