qmk_firmware
qmk_firmware copied to clipboard
[Core] Feature: pointing device automatic mouse layer
Description
This takes an automatic mouse layer feature that @drashna originally created and adds it to core with some added flexibility. This will automatically activate a target layer (chosen at compile and can change during run time) as soon as the mouse cursor is moved and deactivate the layer after a set time. If a keyrecord that is defined as a mouse key is pressed then the layer is held as long as the key is pressed and the timer is reset on key up.
This handles all of the normal layer keys that activate the same layer as auto mouse is set to (tap toggling, one_shot, layer tap etc.) treating them as mouse keys and holding layer on toggle. If a non-mouse key is pressed then the layer is deactivated early. Mod & mod tap keys are ignored (i.e. don't hold layer/reset timer but do not deactivate either) allowing for shift/ctrl clicks etc.
Relevant excerpt from modified feature_pointing_device.md
:
Automatic Mouse Layer :id=pointing-device-auto-mouse
When using a pointing device combined with a keyboard the mouse buttons are often kept on a separate layer from the default keyboard layer, which requires pressing or holding a key to change layers before using the mouse. To make this easier and more efficient an additional pointing device feature may be enabled that will automatically activate a target layer as soon as the pointing device is active (in motion, mouse button pressed etc.) and deactivate the target layer after a set time.
Additionally if any key that is defined as a mouse key is pressed then the layer will be held as long as the key is pressed and the timer will be reset on key release. When a non-mouse key is pressed then the layer is deactivated early (with some exceptions see below). Mod, mod tap, and one shot mod keys are ignored (i.e. don't hold or activate layer but do not deactivate the layer either) when sending a modifier keycode (e.g. hold for mod tap) allowing for mod keys to be used with the mouse without activating the target layer when typing.
All of the standard layer keys (tap toggling, toggle, toggle on, one_shot, layer tap, layer mod) that activate the current target layer are uniquely handled to ensure they behave as expected (see layer key table below). The target layer that can be changed at any point during by calling the set_auto_mouse_layer(<new_target_layer>);
function.
Behaviour of Layer keys that activate the target layer
Layer key as in keymap.c |
Auto Mouse specific behaviour |
---|---|
MO(<target_layer>) |
Treated as a mouse key holding the layer while pressed |
LT(<target_layer>) |
When tapped will be treated as non mouse key and mouse key when held |
LM(<target_layer>) |
Treated as a mouse key |
TG(<target_layer>) |
Will set flag preventing target layer deactivation or removal until pressed again |
TO(<target_layer>) |
Same as TG(<target_layer>) |
TT(<target_layer>) |
Treated as a mouse key when tap.count < TAPPING_TOGGLE and as TG when tap.count == TAPPING_TOGGLE |
DF(<target_layer>) |
Skips auto mouse key processing similar to mod keys |
OSL(<target_layer>) |
Skips, but if current one shot layer is the target layer then it will prevent target layer deactivation or removal |
How to enable:
// in config.h:
#define POINTING_DEVICE_AUTO_MOUSE_ENABLE
// only required if not setting mouse layer elsewhere
#define AUTO_MOUSE_DEFAULT_LAYER <index of your mouse layer>
// in keymap.c:
void pointing_device_init_user(void) {
set_auto_mouse_layer(<mouse_layer>); // only required if AUTO_MOUSE_DEFAULT_LAYER is not set to index of <mouse_layer>
set_auto_mouse_enable(true); // always required before the auto mouse feature will work
}
Because the auto mouse feature can be disabled/enabled during runtime and starts as disabled by default it must be enabled by calling set_auto_mouse_enable(true);
somewhere in firmware before the feature will work.
Note: for setting the target layer during initialization either setting AUTO_MOUSE_DEFAULT_LAYER
in config.h
or calling set_auto_mouse_layer(<mouse_layer>)
can be used.
How to Customize:
There are a few ways to control the auto mouse feature with both config.h
options and functions for controlling it during runtime.
config.h
Options:
Define | Description | Range | Units | Default |
---|---|---|---|---|
POINTING_DEVICE_AUTO_MOUSE_ENABLE |
(Required) Enables auto mouse layer feature | None | Not defined | |
AUTO_MOUSE_DEFAULT_LAYER |
(Optional) Index of layer to use as default target layer | 0 - LAYER_MAX |
uint8_t |
1 |
AUTO_MOUSE_TIME |
(Optional) Time layer remains active after activation | ideal (250-1000) | ms | 650 ms |
AUTO_MOUSE_DELAY |
(Optional) Lockout time after non-mouse key is pressed | ideal (100-1000) | ms | TAPPING_TERM or 200 ms |
AUTO_MOUSE_DEBOUNCE |
(Optional) Time delay from last activation to next update | ideal (10 - 100) | ms | 25 ms |
Adding mouse keys
While all default mouse keys and layer keys(for current mouse layer) are treated as mouse keys, additional Keyrecords can be added to mouse keys by adding them to the is_mouse_record_* stack.
Callbacks for setting up additional key codes as mouse keys:
Callback | Description |
---|---|
bool is_mouse_record_kb(uint16_t keycode, keyrecord_t* record) |
keyboard level callback for adding mouse keys |
bool is_mouse_record_user(uint16_t keycode, keyrecord_t* record) |
user/keymap level callback for adding mouse keys |
To use the callback function to add mouse keys:
The following code will cause the enter key and all of the arrow keys to be treated as mouse keys (hold target layer while they are pressed and reset active layer timer).
// in <keyboard>.c:
bool is_mouse_record_kb(uint16_t keycode, keyrecord_t* record) {
switch(keycode) {
case KC_ENT:
return true;
case KC_RIGHT ... KC_UP:
return true;
default:
return false;
}
return is_mouse_record_user(keycode, record);
}
Advanced control
There are several functions that allow for more advanced interaction with the auto mouse feature allowing for greater control.
Functions to control auto mouse enable and target layer:
Function | Description | Aliases | Return type |
---|---|---|---|
set_auto_mouse_enable(bool enable) |
Enable or disable auto mouse (true:enable, false:disable) | void (None) |
|
get_auto_mouse_enable(void) |
Return auto mouse enable state (true:enabled, false:disabled) | AUTO_MOUSE_ENABLED |
bool |
set_auto_mouse_layer(uint8_t LAYER) |
Change/set the target layer for auto mouse | void (None) |
|
get_auto_mouse_layer(void) |
Return auto mouse target layer index | AUTO_MOUSE_TARGET_LAYER |
uint8_t |
remove_auto_mouse_layer(layer_state_t state, bool force) |
Return state with target layer removed if appropriate (ignore criteria if force ) |
layer_state_t |
|
auto_mouse_layer_off(void) |
Disable target layer if appropriate will call (makes call to layer_state_set ) |
void (None) |
|
auto_mouse_toggle(void) |
Toggle on/off target toggle state (disables layer deactivation when true) | void (None) |
|
get_auto_mouse_toggle(void) |
Return value of toggling state variable | bool |
NOTES:
- Due to the nature of how some functions work, the auto_mouse_trigger_reset
, and auto_mouse_layer_off
functions should never be called in the layer_state_set_*
stack as this can cause indefinite loops.
- It is recommended that remove_auto_mouse_layer
is used in the layer_state_set_*
stack of functions and auto_mouse_layer_off
is used everywhere else
- remove_auto_mouse_layer(state, false)
or auto_mouse_layer_off()
should be called before any instance of set_auto_mouse_enabled(false)
or set_auto_mouse_layer(layer)
to ensure that the target layer will be removed appropriately before disabling auto mouse to avoid a stuck layer
Functions for handling custom key events:
Function | Description | Return type |
---|---|---|
auto_mouse_keyevent(bool pressed) |
Auto mouse mouse key event (true: key down, false: key up) | void (None) |
auto_mouse_trigger_reset(bool pressed) |
Reset auto mouse status on key down and start delay timer (non-mouse key event) | void (None) |
auto_mouse_toggle(void) |
Toggle on/off target toggle state (disables layer deactivation when true) | void (None) |
get_auto_mouse_toggle(void) |
Return value of toggling state variable | bool |
_NOTE: Generally it would be preferable to use the is_mouse_record_* functions to add key records to _ |
Advanced control examples
Disable auto mouse on certain layers:
The auto mouse feature can be disabled any time and this can be helpful if you want to disable the auto mouse feature under certain circumstances such as when particular layers are active. the following function would disable the auto_mouse feature whenever the layers _LAYER5
through _LAYER7
are active as the top most layer (ignoring target layer).
// in keymap.c:
layer_state_t layer_state_set_user(layer_state_t state) {
// checks highest layer other than target layer
switch(get_highest_layer(remove_auto_mouse_layer(state, true))) {
case _LAYER5 ... _LAYER7:
// remove_auto_mouse_target must be called to adjust state *before* setting enable
state = remove_auto_mouse_layer(state, false);
set_auto_mouse_enable(false);
break;
default:
set_auto_mouse_enable(true);
break;
}
// recommend that any code that makes adjustment based on auto mouse layer state would go here
return state;
}
Set different target layer when a particular layer is active:
The below code will change the auto mouse layer target to _MOUSE_LAYER_2
when _DEFAULT_LAYER_2
is highest default layer state.
NOTE: that auto_mouse_layer_off
is used here instead of remove_auto_mouse_layer
as default_layer_state_set_*
stack is separate from the layer_state_set_*
stack
ADDITIONAL NOTE: AUTO_MOUSE_TARGET_LAYER
is checked if already set to avoid deactivating the target layer unless needed
// in keymap.c
layer_state_t default_layer_state_set_user(layer_state_t state) {
// switch on change in default layer need to check if target layer already set to avoid turning off layer needlessly
switch(get_highest_layer(state)) {
case _DEFAULT_LAYER_2:
if ((AUTO_MOUSE_TARGET_LAYER) == _MOUSE_LAYER_2) break;
auto_mouse_layer_off();
set_auto_mouse_layer(_MOUSE_LAYER_2);
break;
default:
if((AUTO_MOUSE_TARGET_LAYER) == _MOUSE_LAYER_1) break;
auto_mouse_layer_off();
set_auto_mouse_layer(_MOUSE_LAYER_1);
}
return state;
}
Use custom keys to control auto mouse:
Custom key records could be created that control the auto mouse feature.
The code example below would create a custom key that would toggle the auto mouse feature on and off when pressed while also setting a bool that could be used to disable other code that may turn it on such as the layer code above.
// in config.h:
enum user_custom_keycodes {
AM_Toggle = SAFE_RANGE
};
// in keymap.c:
bool process_record_user(uint16_t keycode, keyrecord_t* record) {
switch (keycode) {
// toggle auto mouse enable key
case AM_Toggle:
if(record->event.pressed) { // key down
auto_mouse_layer_off(); // disable target layer if needed
set_auto_mouse_enabled((AUTO_MOUSE_ENABLED) ^ 1);
} // do nothing on key up
return false; // prevent further processing of keycode
}
}
Customize Target Layer Activation
Layer activation can be customized by overwriting the auto_mouse_activation
function. This function is checked every pointing_device_task
cycle when inactive and every AUTO_MOUSE_DEBOUNCE
ms when active and evaluates pointing device conditions that trigger target layer activation. When it returns true, the target layer will be activated barring the usual exceptions (e.g. delay time has not expired).
By default it will return true if any of the mouse_report axes x
,y
,h
,v
are non zero or if there is any mouse buttons active in mouse_report
.
Note: The Cirque pinnacle track pad already implements a custom activation function that will activate on touchdown as well as movement, currently this only works for the master side of split keyboards.
Function | Description | Return type |
---|---|---|
auto_mouse_activation(report_mouse_t mouse_report) |
Overwritable function that controls target layer activation (when true) | bool |
Auto Mouse for Custom Pointing Device Task
When using a custom pointing device (overwriting pointing_device_task
) the following code should be somewhere in the pointing_device_task_*
stack:
void pointing_device_task(void) {
//...Custom pointing device task code
// handle automatic mouse layer (needs report_mouse_t as input)
pointing_device_task_auto_mouse(local_mouse_report);
//...More custom pointing device task code
pointing_device_send();
}
In general the following two functions must be implemented in appropriate locations for auto mouse to function:
Function | Description | Suggested location |
---|---|---|
pointing_device_task_auto_mouse(report_mouse_t mouse_report) |
handles target layer activation and is_active status updates | pointing_device_task stack |
process_auto_mouse(uint16_t keycode, keyrecord_t* record) |
Keycode processing for auto mouse | process_record stack |
Types of Changes
- [X] Core
- [ ] Bugfix
- [X] New feature
- [ ] Enhancement/optimization
- [ ] Keyboard (addition or update)
- [ ] Keymap/layout/userspace (addition or update)
- [X] Documentation
Checklist
- [X] My code follows the code style of this project: C, Python
- [X] I have read the PR Checklist document and have made the appropriate changes.
- [X] My change requires a change to the documentation.
- [X] I have updated the documentation accordingly.
- [X] I have read the CONTRIBUTING document.
- [ ] I have added tests to cover my changes.
- [X] I have tested the changes and verified that they work and don't break anything (as well as I can manage).
This works really well for me in that it lessens the time setting up auto mouse layer for new boards.
Added some external functions to handle creating mouse key events for use in actions that are not key presses (such as trackpad taps etc.) will fix linting once I am sure I will make no further changes.
That should handle all of the minor issues mentioned so far and should make for more consistent mod/layer tap key behavior. Was getting mouse layer deactivation when a non mouse key was pressed while holding layer key for the mouse layer, should be fixed now.
The latest updates change a few things:
- Initial documentation is complete
- One shot layer handling should be improved but is still untested. Was based on reread of one shot code in
action.c
andaction_utils.c
- QK_MODS now pass through to default behavior leaving for them to be defined as mouse keys or not by user/keyboard.
- Non mouse key handling has been improved and delay timer should work as expected:
- as of last non mouse key pressed (without a mouse key being held) there will be
AUTO_MOUSE_DELAY_TIME
ms before auto mouse will activate the target layer.
- as of last non mouse key pressed (without a mouse key being held) there will be
- mouse_key_tracker has been switched to signed integer with checking for values <0 in order to catch some possible rare errors
- Such as mouse_key_tracker is decremented more than it is incremented due to missed key downs or extra key ups due to chattering etc.
- This will not the occasions where there are extra key downs or missed key up events but it remains to be seen how likely these are to occur with decent de-bounce settings.
I'm wondering how one would trigger or hold mouse layer with finger/hand presence instead of movement (e.g. Z axis on trackpad or maybe by using a proximity sensor near the pointing device area).
Would it be good to expose a function to "force start" auto mouse? Expand report_mouse_t
to contain this information (at least within quantum/pointing_device/)? Or use a different mechanism altogether?
I'm wondering how one would trigger or hold mouse layer with finger/hand presence instead of movement (e.g. Z axis on trackpad or maybe by using a proximity sensor near the pointing device area). Would it be good to expose a function to "force start" auto mouse? Expand
report_mouse_t
to contain this information (at least within quantum/pointing_device/)? Or use a different mechanism altogether?
Hmm this is an interesting idea I was just thinking that I would also like to be checking for scroll movement to turn on the layer.
Could be useful to add a bool to report_mouse_t
for is_active
or is_moving
and have auto mouse be listening for that instead.
is_active
could be controlled with some functions added to pointing_device_drivers
if we want it driver specific or just in pointing_device
if it is to be left general.
would want to get @drashna 's opinion on this before making moves since it would be a more invasive code change.
For now I can just expose a function that handles layer activation that can be overwritten if need be.
There, auto_mouse_activation
can control when the layer will be activated, (defaults to: x || y || v || h !=0) this can be overwritten to trigger on other conditions such as when a particular driver is selected or for something keyboard specific.
@dkao does this work for the use cases you were thinking of?
@Alabastard-64 Thanks! This works pretty well.
One issue I found: hitting a non-mouse key breaks out of the mouse layer, even when cursor is still moving, or in my case with an overridden auto_mouse_activation()
when my finger is still on the trackpad; mouse layer is then reactivated after timeout and ping-pongs like this every time I hit a non-mouse key. Would it be possible to suppress the non-mouse key deactivation while auto_mouse_activation()
returns true? Or does that contradict the design intent?
@Alabastard-64 Thanks! This works pretty well. One issue I found: hitting a non-mouse key breaks out of the mouse layer, even when cursor is still moving, or in my case with an overridden
auto_mouse_activation()
when my finger is still on the trackpad; mouse layer is then reactivated after timeout and ping-pongs like this every time I hit a non-mouse key. Would it be possible to suppress the non-mouse key deactivation whileauto_mouse_activation()
returns true? Or does that contradict the design intent?
This is partially intentionally, especially as this is based on my code originally.
Though, yeah, it may be worth adding a check to not turn off the layer if any events are actively triggered. (eg, current motion, key pressed, etc)
Having auto_mouse_activation()
prevent layer deactivation while true could work, and I think makes sense without going against intent.
Shouldn't have too much of an impact on the normal behavior either as it would only be true during current active movement (until the next pointing_device update).
Will update the code as soon as I can compile and test.
@dkao could you send the custom auto_mouse_activation()
code to me (or post it) so I could add it to the docs as a code example?
Sure, here's my change for a custom auto_mouse_activation()
in the trackpad driver:
diff --git a/quantum/pointing_device/pointing_device_drivers.c b/quantum/pointing_device/pointing_device_drivers.c
index b96f8ff4b3..79e5f0e862 100644
--- a/quantum/pointing_device/pointing_device_drivers.c
+++ b/quantum/pointing_device/pointing_device_drivers.c
@@ -117,6 +117,14 @@ void cirque_pinnacle_configure_cursor_glide(float trigger_px) {
# endif
# if CIRQUE_PINNACLE_POSITION_MODE
+# ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+static bool is_touch_down;
+
+bool auto_mouse_activation(report_mouse_t mouse_report) {
+ return is_touch_down || mouse_report.x != 0 || mouse_report.y != 0 || mouse_report.h != 0 || mouse_report.v != 0;
+}
+# endif
+
report_mouse_t cirque_pinnacle_get_report(report_mouse_t mouse_report) {
pinnacle_data_t touchData = cirque_pinnacle_read_data();
mouse_xy_report_t report_x = 0, report_y = 0;
@@ -146,6 +154,10 @@ report_mouse_t cirque_pinnacle_get_report(report_mouse_t mouse_report) {
}
# endif
+# ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ is_touch_down = touchData.touchDown;
+# endif
+
// Scale coordinates to arbitrary X, Y resolution
cirque_pinnacle_scale_data(&touchData, cirque_pinnacle_get_scale(), cirque_pinnacle_get_scale());
This snippet is probably not a great example for the docs though, unless this change is included and the docs say "see Cirque Pinnacle absolute mode driver for an example implementation".
EDIT: I just realized this wouldn't work across split halves... Overridable function is still good, having an extended structure carry report_mouse_t
and is_active
would make split transactions cleaner.
Since that is just part of the pointing_device_drivers.c
I could add it as part of this PR and would be in scope I think. Maybe with a note in the docs that trackpad touch sense activation can only work with trackpads on the master half currently.
Good point on the is_active
being cleaner for a few reasons. However, I'm starting to think changing report_mouse_t
might be a bit out of scope for this PR. Perhaps a separate PR for allowing for an in_motion
or is_active
to be added to report_mouse_t
with the justification that it adds more control and feature possibilities, then I could update auto mouse once that is merged.
Sounds good. Will propose the report_mouse_t
in a separate PR, I've got another feature that could also benefit from it.
Reasonably major update with pretty wide changes but testing okay fixed major issue with overridden keys or any key returning false in the process_record
stack which would skip the auto_mouse key record processing.
pretty big update again but I think this should be the last update unless anyone notices any glaring issues or needed features. Something I missed in the update notes:
- Changed keyevent functions so they would be easier to use outside of
process_record_*
context (using pressed bool instead of keyrecord pointer)
bug was found in use of set_auto_mouse_state
in layer_state_set
stack (which is intended as stated in the docs) testing a fix now should commit soon if it works.
may fix a number of other bugs that were happening before so may be able to reduce code size a bit more if this works as intended.
The new version should handle all of the bugs I have encountered thus far and seems to be working great. I have renamed some key functions for a couple of reasons:
- The new names are a little more clear
- For anyone using this feature prior to merge they should review the updated doc and any code involving these functions as changes may need to be made to work correctly
The current version works great for me without any major issues so I will consider this final unless anyone notices any issue or has a good idea for improving or extending the feature. There is however two minor issues that would ideally be addressed prior to merge:
- Currently when a toggle layer key (either
TG
,TO
, ortap_count>TAPPING_TERM
TT
) is pressed for the target layer it is assumed that the user intended to force the target layer to remain active regardless if the target layer is currently active (the target layer is turned back on if the toggle key turned it off).- This behaviour tries to match the original code from @drashna and was carried forward.
- Technically this is not the standard behaviour for this key but since it is interacting with a temporary layer (which is already non standard) non-standard behaviour might be expected
- Alternative behaviour could be that any toggle key press simply behaves as normal (toggling off the target layer if it happens to be on) and then holds that condition until the toggle is triggered again (effectively disabling auto mouse temporarily)
-
[tentatively solved] ~~There has been some inconsistency when using
LAYER_TAP
keys for the target layer which has very rarely resulted in the target layer being stuck on~~- ~~I have not been able to reproduce this with the latest version of the code but I am concerned that it may have to do with quirks of the tapping code preventing keyup or keydown of the held layer key from reaching the
process_auto_mouse
code so open to suggestions on how to fix this if it seems to still be happening.~~ -
UPDATE: I have confirmed that this only seems to happen with
mouse_key_tracker
as a unsigned integer and without the associated <0 error checking/correcting, no idea if this completely fixes the issue or just makes the bug extremely rare but I have been unable to reproduce since changing back
- ~~I have not been able to reproduce this with the latest version of the code but I am concerned that it may have to do with quirks of the tapping code preventing keyup or keydown of the held layer key from reaching the
I am happy to hear anyone's opinion on what should be done about the first issue and I think the second issue may be considered solved until it is proven otherwise.
Also let me know if the doc should be separated from pointing_device.md
Okay reviewed all docs and code and have found a few bugs and formatting issues in docs.
Also found a minor bug:
When using a layer tap key for the target layer followed immediately by a non mouse key that on the target layer directly "above" a different layer tap key on the default layer some wonky behavior could result if this is less than the TAPPING_TERM
Pretty extreme edge case I know, but it this is the case with a commonly used key on my keymap
Testing a fix now.
Okay and with that everything is tested and working as intended and I have not had any issues with the any of the layer keys (OSL, LT, TT, and TG all work as expected. Also I have tried my best to minimize the code size while trying to maintain the flexibility.
Please let me know if there is any requested changes in documentation or if the desired behaviour for toggle and tap toggle for the target layer would be different
Okay document is hopefully all good now I should have gotten rid of all the typos, and all the syntax errors in the example code.
Just let me know if there is anything more I should do to help get this over the finish line.