Skip to content

Commit

Permalink
Merge branch 'master' into host
Browse files Browse the repository at this point in the history
  • Loading branch information
pfps authored Sep 16, 2023
2 parents 83e2f11 + 90a0408 commit 3f3fe85
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 32 deletions.
4 changes: 4 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
59 changes: 39 additions & 20 deletions lib/logitech_receiver/diversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 22 additions & 12 deletions lib/solaar/ui/diversion_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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
Expand All @@ -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):
Expand Down

0 comments on commit 3f3fe85

Please sign in to comment.