rules: add single depress and release options for rule mouse click action

This commit is contained in:
Peter F. Patel-Schneider 2023-08-25 18:59:32 -04:00
parent fc38862e8b
commit 90a0408bd6
3 changed files with 65 additions and 32 deletions

View File

@ -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, `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. 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 check the value of a Solaar setting on a device.
`Setting` conditions take three or four arguments, depending on the setting: `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, 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. 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. 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. 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. An `Execute` action takes a program and arguments and executes it asynchronously.

View File

@ -88,6 +88,8 @@ _KEY_PRESS = 1
_BUTTON_RELEASE = 2 _BUTTON_RELEASE = 2
_BUTTON_PRESS = 3 _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 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 gkeymap = Gdk.Keymap.get_for_display(gdisplay) if gdisplay else None
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
@ -312,20 +314,36 @@ def simulate_key(code, event): # X11 keycode but Solaar event code
def click_xtest(button, count): def click_xtest(button, count):
for _ in range(count): if isinstance(count, int):
if not simulate_xtest(button[0], _BUTTON_PRESS): for _ in range(count):
return False if not simulate_xtest(button[0], _BUTTON_PRESS):
if not simulate_xtest(button[0], _BUTTON_RELEASE): return False
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 return True
def click_uinput(button, count): def click_uinput(button, count):
for _ in range(count): if isinstance(count, int):
if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1): for _ in range(count):
return False if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1):
if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0): return False
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 return True
@ -1073,7 +1091,6 @@ class Action(RuleComponent):
class KeyPress(Action): class KeyPress(Action):
CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release'
def __init__(self, args, warn=True): def __init__(self, args, warn=True):
self.key_names, self.action = self.regularize_args(args) self.key_names, self.action = self.regularize_args(args)
@ -1089,11 +1106,11 @@ class KeyPress(Action):
self.key_symbols = [] self.key_symbols = []
def regularize_args(self, args): def regularize_args(self, args):
action = self.CLICK action = CLICK
if not isinstance(args, list): if not isinstance(args, list):
args = [args] args = [args]
keys = 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] keys = [args[0]] if isinstance(args[0], str) else args[0]
action = args[1] action = args[1]
return keys, action return keys, action
@ -1139,14 +1156,14 @@ class KeyPress(Action):
(keycode, level) = self.keysym_to_keycode(k, modifiers) (keycode, level) = self.keysym_to_keycode(k, modifiers)
if keycode is None: if keycode is None:
_log.warn('rule KeyPress key symbol not currently available %s', self) _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) self.mods(level, modifiers, _KEY_PRESS)
simulate_key(keycode, _KEY_PRESS) simulate_key(keycode, _KEY_PRESS)
def keyUp(self, keysyms, modifiers): def keyUp(self, keysyms, modifiers):
for k in keysyms: for k in keysyms:
(keycode, level) = self.keysym_to_keycode(k, modifiers) (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) simulate_key(keycode, _KEY_RELEASE)
self.mods(level, modifiers, _KEY_RELEASE) self.mods(level, modifiers, _KEY_RELEASE)
@ -1155,9 +1172,9 @@ class KeyPress(Action):
current = gkeymap.get_modifier_state() current = gkeymap.get_modifier_state()
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info('KeyPress action: %s %s, group %s, modifiers %s', self.key_names, self.action, kbdgroup(), current) _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) self.keyDown(self.key_symbols, current)
if self.action != self.DEPRESS: if self.action != DEPRESS:
self.keyUp(reversed(self.key_symbols), current) self.keyUp(reversed(self.key_symbols), current)
_time.sleep(0.01) _time.sleep(0.01)
else: else:
@ -1225,9 +1242,11 @@ class MouseClick(Action):
try: try:
self.count = int(count) self.count = int(count)
except (ValueError, TypeError): except (ValueError, TypeError):
if warn: if count in [CLICK, DEPRESS, RELEASE]:
_log.warn('rule MouseClick action: count %s should be an integer', count) self.count = count
self.count = 1 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): def __str__(self):
return 'MouseClick: %s (%d)' % (self.button, self.count) return 'MouseClick: %s (%d)' % (self.button, self.count)

View File

@ -29,9 +29,9 @@ from typing import Dict
from gi.repository import Gdk, GObject, Gtk from gi.repository import Gdk, GObject, Gtk
from logitech_receiver import diversion as _DIV from logitech_receiver import diversion as _DIV
from logitech_receiver.common import NamedInt, NamedInts, UnsortedNamedInts 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 XK_KEYS as _XK_KEYS
from logitech_receiver.diversion import Key as _Key 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.diversion import buttons as _buttons
from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES
from logitech_receiver.settings import KIND as _SKIND from logitech_receiver.settings import KIND as _SKIND
@ -1770,13 +1770,13 @@ class KeyPressUI(ActionUI):
self.add_btn.connect('clicked', self._clicked_add) self.add_btn.connect('clicked', self._clicked_add)
self.widgets[self.add_btn] = (1, 1, 1, 1) 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 = 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.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 = 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.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 = 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) self.widgets[self.action_released_radio] = (2, 3, 1, 1)
def _create_field(self): def _create_field(self):
@ -1836,8 +1836,8 @@ class KeyPressUI(ActionUI):
self.del_btns[i].hide() self.del_btns[i].hide()
def collect_value(self): def collect_value(self):
action = _KeyPress.CLICK if self.action_clicked_radio.get_active() else \ action = CLICK if self.action_clicked_radio.get_active() else \
_KeyPress.DEPRESS if self.action_pressed_radio.get_active() else _KeyPress.RELEASE 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] return [[f.get_text().strip() for f in self.fields if f.get_visible()], action]
@classmethod @classmethod
@ -1846,8 +1846,7 @@ class KeyPressUI(ActionUI):
@classmethod @classmethod
def right_label(cls, component): def right_label(cls, component):
return ' + '.join(component.key_names return ' + '.join(component.key_names) + (' (' + component.action + ')' if component.action != CLICK else '')
) + (' (' + component.action + ')' if component.action != _KeyPress.CLICK else '')
class MouseScrollUI(ActionUI): class MouseScrollUI(ActionUI):
@ -1911,6 +1910,7 @@ class MouseClickUI(ActionUI):
MIN_VALUE = 1 MIN_VALUE = 1
MAX_VALUE = 9 MAX_VALUE = 9
BUTTONS = list(_buttons.keys()) BUTTONS = list(_buttons.keys())
ACTIONS = [CLICK, DEPRESS, RELEASE]
def create_widgets(self): def create_widgets(self):
self.widgets = {} self.widgets = {}
@ -1919,29 +1919,39 @@ class MouseClickUI(ActionUI):
) )
self.widgets[self.label] = (0, 0, 4, 1) 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_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_b = CompletionEntry(self.BUTTONS)
self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1) 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]: for f in [self.field_b, self.field_c]:
f.set_halign(Gtk.Align.CENTER) f.set_halign(Gtk.Align.CENTER)
f.set_valign(Gtk.Align.START) f.set_valign(Gtk.Align.START)
self.field_b.connect('changed', self._on_update) self.field_b.connect('changed', self._on_update)
self.field_c.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.label_b] = (0, 1, 1, 1)
self.widgets[self.field_b] = (1, 1, 1, 1) self.widgets[self.field_b] = (1, 1, 1, 1)
self.widgets[self.label_c] = (2, 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_c] = (3, 1, 1, 1)
self.widgets[self.field_d] = (4, 1, 1, 1)
def show(self, component, editable): def show(self, component, editable):
super().show(component, editable) super().show(component, editable)
with self.ignore_changes(): with self.ignore_changes():
self.field_b.set_text(component.button) 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): 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: if b not in self.BUTTONS:
b = 'unknown' b = 'unknown'
if d != CLICK:
c = d
return [b, c] return [b, c]
@classmethod @classmethod
@ -1950,7 +1960,7 @@ class MouseClickUI(ActionUI):
@classmethod @classmethod
def right_label(cls, component): 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): class ExecuteUI(ActionUI):