From d115ade2eadd60bd6a5e70fbbef182292a2f68bf Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Thu, 10 Mar 2022 17:55:28 -0500 Subject: [PATCH] setting: add setting to divert gestures --- lib/logitech_receiver/hidpp20.py | 60 ++++++++++++++------- lib/logitech_receiver/settings.py | 34 +++++++----- lib/logitech_receiver/settings_templates.py | 41 ++++++++++---- lib/solaar/cli/show.py | 9 ++-- 4 files changed, 97 insertions(+), 47 deletions(-) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 015a5235..fc77e9b1 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -932,7 +932,7 @@ ACTION_ID._fallback = lambda x: 'unknown:%04X' % x class Gesture: - def __init__(self, device, low, high, next_index): + def __init__(self, device, low, high, next_index, next_diversion_index): self._device = device self.id = low self.gesture = GESTURE[low] @@ -942,31 +942,51 @@ class Gesture: self.desired_software_default = high & 0x08 self.persistent = high & 0x10 self.default_enabled = high & 0x20 - self.index = None - if self.can_be_enabled or self.default_enabled: - self.index = next_index - self.offset, self.mask = self._offset_mask() + self.index = next_index if self.can_be_enabled or self.default_enabled else None + self.diversion_index = next_diversion_index if self.can_be_diverted else None + self._enabled = None + self._diverted = None - def _offset_mask(self): # offset and mask - if self.index is not None: - offset = self.index >> 3 # 8 gestures per byte - mask = 0x1 << (self.index % 8) + def _offset_mask(self, index): # offset and mask + if index is not None: + offset = index >> 3 # 8 gestures per byte + mask = 0x1 << (index % 8) return (offset, mask) else: return (None, None) + enable_offset_mask = lambda gesture: gesture._offset_mask(gesture.index) + + diversion_offset_mask = lambda gesture: gesture._offset_mask(gesture.diversion_index) + def enabled(self): # is the gesture enabled? - if self.offset is not None: - result = feature_request(self._device, FEATURE.GESTURE_2, 0x10, self.offset, 0x01, self.mask) - return bool(result[0] & self.mask) if result else None + if self._enabled is None and self.index is not None: + offset, mask = self.enable_offset_mask() + result = feature_request(self._device, FEATURE.GESTURE_2, 0x10, offset, 0x01, mask) + self._enabled = bool(result[0] & mask) if result else None + return self._enabled def set(self, enable): # enable or disable the gesture if not self.can_be_enabled: return None - if self.offset is not None: - reply = feature_request( - self._device, FEATURE.GESTURE_2, 0x20, self.offset, 0x01, self.mask, self.mask if enable else 0x00 - ) + if self.index is not None: + offset, mask = self.enable_offset_mask() + reply = feature_request(self._device, FEATURE.GESTURE_2, 0x20, offset, 0x01, mask, mask if enable else 0x00) + return reply + + def diverted(self): # is the gesture diverted? + if self._diverted is None and self.diversion_index is not None: + offset, mask = self.diversion_offset_mask() + result = feature_request(self._device, FEATURE.GESTURE_2, 0x30, offset, 0x01, mask) + self._diverted = bool(result[0] & mask) if result else None + return self._diverted + + def divert(self, diverted): # divert or undivert the gesture + if not self.can_be_diverted: + return None + if self.diversion_index is not None: + offset, mask = self.diversion_offset_mask() + reply = feature_request(self._device, FEATURE.GESTURE_2, 0x40, offset, 0x01, mask, mask if diverted else 0x00) return reply def as_int(self): @@ -976,7 +996,7 @@ class Gesture: return self.id def __repr__(self): - return f'' + return f'' # allow a gesture to be used as a settings reader/writer to enable and disable the gesture read = enabled @@ -1072,7 +1092,7 @@ class Gestures: self.params = {} self.specs = {} index = 0 - next_gesture_index = 0 + next_gesture_index = next_divsn_index = 0 field_high = 0x00 while field_high != 0x01: # end of fields # retrieve the next eight fields @@ -1085,8 +1105,9 @@ class Gestures: if field_high == 0x1: # end of fields break elif field_high & 0x80: - gesture = Gesture(device, field_low, field_high, next_gesture_index) + gesture = Gesture(device, field_low, field_high, next_gesture_index, next_divsn_index) next_gesture_index = next_gesture_index if gesture.index is None else next_gesture_index + 1 + next_divsn_index = next_divsn_index if gesture.diversion_index is None else next_divsn_index + 1 self.gestures[gesture.gesture] = gesture elif field_high & 0xF0 == 0x30 or field_high & 0xF0 == 0x20: param = Param(device, field_low, field_high) @@ -1100,7 +1121,6 @@ class Gestures: else: _log.warn(f'Unimplemented GESTURE_2 field {field_low} {field_high} found.') index += 1 - device._gestures = self def gesture(self, gesture): return self.gestures.get(gesture, None) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 5ff8d025..8eda1bc0 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -811,28 +811,32 @@ class BitFieldWithOffsetAndMaskValidator(Validator): kind = KIND.multiple_toggle sep = 0x01 - def __init__(self, options, byte_count=None): + def __init__(self, options, om_method=None, byte_count=None): assert (isinstance(options, list)) - # each element of options must have .offset and .mask, - # and its int representation must be its id (not its index) + # each element of options is an instance of a class + # that has an id (which is used as an index in other dictionaries) + # and where om_method is a method that returns a byte offset and byte mask + # that says how to access and modify the bit toogle for the option self.options = options + self.om_method = om_method # to retrieve the options efficiently: self._option_from_key = {} self._mask_from_offset = {} self._option_from_offset_mask = {} for opt in options: + offset, mask = om_method(opt) self._option_from_key[int(opt)] = opt try: - self._mask_from_offset[opt.offset] |= opt.mask + self._mask_from_offset[offset] |= mask except KeyError: - self._mask_from_offset[opt.offset] = opt.mask + self._mask_from_offset[offset] = mask try: - mask_to_opt = self._option_from_offset_mask[opt.offset] + mask_to_opt = self._option_from_offset_mask[offset] except KeyError: mask_to_opt = {} - self._option_from_offset_mask[opt.offset] = mask_to_opt - mask_to_opt[opt.mask] = opt - self.byte_count = (max(x.mask.bit_length() for x in options) + 7) // 8 + self._option_from_offset_mask[offset] = mask_to_opt + mask_to_opt[mask] = opt + self.byte_count = (max(om_method(x)[1].bit_length() for x in options) + 7) // 8 # is this correct?? if byte_count: assert (isinstance(byte_count, int) and byte_count >= self.byte_count) self.byte_count = byte_count @@ -849,8 +853,9 @@ class BitFieldWithOffsetAndMaskValidator(Validator): option = self._option_from_key.get(key, None) if option is None: return None - b = option.offset << (8 * (self.byte_count + 1)) - b |= (self.sep << (8 * self.byte_count)) | option.mask + offset, mask = option.om_method(option) + b = offset << (8 * (self.byte_count + 1)) + b |= (self.sep << (8 * self.byte_count)) | mask return _int2bytes(b, self.byte_count + 2) def validate_read(self, reply_bytes_dict): @@ -872,10 +877,11 @@ class BitFieldWithOffsetAndMaskValidator(Validator): w = {} for k, v in new_value.items(): option = self._option_from_key[int(k)] - if option.offset not in w: - w[option.offset] = 0 + offset, mask = self.om_method(option) + if offset not in w: + w[offset] = 0 if v: - w[option.offset] |= option.mask + w[offset] |= mask return [ _int2bytes((offset << (8 * (2 * self.byte_count + 1))) | (self.sep << (16 * self.byte_count)) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 1f9c86fd..88055acc 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -459,10 +459,11 @@ class ReprogrammableKeys(_Settings): @classmethod def build(cls, setting_class, device): choices = {} - for k in device.keys: - tgts = k.remappable_to - if len(tgts) > 1: - choices[k.key] = tgts + if device.keys: + for k in device.keys: + tgts = k.remappable_to + if len(tgts) > 1: + choices[k.key] = tgts return cls(choices, key_byte_count=2, byte_count=2, extra_default=0) if choices else None @@ -494,9 +495,10 @@ class DivertKeys(_Settings): @classmethod def build(cls, setting_class, device): choices = {} - for k in device.keys: - if 'divertable' in k.flags and 'virtual' not in k.flags: - choices[k.key] = setting_class.choices_universe + if device.keys: + for k in device.keys: + if 'divertable' in k.flags and 'virtual' not in k.flags: + choices[k.key] = setting_class.choices_universe if not choices: return None return cls(choices, key_byte_count=2, byte_count=1, mask=0x01) @@ -982,14 +984,32 @@ class Gesture2Gestures(_BitFieldOMSetting): description = _('Tweak the mouse/touchpad behaviour.') feature = _F.GESTURE_2 rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'om_method': _hidpp20.Gesture.enable_offset_mask} choices_universe = _hidpp20.GESTURE _labels = _GESTURE2_GESTURES_LABELS class validator_class(_BitFieldOMV): @classmethod - def build(cls, setting_class, device): - options = [g for g in _hidpp20.get_gestures(device).gestures.values() if g.can_be_enabled or g.default_enabled] - return cls(options) if options else None + def build(cls, setting_class, device, om_method=None): + options = [g for g in device.gestures.gestures.values() if g.can_be_enabled or g.default_enabled] + return cls(options, om_method=om_method) if options else None + + +class Gesture2Divert(_BitFieldOMSetting): + name = 'gesture2-divert' + label = _('Gestures Diversion') + description = _('Divert mouse/touchpad gestures.') + feature = _F.GESTURE_2 + rw_options = {'read_fnid': 0x30, 'write_fnid': 0x40} + validator_options = {'om_method': _hidpp20.Gesture.diversion_offset_mask} + choices_universe = _hidpp20.GESTURE + _labels = _GESTURE2_GESTURES_LABELS + + class validator_class(_BitFieldOMV): + @classmethod + def build(cls, setting_class, device, om_method=None): + options = [g for g in device.gestures.gestures.values() if g.can_be_diverted] + return cls(options, om_method=om_method) if options else None class Gesture2Params(_LongSettings): @@ -1147,6 +1167,7 @@ SETTINGS = [ DualPlatform, # simple ChangeHost, # working Gesture2Gestures, # working + Gesture2Divert, Gesture2Params, # working ] diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index ac6eaddd..8163dbcc 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -262,11 +262,14 @@ def _print_device(dev, num=None): (len(dev.gestures.gestures), len(dev.gestures.params), len(dev.gestures.specs)) ) for k in dev.gestures.gestures.values(): - print(' %-26s Enabled (%4s): %s' % (k.gesture, k.index, k.enabled())) + print( + ' %-26s Enabled(%4s): %-5s Diverted:(%4s) %s' % + (k.gesture, k.index, k.enabled(), k.diversion_index, k.diverted()) + ) for k in dev.gestures.params.values(): - print(' %-26s Value (%4s): %s [Default: %s]' % (k.param, k.index, k.value, k.default_value)) + print(' %-26s Value (%4s): %s [Default: %s]' % (k.param, k.index, k.value, k.default_value)) for k in dev.gestures.specs.values(): - print(' %-26s Spec (%4s): %s' % (k.spec, k.id, k.value)) + print(' %-26s Spec (%4s): %s' % (k.spec, k.id, k.value)) if dev.online: _battery_line(dev) else: