Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Add Layer Lock feature #23430

Merged
merged 21 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions builddefs/generic_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ GENERIC_FEATURES = \
HAPTIC \
KEY_LOCK \
KEY_OVERRIDE \
LAYER_LOCK \
LEADER \
MAGIC \
MOUSEKEY \
Expand Down
7 changes: 7 additions & 0 deletions data/constants/keycodes/keycodes_0.0.6_quantum.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
}
3 changes: 3 additions & 0 deletions data/mappings/info_config.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
6 changes: 6 additions & 0 deletions data/schemas/keyboard.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,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"}
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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" },
Expand Down
3 changes: 3 additions & 0 deletions docs/feature_layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
139 changes: 139 additions & 0 deletions docs/features/layer_lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# 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://i.imgur.com/DkEhj9x.png)

The highlighted key is a momentary layer switch `MO(NAV)`. Holding it accesses a
navigation layer.

![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
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
bool layer_lock_set_user(layer_state_t locked_layers) {
// Do something like `set_led(is_layer_locked(NAV));`
return true;
}
```

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 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.

8 changes: 8 additions & 0 deletions docs/keycodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions quantum/keyboard.c
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#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) {
Expand Down Expand Up @@ -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. */
Expand Down
6 changes: 4 additions & 2 deletions quantum/keycodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,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,
Expand Down Expand Up @@ -1444,6 +1445,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
Expand Down Expand Up @@ -1500,7 +1502,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)

Expand All @@ -1526,6 +1528,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
81 changes: 81 additions & 0 deletions quantum/layer_lock.c
Original file line number Diff line number Diff line change
@@ -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_kb(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_kb(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
Loading