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`
This commit is contained in:
Karthik Nishanth 2021-07-12 21:51:57 +02:00 committed by GitHub
parent 6290c84efd
commit 85a86ec3c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 24 deletions

View File

@ -58,7 +58,9 @@ can only be `Shift`, `Control`, `Alt`, and `Super`.
Modifiers conditions are true if their argument is the current keyboard Modifiers conditions are true if their argument is the current keyboard
modifiers. modifiers.
`Key` conditions are true if the Logitech name of the last diverted key or button down is their `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. 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, `Test` conditions are true if their test evaluates to true on the feature,
report, and data of the current notification. report, and data of the current notification.

View File

@ -154,6 +154,7 @@ if x11:
# See docs/rules.md for documentation # See docs/rules.md for documentation
key_down = None key_down = None
key_up = None
def signed(bytes): def signed(bytes):
@ -406,21 +407,50 @@ class Modifiers(Condition):
class Key(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: if isinstance(key, str) and key in _CONTROL:
self.key = _CONTROL[key] self.key = _CONTROL[key]
else: else:
_log.warn('rule Key argument not name of a Logitech key: %s', key) _log.warn('rule Key key name not name of a Logitech key: %s' % key)
self.key = 0 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): 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): 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): def data(self):
return {'Key': str(self.key)} return {'Key': [str(self.key), self.action]}
def bit_test(start, end, bits): def bit_test(start, end, bits):
@ -728,28 +758,36 @@ if x11:
]) ])
keys_down = [] keys_down = []
g_keys_down = 0x00 g_keys_down = 0x00000000
# process a notification # process a notification
def process_notification(device, status, notification, feature): def process_notification(device, status, notification, feature):
if not x11: if not x11:
return return
global keys_down, g_keys_down, key_down global keys_down, g_keys_down, key_down, key_up
key_down = None key_down, key_up = None, None
# need to keep track of keys that are down to find a new key down # 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: if feature == _F.REPROG_CONTROLS_V4 and notification.address == 0x00:
new_keys_down = _unpack('!4H', notification.data[:8]) new_keys_down = _unpack('!4H', notification.data[:8])
for key in new_keys_down: for key in new_keys_down:
if key and key not in keys_down: if key and key not in keys_down:
key_down = key 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 keys_down = new_keys_down
# and also G keys down # and also G keys down
elif feature == _F.GKEY and notification.address == 0x00: elif feature == _F.GKEY and notification.address == 0x00:
new_g_keys_down, = _unpack('!B', notification.data[:1]) new_g_keys_down = _unpack('!4B', notification.data[:4])
for i in range(1, 9): # process 32 bits, byte by byte
if new_g_keys_down & (0x01 << (i - 1)) and not g_keys_down & (0x01 << (i - 1)): for byte_idx in range(4):
key_down = _CONTROL['G' + str(i)] 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 g_keys_down = new_g_keys_down
rules.evaluate(feature, notification, device, status, True) rules.evaluate(feature, notification, device, status, True)

View File

@ -272,7 +272,7 @@ CONTROL = _NamedInts(
LED_Toggle=0x013B, # 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[0x1000 + i] = 'G' + str(i)
CONTROL._fallback = lambda x: 'unknown:%04X' % x CONTROL._fallback = lambda x: 'unknown:%04X' % x

View File

@ -27,6 +27,7 @@ from shlex import quote as shlex_quote
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.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 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.special_keys import CONTROL as _CONTROL from logitech_receiver.special_keys import CONTROL as _CONTROL
@ -1000,25 +1001,36 @@ class KeyUI(ConditionUI):
def create_widgets(self): def create_widgets(self):
self.widgets = {} 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.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True
) )
self.field.set_size_request(600, 0) self.key_field.set_size_request(600, 0)
self.field.connect('changed', self._on_update) self.key_field.connect('changed', self._on_update)
self.widgets[self.field] = (0, 0, 1, 1) 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): def show(self, component):
super().show(component) super().show(component)
with self.ignore_changes(): 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): 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): def _on_update(self, *args):
super()._on_update(*args) super()._on_update(*args)
icon = 'dialog-warning' if not self.component.key else '' icon = 'dialog-warning' if not self.component.key or not self.component.action else ''
self.field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) self.key_field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
@classmethod @classmethod
def left_label(cls, component): def left_label(cls, component):
@ -1026,7 +1038,7 @@ class KeyUI(ConditionUI):
@classmethod @classmethod
def right_label(cls, component): 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): class TestUI(ConditionUI):