Repository: pwr-Solaar/Solaar

Issue:
a rule condition like Process that matches the program the cursor is over, not the program that is focused
hello!

there's quite a few gestures on linux that you can send to windows that are under the cursor but not focused, like scrolling over the volume widget. process based rules can that change scrolling behavior interfere with them

a match that works with the process name under the cursor at the time avoids this

thanks,
  josh




Discussion:
I'm a bit confused as to what the problem is.  

Solaar should not be interfering with things like scrolling over the volume widget.  Of course, if you divert scrolling and the Solaar rule doesn't emit a scroll then you won't get this behaviour, but something like using Solaar rules to change scrolling speed should still trigger any special processing of scrolling.  If this is not the case then the behaviour either needs to be documented in Solaar or fixed.  Please let me know if this is the case.

If all you want to do is to let Solaar rules have access to the program under the cursor then this might be possible, depending on whether X11 provides access to this information.


that was only an example. the information required is the same as used in the process rules, it's just based on focus and not what's currently under the cursor

I scroll a lot of things that aren't in focus and the rules I added for chrome change the behavior of scrolling when it is focused but not the target of scroll events
My take on this request is to add a new rule condition for the process of the window that the cursor is over.  

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
@@ -52,58 +52,14 @@
     XK_KEYS = vars(_XK)
     disp_prog = Display()
     x11 = True
+    NET_ACTIVE_WINDOW = disp_prog.intern_atom('_NET_ACTIVE_WINDOW')
+    NET_WM_PID = disp_prog.intern_atom('_NET_WM_PID')
+    WM_CLASS = disp_prog.intern_atom('WM_CLASS')
 except Exception:
     _log.warn('X11 not available - rules will not be activated', exc_info=_sys.exc_info())
     XK_KEYS = {}
     x11 = False
 
-if x11:
-    # determine name of active process
-    NET_ACTIVE_WINDOW = disp_prog.intern_atom('_NET_ACTIVE_WINDOW')
-    NET_WM_PID = disp_prog.intern_atom('_NET_WM_PID')
-    WM_CLASS = disp_prog.intern_atom('WM_CLASS')
-    root2 = disp_prog.screen().root
-    root2.change_attributes(event_mask=Xlib.X.PropertyChangeMask)
-
-active_process_name = None
-active_wm_class_name = None
-
-
-def active_program_name():
-    try:
-        window_id = root2.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0]
-        window = disp_prog.create_resource_object('window', window_id)
-        window_pid = window.get_full_property(NET_WM_PID, 0).value[0]
-        return psutil.Process(window_pid).name()
-    except (Xlib.error.XError, AttributeError):  # simplify dealing with BadWindow
-        return None
-
-
-def active_program_wm_class():
-    try:
-        window_id = root2.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0]
-        window = disp_prog.create_resource_object('window', window_id)
-        window_wm_class = window.get_wm_class()[0]
-        return window_wm_class
-    except (Xlib.error.XError, AttributeError):  # simplify dealing with BadWindow
-        return None
-
-
-def determine_active_program_and_wm_class():
-    global active_process_name
-    global active_wm_class_name
-    active_process_name = active_program_name()
-    active_wm_class_name = active_program_wm_class()
-    while True:
-        event = disp_prog.next_event()
-        if event.type == Xlib.X.PropertyNotify and event.atom == NET_ACTIVE_WINDOW:
-            active_process_name = active_program_name()
-            active_wm_class_name = active_program_wm_class()
-
-
-if x11:
-    _thread.start_new_thread(determine_active_program_and_wm_class, ())
-
 # determine current key modifiers
 # there must be a better way to do this
 
@@ -327,6 +283,32 @@ def data(self):
         return {'And': [c.data() for c in self.components]}
 
 
+def x11_focus_prog():
+    pid = wm_class = None
+    window = disp_prog.get_input_focus().focus
+    while window:
+        pid = window.get_full_property(NET_WM_PID, 0)
+        wm_class = window.get_wm_class()
+        if wm_class:
+            break
+        window = window.query_tree().parent
+    name = psutil.Process(pid.value[0]).name() if pid else None
+    return (wm_class[0], wm_class[1], name) if wm_class else (name)
+
+
+def x11_pointer_prog():
+    pid = wm_class = None
+    window = disp_prog.screen().root.query_pointer().child
+    for window in reversed(window.query_tree().children):
+        pid = window.get_full_property(NET_WM_PID, 0)
+        wm_class = window.get_wm_class()
+        if wm_class:
+            break
+        window = window.query_tree().parent
+    name = psutil.Process(pid.value[0]).name() if pid else None
+    return (wm_class[0], wm_class[1], name) if wm_class else (name)
+
+
 class Process(Condition):
     def __init__(self, process):
         self.process = process
@@ -340,13 +322,33 @@ def __str__(self):
     def evaluate(self, feature, notification, device, status, last_result):
         if not isinstance(self.process, str):
             return False
-        return bool(active_process_name and active_process_name.startswith(self.process)) or \
-            bool(active_wm_class_name and active_wm_class_name.startswith(self.process))
+        result = any(bool(s and s.startswith(self.process)) for s in x11_focus_prog())
+        return result
 
     def data(self):
         return {'Process': str(self.process)}
 
 
+class MouseProcess(Condition):
+    def __init__(self, process):
+        self.process = process
+        if not isinstance(process, str):
+            _log.warn('rule MouseProcess argument not a string: %s', process)
+            self.process = str(process)
+
+    def __str__(self):
+        return 'MouseProcess: ' + str(self.process)
+
+    def evaluate(self, feature, notification, device, status, last_result):
+        if not isinstance(self.process, str):
+            return False
+        result = any(bool(s and s.startswith(self.process)) for s in x11_pointer_prog())
+        return result
+
+    def data(self):
+        return {'MouseProcess': str(self.process)}
+
+
 class Feature(Condition):
     def __init__(self, feature):
         if not (isinstance(feature, str) and feature in _F):
@@ -715,6 +717,7 @@ def data(self):
     'Or': Or,
     'And': And,
     'Process': Process,
+    'MouseProcess': MouseProcess,
     'Feature': Feature,
     'Report': Report,
     'Modifiers': Modifiers,
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
@@ -508,6 +508,7 @@ def _menu_insert(self, m, it, below=False):
                     [
                         (_('Feature'), _DIV.Feature, FeatureUI.FEATURES_WITH_DIVERSION[0]),
                         (_('Process'), _DIV.Process, ''),
+                        (_('MouseProcess'), _DIV.MouseProcess, ''),
                         (_('Report'), _DIV.Report, 0),
                         (_('Modifiers'), _DIV.Modifiers, []),
                         (_('Key'), _DIV.Key, ''),
@@ -877,6 +878,34 @@ def right_label(cls, component):
         return str(component.process)
 
 
+class MouseProcessUI(ConditionUI):
+
+    CLASS = _DIV.MouseProcess
+
+    def create_widgets(self):
+        self.widgets = {}
+        self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True)
+        self.field.set_size_request(600, 0)
+        self.field.connect('changed', self._on_update)
+        self.widgets[self.field] = (0, 0, 1, 1)
+
+    def show(self, component):
+        super().show(component)
+        with self.ignore_changes():
+            self.field.set_text(component.process)
+
+    def collect_value(self):
+        return self.field.get_text()
+
+    @classmethod
+    def left_label(cls, component):
+        return _('MouseProcess')
+
+    @classmethod
+    def right_label(cls, component):
+        return str(component.process)
+
+
 class FeatureUI(ConditionUI):
 
     CLASS = _DIV.Feature
@@ -1433,6 +1462,7 @@ def right_label(cls, component):
     _DIV.Or: OrUI,
     _DIV.And: AndUI,
     _DIV.Process: ProcessUI,
+    _DIV.MouseProcess: MouseProcessUI,
     _DIV.Feature: FeatureUI,
     _DIV.Report: ReportUI,
     _DIV.Modifiers: ModifiersUI,


Test patch: