From 85a86ec3c5924b5df37345c0c2c03b7a35141a2f Mon Sep 17 00:00:00 2001 From: Karthik Nishanth Date: Mon, 12 Jul 2021 21:51:57 +0200 Subject: [PATCH] diversion: implement pressed and released action on Key condition (#1189) - Track `key_up` key in addition to `key_down` - Support `pressed` or `released` action in `Key` condition - Add radio button to KeyUI to represent `pressed` or `released` --- docs/rules.md | 4 +- lib/logitech_receiver/diversion.py | 64 +++++++++++++++++++++------ lib/logitech_receiver/special_keys.py | 2 +- lib/solaar/ui/diversion_rules.py | 30 +++++++++---- 4 files changed, 76 insertions(+), 24 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index 6f67f4d8..ba8228a3 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -58,7 +58,9 @@ can only be `Shift`, `Control`, `Alt`, and `Super`. Modifiers conditions are true if their argument is the current keyboard modifiers. `Key` conditions are true if the Logitech name of the last diverted key or button down is their -string argument. Logitech key and button names are shown in the `Key/Button Diversion` +string argument. Alternatively, if the argument is a list `[name, action]` where `action` +is either `'pressed'` or `'released'`, the key down or key up events of `name` argument are +matched, respectively. Logitech key and button names are shown in the `Key/Button Diversion` setting. Some keyboards have Gn keys, which are diverted using the 'Divert G Keys' setting. `Test` conditions are true if their test evaluates to true on the feature, report, and data of the current notification. diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index d7310e08..5097abb7 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -154,6 +154,7 @@ if x11: # See docs/rules.md for documentation key_down = None +key_up = None def signed(bytes): @@ -406,21 +407,50 @@ class Modifiers(Condition): class Key(Condition): - def __init__(self, key): + DOWN = 'pressed' + UP = 'released' + + def __init__(self, args): + default_key = 0 + default_action = self.DOWN + + key, action = None, None + + if not args or not isinstance(args, (list, str)): + _log.warn('rule Key arguments unknown: %s' % args) + key = default_key + action = default_action + elif isinstance(args, str): + _log.debug('rule Key assuming action "%s" for "%s"' % (default_action, args)) + key = args + action = default_action + elif isinstance(args, list): + if len(args) == 1: + _log.debug('rule Key assuming action "%s" for "%s"' % (default_action, args)) + key, action = args[0], default_action + elif len(args) >= 2: + key, action = args[:2] + if isinstance(key, str) and key in _CONTROL: self.key = _CONTROL[key] else: - _log.warn('rule Key argument not name of a Logitech key: %s', key) - self.key = 0 + _log.warn('rule Key key name not name of a Logitech key: %s' % key) + self.key = default_key + + if isinstance(action, str) and action in (self.DOWN, self.UP): + self.action = action + else: + _log.warn('rule Key action unknown: %s, assuming %s' % (action, default_action)) + self.action = default_action def __str__(self): - return 'Key: ' + (str(self.key) if self.key else 'None') + return 'Key: %s (%s)' % ((str(self.key) if self.key else 'None'), self.action) def evaluate(self, feature, notification, device, status, last_result): - return self.key and self.key == key_down + return bool(self.key and self.key == (key_down if self.action == self.DOWN else key_up)) def data(self): - return {'Key': str(self.key)} + return {'Key': [str(self.key), self.action]} def bit_test(start, end, bits): @@ -728,28 +758,36 @@ if x11: ]) keys_down = [] -g_keys_down = 0x00 +g_keys_down = 0x00000000 # process a notification def process_notification(device, status, notification, feature): if not x11: return - global keys_down, g_keys_down, key_down - key_down = None + global keys_down, g_keys_down, key_down, key_up + key_down, key_up = None, None # need to keep track of keys that are down to find a new key down if feature == _F.REPROG_CONTROLS_V4 and notification.address == 0x00: new_keys_down = _unpack('!4H', notification.data[:8]) for key in new_keys_down: if key and key not in keys_down: key_down = key + for key in keys_down: + if key and key not in new_keys_down: + key_up = key keys_down = new_keys_down # and also G keys down elif feature == _F.GKEY and notification.address == 0x00: - new_g_keys_down, = _unpack('!B', notification.data[:1]) - for i in range(1, 9): - if new_g_keys_down & (0x01 << (i - 1)) and not g_keys_down & (0x01 << (i - 1)): - key_down = _CONTROL['G' + str(i)] + 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)] g_keys_down = new_g_keys_down rules.evaluate(feature, notification, device, status, True) diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py index 08ca70a3..20eff4b1 100644 --- a/lib/logitech_receiver/special_keys.py +++ b/lib/logitech_receiver/special_keys.py @@ -272,7 +272,7 @@ CONTROL = _NamedInts( LED_Toggle=0x013B, # ) -for i in range(1, 7): # add in G keys - these are not really Logitech Controls +for i in range(1, 33): # add in G keys - these are not really Logitech Controls CONTROL[0x1000 + i] = 'G' + str(i) CONTROL._fallback = lambda x: 'unknown:%04X' % x diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py index b84bce9d..3eaf0a3d 100644 --- a/lib/solaar/ui/diversion_rules.py +++ b/lib/solaar/ui/diversion_rules.py @@ -27,6 +27,7 @@ from shlex import quote as shlex_quote from gi.repository import Gdk, GObject, Gtk from logitech_receiver import diversion as _DIV from logitech_receiver.diversion import XK_KEYS as _XK_KEYS +from logitech_receiver.diversion import Key as _Key from logitech_receiver.diversion import buttons as _buttons from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES from logitech_receiver.special_keys import CONTROL as _CONTROL @@ -1000,25 +1001,36 @@ class KeyUI(ConditionUI): def create_widgets(self): self.widgets = {} - self.field = CompletionEntry( + self.key_field = CompletionEntry( self.KEY_NAMES, 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) + self.key_field.set_size_request(600, 0) + self.key_field.connect('changed', self._on_update) + self.widgets[self.key_field] = (0, 0, 2, 1) + self.action_pressed_radio = Gtk.RadioButton.new_with_label_from_widget(None, 'Key down') + self.action_pressed_radio.connect('toggled', self._on_update, _Key.DOWN) + self.widgets[self.action_pressed_radio] = (2, 0, 1, 1) + self.action_released_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_pressed_radio, 'Key up') + self.action_released_radio.connect('toggled', self._on_update, _Key.UP) + self.widgets[self.action_released_radio] = (3, 0, 1, 1) def show(self, component): super().show(component) with self.ignore_changes(): - self.field.set_text(str(component.key) if self.component.key else '') + self.key_field.set_text(str(component.key) if self.component.key else '') + if not component.action or component.action == _Key.DOWN: + self.action_pressed_radio.set_active(True) + else: + self.action_released_radio.set_active(True) def collect_value(self): - return self.field.get_text() + action = _Key.UP if self.action_released_radio.get_active() else _Key.DOWN + return [self.key_field.get_text(), action] def _on_update(self, *args): super()._on_update(*args) - icon = 'dialog-warning' if not self.component.key else '' - self.field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) + icon = 'dialog-warning' if not self.component.key or not self.component.action else '' + self.key_field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) @classmethod def left_label(cls, component): @@ -1026,7 +1038,7 @@ class KeyUI(ConditionUI): @classmethod def right_label(cls, component): - return '%s (%04X)' % (str(component.key), int(component.key)) if component.key else 'None' + return '%s (%04X) (%s)' % (str(component.key), int(component.key), component.action) if component.key else 'None' class TestUI(ConditionUI):