diff --git a/docs/rules.md b/docs/rules.md index 5d4ba3820..410c7043e 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -119,6 +119,8 @@ or the window's Window manager class or instance name starts with their string a `Device` and `Active` conditions take one argument, which is the Serial number or Unit ID of a device, as shown in Solaar's detail pane. +`Host' conditions are true if the computers hostname starts with the condition's argument. + `Setting` conditions check the value of a Solaar setting on a device. `Setting` conditions take three or four arguments, depending on the setting: the Serial number or Unit ID of a device, as shown in Solaar's detail pane, @@ -214,6 +216,8 @@ to go wrong under Wayland than under X11. A `MouseScroll` action takes a sequence of two numbers and simulates a horizontal and vertical mouse scroll of these amounts. If the previous condition in the parent rule returns a number the scroll amounts are multiplied by this number. +A `MouseClick` action takes a mouse button name (`left`, `middle` or `right`) and a positive number or 'click', 'depress', or 'release'. +The action simulates that number of clicks of the specified button or just one click, depress, or release of the button. A `MouseClick` action takes a mouse button name (`left`, `middle` or `right`) and a positive number, and simulates that number of clicks of the specified button. An `Execute` action takes a program and arguments and executes it asynchronously. diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 72ce7de73..cb6f27b25 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -88,6 +88,8 @@ _BUTTON_RELEASE = 2 _BUTTON_PRESS = 3 +CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release' + gdisplay = Gdk.Display.get_default() # can be None if Solaar is run without a full window system gkeymap = Gdk.Keymap.get_for_display(gdisplay) if gdisplay else None if _log.isEnabledFor(_INFO): @@ -312,20 +314,36 @@ def simulate_key(code, event): # X11 keycode but Solaar event code def click_xtest(button, count): - for _ in range(count): - if not simulate_xtest(button[0], _BUTTON_PRESS): - return False - if not simulate_xtest(button[0], _BUTTON_RELEASE): - return False + if isinstance(count, int): + for _ in range(count): + if not simulate_xtest(button[0], _BUTTON_PRESS): + return False + if not simulate_xtest(button[0], _BUTTON_RELEASE): + return False + else: + if count != RELEASE: + if not simulate_xtest(button[0], _BUTTON_PRESS): + return False + if count != DEPRESS: + if not simulate_xtest(button[0], _BUTTON_RELEASE): + return False return True def click_uinput(button, count): - for _ in range(count): - if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1): - return False - if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0): - return False + if isinstance(count, int): + for _ in range(count): + if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1): + return False + if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0): + return False + else: + if count != RELEASE: + if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1): + return False + if count != DEPRESS: + if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0): + return False return True @@ -1073,7 +1091,6 @@ def evaluate(self, feature, notification, device, status, last_result): class KeyPress(Action): - CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release' def __init__(self, args, warn=True): self.key_names, self.action = self.regularize_args(args) @@ -1089,11 +1106,11 @@ def __init__(self, args, warn=True): self.key_symbols = [] def regularize_args(self, args): - action = self.CLICK + action = CLICK if not isinstance(args, list): args = [args] keys = args - if len(args) == 2 and args[1] in [self.CLICK, self.DEPRESS, self.RELEASE]: + if len(args) == 2 and args[1] in [CLICK, DEPRESS, RELEASE]: keys = [args[0]] if isinstance(args[0], str) else args[0] action = args[1] return keys, action @@ -1139,14 +1156,14 @@ def keyDown(self, keysyms, modifiers): (keycode, level) = self.keysym_to_keycode(k, modifiers) if keycode is None: _log.warn('rule KeyPress key symbol not currently available %s', self) - elif self.action != self.CLICK or self.needed(keycode, modifiers): # only check needed when clicking + elif self.action != CLICK or self.needed(keycode, modifiers): # only check needed when clicking self.mods(level, modifiers, _KEY_PRESS) simulate_key(keycode, _KEY_PRESS) def keyUp(self, keysyms, modifiers): for k in keysyms: (keycode, level) = self.keysym_to_keycode(k, modifiers) - if keycode and (self.action != self.CLICK or self.needed(keycode, modifiers)): # only check needed when clicking + if keycode and (self.action != CLICK or self.needed(keycode, modifiers)): # only check needed when clicking simulate_key(keycode, _KEY_RELEASE) self.mods(level, modifiers, _KEY_RELEASE) @@ -1155,9 +1172,9 @@ def evaluate(self, feature, notification, device, status, last_result): current = gkeymap.get_modifier_state() if _log.isEnabledFor(_INFO): _log.info('KeyPress action: %s %s, group %s, modifiers %s', self.key_names, self.action, kbdgroup(), current) - if self.action != self.RELEASE: + if self.action != RELEASE: self.keyDown(self.key_symbols, current) - if self.action != self.DEPRESS: + if self.action != DEPRESS: self.keyUp(reversed(self.key_symbols), current) _time.sleep(0.01) else: @@ -1225,9 +1242,11 @@ def __init__(self, args, warn=True): try: self.count = int(count) except (ValueError, TypeError): - if warn: - _log.warn('rule MouseClick action: count %s should be an integer', count) - self.count = 1 + if count in [CLICK, DEPRESS, RELEASE]: + self.count = count + elif warn: + _log.warn('rule MouseClick action: argument %s should be an integer or CLICK, PRESS, or RELEASE', count) + self.count = 1 def __str__(self): return 'MouseClick: %s (%d)' % (self.button, self.count) diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py index 770bb1f94..99450999f 100644 --- a/lib/solaar/ui/diversion_rules.py +++ b/lib/solaar/ui/diversion_rules.py @@ -29,9 +29,9 @@ from gi.repository import Gdk, GObject, Gtk from logitech_receiver import diversion as _DIV from logitech_receiver.common import NamedInt, NamedInts, UnsortedNamedInts +from logitech_receiver.diversion import CLICK, DEPRESS, RELEASE 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 @@ -1770,13 +1770,13 @@ def create_widgets(self): self.add_btn.connect('clicked', self._clicked_add) self.widgets[self.add_btn] = (1, 1, 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.action_clicked_radio.connect('toggled', self._on_update, CLICK) self.widgets[self.action_clicked_radio] = (0, 3, 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.action_pressed_radio.connect('toggled', self._on_update, DEPRESS) self.widgets[self.action_pressed_radio] = (1, 3, 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.action_released_radio.connect('toggled', self._on_update, RELEASE) self.widgets[self.action_released_radio] = (2, 3, 1, 1) def _create_field(self): @@ -1836,8 +1836,8 @@ def show(self, component, editable=True): self.del_btns[i].hide() def collect_value(self): - action = _KeyPress.CLICK if self.action_clicked_radio.get_active() else \ - _KeyPress.DEPRESS if self.action_pressed_radio.get_active() else _KeyPress.RELEASE + action = CLICK if self.action_clicked_radio.get_active() else \ + DEPRESS if self.action_pressed_radio.get_active() else RELEASE return [[f.get_text().strip() for f in self.fields if f.get_visible()], action] @classmethod @@ -1846,8 +1846,7 @@ def left_label(cls, component): @classmethod def right_label(cls, component): - return ' + '.join(component.key_names - ) + (' (' + component.action + ')' if component.action != _KeyPress.CLICK else '') + return ' + '.join(component.key_names) + (' (' + component.action + ')' if component.action != CLICK else '') class MouseScrollUI(ActionUI): @@ -1911,6 +1910,7 @@ class MouseClickUI(ActionUI): MIN_VALUE = 1 MAX_VALUE = 9 BUTTONS = list(_buttons.keys()) + ACTIONS = [CLICK, DEPRESS, RELEASE] def create_widgets(self): self.widgets = {} @@ -1919,29 +1919,39 @@ def create_widgets(self): ) self.widgets[self.label] = (0, 0, 4, 1) self.label_b = Gtk.Label(label=_('Button'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True) - self.label_c = Gtk.Label(label=_('Count'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True) + self.label_c = Gtk.Label(label=_('Count and Action'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True) self.field_b = CompletionEntry(self.BUTTONS) self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1) + self.field_d = CompletionEntry(self.ACTIONS) for f in [self.field_b, self.field_c]: f.set_halign(Gtk.Align.CENTER) f.set_valign(Gtk.Align.START) self.field_b.connect('changed', self._on_update) self.field_c.connect('changed', self._on_update) + self.field_d.connect('changed', self._on_update) self.widgets[self.label_b] = (0, 1, 1, 1) self.widgets[self.field_b] = (1, 1, 1, 1) self.widgets[self.label_c] = (2, 1, 1, 1) self.widgets[self.field_c] = (3, 1, 1, 1) + self.widgets[self.field_d] = (4, 1, 1, 1) def show(self, component, editable): super().show(component, editable) with self.ignore_changes(): self.field_b.set_text(component.button) - self.field_c.set_value(component.count) + if isinstance(component.count, int): + self.field_c.set_value(component.count) + self.field_d.set_text(CLICK) + else: + self.field_c.set_value(1) + self.field_d.set_text(component.count) def collect_value(self): - b, c = self.field_b.get_text(), int(self.field_c.get_value()) + b, c, d = self.field_b.get_text(), int(self.field_c.get_value()), self.field_d.get_text() if b not in self.BUTTONS: b = 'unknown' + if d != CLICK: + c = d return [b, c] @classmethod @@ -1950,7 +1960,7 @@ def left_label(cls, component): @classmethod def right_label(cls, component): - return f'{component.button} (x{component.count})' + return f'{component.button} ({"x" if isinstance(component.count, int) else ""}{component.count})' class ExecuteUI(ActionUI):