Repository: pwr-Solaar/Solaar

Issue:
Mouse side buttons as modifier keys

- Solaar version (`solaar --version` and `git describe --tags`):
solaar 1.1.3
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
- Distribution:
Arch
- Kernel version (ex. `uname -srmo`):
5.18.1
- Output of `solaar show` for the target device (if applicable):


<details>
Solaar version 1.1.3

USB and Bluetooth Devices

  1: MX Master 3 Wireless Mouse
     Device path  : /dev/hidraw1
     USB id       : 046d:B023
     Codename     : MX Master 3
     Kind         : mouse
     Protocol     : HID++ 4.5
     Serial number: 
     Model ID:      B02340820000
     Unit ID:       B001CB47
        Bootloader: BOT 95.01.B0015
          Firmware: MPM 19.01.B0015
             Other: 
     Supports 31 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 95.01.B0015 00006A9CA89D
            Firmware: Firmware MPM 19.01.B0015 B0236A9CA89D
            Firmware: Other   
            Unit ID: B001CB47  Model ID: B02340820000  Transport IDs: {'btleid': 'B023', 'wpid': '4082'}
         3: DEVICE NAME            {0005}   
            Name: Wireless Mouse MX Master 3
            Kind: mouse
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: CRYPTO ID              {0021}   
         7: DEVICE FRIENDLY NAME   {0007}   
            Friendly Name: MX Master 3
         8: BATTERY STATUS         {1000}   
            Battery: 100%, discharging, next level 50%.
         9: REPROG CONTROLS V4     {1B04}   
            DPI Sliding Adjustment (saved): Off
            DPI Sliding Adjustment        : Off
            Mouse Gestures (saved): 195
            Mouse Gestures        : Off
            Key/Button Actions (saved): {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Mouse Gesture Button:Gesture Button Navigation, Smart Shift:Smart Shift}
            Key/Button Actions        : {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Mouse Gesture Button:Gesture Button Navigation, Smart Shift:Smart Shift}
            Key/Button Diversion (saved): {Middle Button:Regular, Back Button:Diverted, Forward Button:Diverted, Mouse Gesture Button:Diverted, Smart Shift:Regular}
            Key/Button Diversion        : {Middle Button:Regular, Back Button:Diverted, Forward Button:Diverted, Mouse Gesture Button:Diverted, Smart Shift:Regular}
        10: CHANGE HOST            {1814}   
            Change Host        : 2:LTTTOP
        11: XY STATS               {2250}   
        12: ADJUSTABLE DPI         {2201}   
            Sensitivity (DPI) (saved): 1000
            Sensitivity (DPI)        : 1000
        13: SMART SHIFT            {2110}   
            Scroll Wheel Rachet (saved): 10
            Scroll Wheel Rachet        : 10
        14: HIRES WHEEL            {2121}   
            Multiplier: 15
            Has invert: Normal wheel motion
            Has ratchet switch: Normal wheel mode
            High resolution mode
            HID notification
            Scroll Wheel Direction (saved): False
            Scroll Wheel Direction        : False
            Scroll Wheel Resolution (saved): True
            Scroll Wheel Resolution        : True
            Scroll Wheel Diversion (saved): False
            Scroll Wheel Diversion        : False
        15: THUMB WHEEL            {2150}   
            Thumb Wheel Diversion (saved): False
            Thumb Wheel Diversion        : False
            Thumb Wheel Direction (saved): False
            Thumb Wheel Direction        : False
        16: WHEEL STATS            {2251}   
        17: DFUCONTROL SIGNED      {00C2}   
        18: DEVICE RESET           {1802}   internal, hidden
        19: unknown:1803           {1803}   internal, hidden
        20: CONFIG DEVICE PROPS    {1806}   internal, hidden
        21: unknown:1813           {1813}   internal, hidden
        22: OOBSTATE               {1805}   internal, hidden
        23: unknown:1830           {1830}   internal, hidden
        24: unknown:18A1           {18A1}   internal, hidden
        25: unknown:1E00           {1E00}   hidden
        26: unknown:1EB0           {1EB0}   internal, hidden
        27: unknown:1861           {1861}   internal, hidden
        28: unknown:9300           {9300}   internal, hidden
        29: unknown:9001           {9001}   internal, hidden
        30: unknown:9205           {9205}   internal, hidden
     Has 8 reprogrammable keys:
         0: Left Button               , default: Left Click                  => Left Click                
             mse, analytics key events, pos:0, group:1, group mask:g1
             reporting: default
         1: Right Button              , default: Right Click                 => Right Click               
             mse, analytics key events, pos:0, group:1, group mask:g1
             reporting: default
         2: Middle Button             , default: Mouse Middle Button         => Mouse Middle Button       
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:3, group mask:g1,g2,g3
             reporting: default
         3: Back Button               , default: Mouse Back Button           => Mouse Back Button         
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
             reporting: diverted
         4: Forward Button            , default: Mouse Forward Button        => Mouse Forward Button      
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
             reporting: diverted
         5: Mouse Gesture Button      , default: Gesture Button Navigation   => Gesture Button Navigation 
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:3, group mask:g1,g2,g3
             reporting: diverted, raw XY diverted
         6: Smart Shift               , default: Smart Shift                 => Smart Shift               
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:3, group mask:g1,g2,g3
             reporting: default
         7: Virtual Gesture Button    , default: Virtual Gesture Button      => Virtual Gesture Button    
             divertable, virtual, raw XY, force raw XY, pos:0, group:4, group mask:empty
             reporting: default
     Battery: 100%, discharging, next level 50%.

</details>

**Describe the solution you'd like**
Ability to use the mouse buttons as modifiers. I like to have a Super, Control and Shift key on my mouse to make selecting lists and adjusting windows easily (Super + click to move windows, super + right click to resize etc)

This is possible in Windows but through Solaar the mouse seems to stop taking button input when another is held down - this is normal for the gesture pad but the back and forward buttons do it to - the cursor will still move but clicking cancels out whatever button is already pressed.

If possible this would be an option to apply to the gestures pad aswell so foward, back and the gesture button can be used as modifiers.


**Describe alternatives you've considered**
Using 2 hands to click things
Not using a logitech mouse



Discussion:
It should be possible to do this in a customization program for your window manager.  However, it appears that window managers in Linux don't have this level of customization.

I don't understand what you mean by "the mouse stops taking button input when another is held down".  There should be nothing in Linux or Solaar that prevents programs from seeing the mouse buttons go up and down independently.  I use Emacs, and Emacs can see the back button going down, then the left button going down, then the left button coming up, and then the back button coming up.

What you want can almost be done in Solaar, but it needs Solaar rules.  There is no facility in an MX Master 3 to convert button motions to arbitrary key motions.   What can be done in Solaar is to divert the back and forward buttons and use button presses and releases to simulate keyboard input.  What can't be done is have button press simulate key press and button release simulate key release - the button press or release simulates both key press and key release.  Independent press and release wasn't added because it is dangerous - if you don't have the release corresponding to the press then a key or button would stay down and potentially make your system unusable.

There have been several requests for this ability, however.  If this ability will suit you please respond and it might be added.

One issue is that the current rule action is KeyPress, which isn't a good name when there can be key presses and key releases.  Probably there would be an extra argument which would be either down, up, or click.  Suggestions for better names are welcome.
Apologies just retested and you can ignore the part about not taking input, just me being dumb.

At the moment I've got the back and forward buttons to press Shift and Control respectively, as well as interrupting the default back and forward action, but holding the button to keep control/shift held is what I'm struggling with now. It sounds like the seperate key up key down control would solve this, maybe it could be an advanced mode setting so it's harder to set on accident
How are you doing this?  With Solaar rules?  If so, please post your current rule set.
Yep just Solaar rules, screenshot attached

![image](https://user-images.githubusercontent.com/13917133/172651069-377611d4-aaad-4a59-aa9c-cba78331906b.png)

They all activate, I'm just not able to use them while held down. The gesture button this makes sense for as the cursor freezes when it's held to activate gestures but the other 2 should be working as I understand it

These rule will press-and-release the modifier keys when the button is pressed, provided that the button is diverted, so they won't solve your issue.

But I don't see any reason that your mouse freezes when the back or forward button is pressed.  That doesn't happen on my MX Master 3, whether the button is diverted or not.  For the mouse gesture button this depends on whether you have mouse gestures on.  If so, the mouse cursor will not move; if not, the mouse cursor should continue to move.
The cursor isn't freezing with the 2 buttons, only the gesture pad, but when I turned off gestures, it stopped following the button rule
See Issue #1517, which is a direct request for this capability.

To do what you want, you will need two rules, one to depress the modifier key, triggered by the button press, and one to release the modifier key, triggered by button release.
Ah okay cool I'll follow it over there then, thanks for the help!

Resolving patch:
diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py
--- a/lib/logitech_receiver/diversion.py
+++ b/lib/logitech_receiver/diversion.py
@@ -820,15 +820,29 @@ def evaluate(self, feature, notification, device, status, last_result):
 
 
 class KeyPress(Action):
-    def __init__(self, keys):
-        if isinstance(keys, str):
-            keys = [keys]
-        self.key_names = keys
-        self.key_symbols = [XK_KEYS.get(k, None) for k in keys]
+    CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release'
+
+    def __init__(self, args):
+        self.key_names, self.action = self.regularize_args(args)
+        if not isinstance(self.key_names, list):
+            _log.warn('rule KeyPress keys not key names %s', self.keys_names)
+            self.key_symbols = []
+        else:
+            self.key_symbols = [XK_KEYS.get(k, None) for k in self.key_names]
         if not all(self.key_symbols):
-            _log.warn('rule KeyPress not sequence of key names %s', keys)
+            _log.warn('rule KeyPress keys not key names %s', self.key_names)
             self.key_symbols = []
 
+    def regularize_args(self, args):
+        action = self.CLICK
+        if not isinstance(args, list):
+            args = [args]
+        keys = args
+        if len(args) == 2 and args[1] in [self.CLICK, self.DEPRESS, self.RELEASE]:
+            keys = [args[0]] if isinstance(args[0], str) else args[0]
+            action = args[1]
+        return keys, action
+
     # WARNING:  This is an attempt to reverse the keycode to keysym mappping in XKB.  It may not be completely general.
     def keysym_to_keycode(self, keysym, modifiers):  # maybe should take shift into account
         group = kbdgroup() or 0
@@ -849,7 +863,7 @@ def keysym_to_keycode(self, keysym, modifiers):  # maybe should take shift into
         return (keycode, level)
 
     def __str__(self):
-        return 'KeyPress: ' + ' '.join(self.key_names)
+        return 'KeyPress: ' + ' '.join(self.key_names) + ' ' + self.action
 
     def needed(self, k, modifiers):
         code = modifier_code(k)
@@ -885,16 +899,18 @@ def evaluate(self, feature, notification, device, status, last_result):
         if gkeymap:
             current = gkeymap.get_modifier_state()
             if _log.isEnabledFor(_INFO):
-                _log.info('KeyPress action: %s, group %s, modifiers %s', self.key_names, kbdgroup(), current)
-            self.keyDown(self.key_symbols, current)
-            self.keyUp(reversed(self.key_symbols), current)
+                _log.info('KeyPress action: %s %s, group %s, modifiers %s', self.key_names, self.action, kbdgroup(), current)
+            if self.action != self.RELEASE:
+                self.keyDown(self.key_symbols, current)
+            if self.action != self.DEPRESS:
+                self.keyUp(reversed(self.key_symbols), current)
             _time.sleep(0.01)
         else:
             _log.warn('no keymap so cannot determine which keycode to send')
         return None
 
     def data(self):
-        return {'KeyPress': [str(k) for k in self.key_names]}
+        return {'KeyPress': [[str(k) for k in self.key_names], self.action]}
 
 
 # KeyDown is dangerous as the key can auto-repeat and make your system unusable
diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py
--- a/lib/solaar/ui/diversion_rules.py
+++ b/lib/solaar/ui/diversion_rules.py
@@ -31,6 +31,7 @@
 from logitech_receiver.common import NamedInt, NamedInts, UnsortedNamedInts
 from logitech_receiver.diversion import XK_KEYS as _XK_KEYS
 from logitech_receiver.diversion import Key as _Key
+from logitech_receiver.diversion import KeyPress as _KeyPress
 from logitech_receiver.diversion import buttons as _buttons
 from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES
 from logitech_receiver.settings import KIND as _SKIND
@@ -1627,6 +1628,15 @@ def create_widgets(self):
         self.add_btn = Gtk.Button(_('Add key'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
         self.add_btn.connect('clicked', self._clicked_add)
         self.widgets[self.add_btn] = (1, 0, 1, 1)
+        self.action_clicked_radio = Gtk.RadioButton.new_with_label_from_widget(None, _('Click'))
+        self.action_clicked_radio.connect('toggled', self._on_update, _KeyPress.CLICK)
+        self.widgets[self.action_clicked_radio] = (0, 2, 1, 1)
+        self.action_pressed_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_clicked_radio, _('Depress'))
+        self.action_pressed_radio.connect('toggled', self._on_update, _KeyPress.DEPRESS)
+        self.widgets[self.action_pressed_radio] = (1, 2, 1, 1)
+        self.action_released_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_pressed_radio, _('Release'))
+        self.action_released_radio.connect('toggled', self._on_update, _KeyPress.RELEASE)
+        self.widgets[self.action_released_radio] = (2, 2, 1, 1)
 
     def _create_field(self):
         field = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
@@ -1643,14 +1653,15 @@ def _create_del_btn(self):
         return btn
 
     def _clicked_add(self, _btn):
-        self.component.__init__(self.collect_value() + [''])
+        keys, action = self.component.regularize_args(self.collect_value())
+        self.component.__init__([keys + [''], action])
         self.show(self.component, editable=True)
         self.fields[len(self.component.key_names) - 1].grab_focus()
 
     def _clicked_del(self, _btn, pos):
-        v = self.collect_value()
-        v.pop(pos)
-        self.component.__init__(v)
+        keys, action = self.component.regularize_args(self.collect_value())
+        keys.pop(pos)
+        self.component.__init__([keys, action])
         self.show(self.component, editable=True)
         self._on_update_callback()
 
@@ -1682,7 +1693,9 @@ def show(self, component, editable=True):
         self.add_btn.set_valign(Gtk.Align.END if n >= 1 else Gtk.Align.CENTER)
 
     def collect_value(self):
-        return [f.get_text().strip() for f in self.fields if f.get_visible()]
+        action = _KeyPress.CLICK if self.action_clicked_radio.get_active() else \
+                 _KeyPress.DEPRESS if self.action_pressed_radio.get_active() else _KeyPress.RELEASE
+        return [[f.get_text().strip() for f in self.fields if f.get_visible()], action]
 
     @classmethod
     def left_label(cls, component):
@@ -1690,7 +1703,8 @@ def left_label(cls, component):
 
     @classmethod
     def right_label(cls, component):
-        return ' + '.join(component.key_names)
+        return ' + '.join(component.key_names
+                          ) + ('  (' + component.action + ')' if component.action != _KeyPress.CLICK else '')
 
 
 class MouseScrollUI(ActionUI):


Test patch: