Repository: pwr-Solaar/Solaar

Issue:
Possible to have custom scroll wheel actions when gesture button is pressed
I am trying to setup a workflow where when I have the gesture button pressed, scrolling the thumb wheel up or down a notch changes the browser tabs (so basically send Ctrl + Page up / doen when scrolling up or down a notch respectively),
I wasnt able to find a condition to detect if the gesture button is pressed,
 is it that the above workflow is possible with the current capabilities and I am missing something or would something need to be implemented to support such a workflow or is this just impossible ?


Discussion:
It's not really possible for general keys.  
For modifier keys this is already possible using the Modifiers condition.
It would be possible to add a new condition that is true if a diverted key or button is down.  Would that work?
That should be okay but would that work if the button is set as Mouse Gestures and not Diverted ?
If yes then that would work.
Mouse Gestures is a kind of diversion so that should work.

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
@@ -759,6 +759,37 @@ def data(self):
         return {'Key': [str(self.key), self.action]}
 
 
+class KeyIsDown(Condition):
+
+    def __init__(self, args, warn=True):
+        default_key = 0
+
+        key = None
+
+        if not args or not isinstance(args, str):
+            if warn:
+                _log.warn('rule KeyDown arguments unknown: %s' % args)
+            key = default_key
+        elif isinstance(args, str):
+            key = args
+
+        if isinstance(key, str) and key in _CONTROL:
+            self.key = _CONTROL[key]
+        else:
+            if warn:
+                _log.warn('rule Key key name not name of a Logitech key: %s' % key)
+            self.key = default_key
+
+    def __str__(self):
+        return 'KeyIsDown: %s' % (str(self.key) if self.key else 'None')
+
+    def evaluate(self, feature, notification, device, status, last_result):
+        return key_is_down(self.key)
+
+    def data(self):
+        return {'KeyIsDown': str(self.key)}
+
+
 def bit_test(start, end, bits):
     return lambda f, r, d: int.from_bytes(d[start:end], byteorder='big', signed=True) & bits
 
@@ -1195,6 +1226,7 @@ def data(self):
     'Setting': Setting,
     'Modifiers': Modifiers,
     'Key': Key,
+    'KeyIsDown': KeyIsDown,
     'Test': Test,
     'TestBytes': TestBytes,
     'MouseGesture': MouseGesture,
@@ -1233,12 +1265,23 @@ def data(self):
     ])
 
 keys_down = []
-g_keys_down = [0, 0, 0, 0]
+g_keys_down = 0
 m_keys_down = 0
 mr_key_down = False
 thumb_wheel_displacement = 0
 
 
+def key_is_down(key):
+    if key == _CONTROL.MR:
+        return mr_key_down
+    elif _CONTROL.M1 <= key <= _CONTROL.M8:
+        return bool(m_keys_down & (0x01 << (key - _CONTROL.M1)))
+    elif _CONTROL.G1 <= key <= _CONTROL.G32:
+        return bool(g_keys_down & (0x01 << (key - _CONTROL.G1)))
+    else:
+        return key in keys_down
+
+
 # process a notification
 def process_notification(device, status, notification, feature):
     global keys_down, g_keys_down, m_keys_down, mr_key_down, key_down, key_up, thumb_wheel_displacement
@@ -1255,15 +1298,12 @@ def process_notification(device, status, notification, feature):
         keys_down = new_keys_down
     # and also G keys down
     elif feature == _F.GKEY and notification.address == 0x00:
-        new_g_keys_down = _unpack('!4B', notification.data[:4])
-        # process 32 bits, byte by byte
-        for byte_idx in range(4):
-            new_byte, old_byte = new_g_keys_down[byte_idx], g_keys_down[byte_idx]
-            for i in range(1, 9):
-                if new_byte & (0x01 << (i - 1)) and not old_byte & (0x01 << (i - 1)):
-                    key_down = _CONTROL['G' + str(i + 8 * byte_idx)]
-                if old_byte & (0x01 << (i - 1)) and not new_byte & (0x01 << (i - 1)):
-                    key_up = _CONTROL['G' + str(i + 8 * byte_idx)]
+        new_g_keys_down = _unpack('!4I', notification.data[:4])[0]
+        for i in range(32):
+            if new_g_keys_down & (0x01 << (i - 1)) and not g_keys_down & (0x01 << (i - 1)):
+                key_down = _CONTROL['G' + str(i + 1)]
+            if g_keys_down & (0x01 << (i - 1)) and not new_g_keys_down & (0x01 << (i - 1)):
+                key_up = _CONTROL['G' + str(i + 1)]
         g_keys_down = new_g_keys_down
     # and also M keys down
     elif feature == _F.MKEYS and notification.address == 0x00:
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
@@ -518,6 +518,7 @@ def _menu_insert(self, m, it, below=False):
                         (_('Mouse process'), _DIV.MouseProcess, ''),
                         (_('Modifiers'), _DIV.Modifiers, []),
                         (_('Key'), _DIV.Key, ''),
+                        (_('KeyIsDown'), _DIV.KeyIsDown, ''),
                         (_('Active'), _DIV.Active, ''),
                         (_('Setting'), _DIV.Setting, [None, '', None]),
                         (_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
@@ -1378,7 +1379,7 @@ def create_widgets(self):
         self.label.set_text(
             _(
                 'Diverted key or button depressed or released.\n'
-                'Use the Key/Button Diversion setting to divert keys and buttons.'
+                'Use the Key/Button Diversion and Divert G Keys settings to divert keys and buttons.'
             )
         )
         self.widgets[self.label] = (0, 0, 5, 1)
@@ -1420,6 +1421,48 @@ def right_label(cls, component):
         return '%s (%04X) (%s)' % (str(component.key), int(component.key), _(component.action)) if component.key else 'None'
 
 
+class KeyIsDownUI(ConditionUI):
+
+    CLASS = _DIV.KeyIsDown
+    KEY_NAMES = map(str, _CONTROL)
+
+    def create_widgets(self):
+        self.widgets = {}
+        self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
+        self.label.set_text(
+            _(
+                'Diverted key or button is currently down.\n'
+                'Use the Key/Button Diversion and Divert G Keys settings to divert keys and buttons.'
+            )
+        )
+        self.widgets[self.label] = (0, 0, 5, 1)
+        self.key_field = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
+        self.key_field.set_size_request(600, 0)
+        self.key_field.connect('changed', self._on_update)
+        self.widgets[self.key_field] = (0, 1, 1, 1)
+
+    def show(self, component, editable):
+        super().show(component, editable)
+        with self.ignore_changes():
+            self.key_field.set_text(str(component.key) if self.component.key else '')
+
+    def collect_value(self):
+        return self.key_field.get_text()
+
+    def _on_update(self, *args):
+        super()._on_update(*args)
+        icon = 'dialog-warning' if not self.component.key else ''
+        self.key_field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
+
+    @classmethod
+    def left_label(cls, component):
+        return _('KeyIsDown')
+
+    @classmethod
+    def right_label(cls, component):
+        return '%s (%04X)' % (str(component.key), int(component.key)) if component.key else 'None'
+
+
 class TestUI(ConditionUI):
 
     CLASS = _DIV.Test
@@ -2573,6 +2616,7 @@ def _on_update(self, *_args):
     _DIV.Report: ReportUI,
     _DIV.Modifiers: ModifiersUI,
     _DIV.Key: KeyUI,
+    _DIV.KeyIsDown: KeyIsDownUI,
     _DIV.Test: TestUI,
     _DIV.TestBytes: TestBytesUI,
     _DIV.Setting: SettingUI,


Test patch: