qmk_firmware icon indicating copy to clipboard operation
qmk_firmware copied to clipboard

[Core] Feature: pointing device automatic mouse layer

Open Alabastard-64 opened this issue 2 years ago • 23 comments

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

Alabastard-64 avatar Aug 09 '22 04:08 Alabastard-64

This works really well for me in that it lessens the time setting up auto mouse layer for new boards.

freznel10 avatar Aug 11 '22 13:08 freznel10

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.

Alabastard-64 avatar Aug 13 '22 02:08 Alabastard-64

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.

Alabastard-64 avatar Aug 15 '22 13:08 Alabastard-64

The latest updates change a few things:

  1. Initial documentation is complete
  2. One shot layer handling should be improved but is still untested. Was based on reread of one shot code in action.c and action_utils.c
  3. QK_MODS now pass through to default behavior leaving for them to be defined as mouse keys or not by user/keyboard.
  4. 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.
  5. 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.

Alabastard-64 avatar Aug 16 '22 14:08 Alabastard-64

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?

dkao avatar Aug 17 '22 08:08 dkao

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.

Alabastard-64 avatar Aug 17 '22 19:08 Alabastard-64

For now I can just expose a function that handles layer activation that can be overwritten if need be.

Alabastard-64 avatar Aug 17 '22 23:08 Alabastard-64

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 avatar Aug 18 '22 01:08 Alabastard-64

@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?

dkao avatar Aug 18 '22 04:08 dkao

@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?

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)

drashna avatar Aug 18 '22 04:08 drashna

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?

Alabastard-64 avatar Aug 19 '22 16:08 Alabastard-64

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.

dkao avatar Aug 19 '22 20:08 dkao

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.

Alabastard-64 avatar Aug 19 '22 22:08 Alabastard-64

Sounds good. Will propose the report_mouse_t in a separate PR, I've got another feature that could also benefit from it.

dkao avatar Aug 20 '22 05:08 dkao

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.

Alabastard-64 avatar Aug 30 '22 07:08 Alabastard-64

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)

Alabastard-64 avatar Sep 01 '22 14:09 Alabastard-64

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.

Alabastard-64 avatar Sep 03 '22 16:09 Alabastard-64

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:

  1. The new names are a little more clear
  2. 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

Alabastard-64 avatar Sep 04 '22 19:09 Alabastard-64

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:

  1. Currently when a toggle layer key (either TG, TO, or tap_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)
  2. [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 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.

Alabastard-64 avatar Sep 04 '22 19:09 Alabastard-64

Also let me know if the doc should be separated from pointing_device.md

Alabastard-64 avatar Sep 04 '22 22:09 Alabastard-64

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.

Alabastard-64 avatar Sep 08 '22 01:09 Alabastard-64

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

Alabastard-64 avatar Sep 09 '22 16:09 Alabastard-64

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.

Alabastard-64 avatar Sep 13 '22 02:09 Alabastard-64