diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index 6ef8b127..0793ab88 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -18,11 +18,10 @@ from collections import namedtuple +from . import settings_templates as _ST from .common import NamedInts as _NamedInts from .hidpp10 import DEVICE_KIND as _DK from .hidpp10 import REGISTERS as _R -from .settings_templates import FeatureSettings as _FS -from .settings_templates import RegisterSettings as _RS # # @@ -66,13 +65,6 @@ def _D( assert codename is not None, 'descriptor for %s does not have codename set' % name if protocol is not None: - # ? 2.0 devices should not have any registers - _kind = lambda s: s._rw.kind if hasattr(s, '_rw') else s._rw_kind - if protocol < 2.0: - assert settings is None or all(_kind(s) == 1 for s in settings) - else: - assert registers is None - assert settings is None or all(_kind(s) == 2 for s in settings) if wpid: for w in wpid if isinstance(wpid, tuple) else (wpid, ): @@ -136,12 +128,6 @@ def get_btid(btid): return found -# -# -# - -_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) - # # # @@ -201,7 +187,7 @@ _D('Wireless Keyboard EX100', codename='EX100', protocol=1.0, wpid='0065', regis _D('Wireless Keyboard MK300', protocol=1.0, wpid='0068', registers=(_R.battery_status, )) _D('Number Pad N545', protocol=1.0, wpid='2006', registers=(_R.battery_status, )) _D('Wireless Compact Keyboard K340', protocol=1.0, wpid='2007', registers=(_R.battery_status, )) -_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', registers=(_R.battery_status, ), settings=[_RS.fn_swap()]) +_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap]) _D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A', registers=(_R.battery_status, )) _D('Wireless Keyboard MK320', protocol=1.0, wpid='200F', registers=(_R.battery_status, )) _D( @@ -213,23 +199,23 @@ _D( _R.three_leds, ), settings=[ - _RS.fn_swap(), - _RS.hand_detection(), + _ST.RegisterFnSwap, + _ST.RegisterHandDetection, ], ) -_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', registers=(_R.battery_status, ), settings=[_RS.fn_swap()]) -_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', settings=[_FS.fn_swap()]) +_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap]) +_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', settings=[_ST.FnSwap]) _D('Wireless Keyboard K270 (unifying)', protocol=2.0, wpid='4003') -_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', settings=[_FS.fn_swap()]) +_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', settings=[_ST.FnSwap]) _D('Wireless Keyboard K230', protocol=2.0, wpid='400D') -_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'), settings=[_FS.fn_swap()]) -_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023', settings=[_FS.fn_swap()]) -_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032', settings=[_FS.new_fn_swap()]) +_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'), settings=[_ST.FnSwap]) +_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023', settings=[_ST.FnSwap]) +_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032', settings=[_ST.NewFnSwap]) _D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D') -_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B', settings=[_FS.new_fn_swap()]) -_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061', settings=[_FS.k375s_fn_swap()]) +_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B', settings=[_ST.NewFnSwap]) +_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061', settings=[_ST.K375sFnSwap]) _D('Craft Advanced Keyboard', codename='Craft', protocol=4.5, wpid='4066', btid=0xB350) -_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E', settings=[_FS.fn_swap()]) +_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E', settings=[_ST.FnSwap]) _D('MX Keys Keyboard', codename='MX Keys', protocol=4.5, wpid='408A', btid=0xB35B) _D('G915 TKL LIGHTSPEED Wireless RGB Mechanical Gaming Keyboard', codename='G915 TKL', protocol=4.2, wpid='408E', usbid=0xC343) _D('G512 RGB Mechanical Gaming Keyboard', codename='G512', usbid=0xc33c, interface=1) @@ -268,7 +254,7 @@ _D( protocol=1.0, wpid=('100B', '100F'), registers=(_R.battery_charge, ), - settings=[_RS.smooth_scroll(), _RS.side_scroll()] + settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll] ) _D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011', registers=(_R.battery_charge, )) _D( @@ -278,8 +264,8 @@ _D( wpid='1013', registers=(_R.battery_charge, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D( @@ -290,8 +276,8 @@ _D( wpid='1014', registers=(_R.battery_charge, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D( @@ -301,10 +287,17 @@ _D( wpid='1017', registers=(_R.battery_charge, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) + + +class _PerformanceMXDpi(_ST.RegisterDpi): + choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) + validator_options = {'choices': choices_universe} + + _D( 'Performance Mouse MX', codename='Performance MX', @@ -315,9 +308,9 @@ _D( _R.three_leds, ), settings=[ - _RS.dpi(choices=_PERFORMANCE_MX_DPIS), - _RS.smooth_scroll(), - _RS.side_scroll(), + _PerformanceMXDpi, + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D( @@ -327,8 +320,8 @@ _D( wpid='101B', registers=(_R.battery_charge, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D('Wireless Mouse M350', protocol=1.0, wpid='101C', registers=(_R.battery_charge, )) @@ -339,11 +332,11 @@ _D( wpid='101D', registers=(_R.battery_charge, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) -_D('Wireless Mouse M305', protocol=1.0, wpid='101F', registers=(_R.battery_status, ), settings=[_RS.side_scroll()]) +_D('Wireless Mouse M305', protocol=1.0, wpid='101F', registers=(_R.battery_status, ), settings=[_ST.RegisterSideScroll]) _D('Wireless Mouse M215', protocol=1.0, wpid='1020') _D( 'G700 Gaming Mouse', @@ -357,12 +350,12 @@ _D( _R.three_leds, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D('Wireless Mouse M310', protocol=1.0, wpid='1024', registers=(_R.battery_status, )) -_D('Wireless Mouse M510', protocol=1.0, wpid='1025', registers=(_R.battery_status, ), settings=[_RS.side_scroll()]) +_D('Wireless Mouse M510', protocol=1.0, wpid='1025', registers=(_R.battery_status, ), settings=[_ST.RegisterSideScroll]) _D('Fujitsu Sonic Mouse', codename='Sonic', protocol=1.0, wpid='1029') _D( 'G700s Gaming Mouse', @@ -376,14 +369,14 @@ _D( _R.three_leds, ), settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), + _ST.RegisterSmoothScroll, + _ST.RegisterSideScroll, ], ) _D('Couch Mouse M515', protocol=2.0, wpid='4007') _D('Wireless Mouse M175', protocol=2.0, wpid='4008') -_D('Wireless Mouse M325', protocol=2.0, wpid='400A', settings=[_FS.hi_res_scroll()]) +_D('Wireless Mouse M325', protocol=2.0, wpid='400A', settings=[_ST.HiResScroll]) _D('Wireless Mouse M525', protocol=2.0, wpid='4013') _D('Wireless Mouse M345', protocol=2.0, wpid='4017') _D('Wireless Mouse M187', protocol=2.0, wpid='4019') @@ -391,16 +384,16 @@ _D('Touch Mouse M600', protocol=2.0, wpid='401A') _D('Wireless Mouse M150', protocol=2.0, wpid='4022') _D('Wireless Mouse M185', protocol=2.0, wpid='4038') _D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041', btid=0xb012) -_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A', settings=[_FS.hires_smooth_invert()]) -_D('Wireless Mouse M510', protocol=2.0, wpid='4051', codename='M510v2', settings=[_FS.lowres_smooth_scroll()]) +_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A', settings=[_ST.HiresSmoothInvert]) +_D('Wireless Mouse M510', protocol=2.0, wpid='4051', codename='M510v2', settings=[_ST.LowresSmoothScroll]) _D( 'Wireless Mouse M185 new', codename='M185n', protocol=4.5, wpid='4054', settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), + _ST.LowresSmoothScroll, + _ST.PointerSpeed, ] ) _D( @@ -409,8 +402,8 @@ _D( protocol=4.5, wpid='4055', settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), + _ST.LowresSmoothScroll, + _ST.PointerSpeed, ] ) _D( @@ -420,8 +413,7 @@ _D( wpid='4069', btid=0xb019, settings=[ - _FS.hires_smooth_invert(), - _FS.gesture2_gestures(), + _ST.HiresSmoothInvert, ], ) _D( @@ -430,8 +422,8 @@ _D( protocol=4.5, wpid='406B', settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), + _ST.LowresSmoothScroll, + _ST.PointerSpeed, ], ) _D( @@ -440,8 +432,8 @@ _D( protocol=4.5, wpid='406D', settings=[ - _FS.hires_smooth_invert(), - _FS.pointer_speed(), + _ST.HiresSmoothInvert, + _ST.PointerSpeed, ] ) _D('MX Vertical Wireless Mouse', codename='MX Vertical', protocol=4.5, wpid='407B', btid=0xb020, usbid=0xc08a) diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index c7c03261..ec17b5a8 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -306,9 +306,9 @@ class Device: if not self._settings: self._settings = [] if self.persister and self.descriptor and self.descriptor.settings: - for s in self.descriptor.settings: + for sclass in self.descriptor.settings: try: - setting = s(self) + setting = sclass.build(self) except Exception as e: # Do nothing if the device is offline setting = None if self.online: diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 1e872e0d..c0f672c5 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -18,7 +18,6 @@ import math -from copy import copy as _copy from logging import DEBUG as _DEBUG from logging import WARNING as _WARNING from logging import getLogger @@ -58,51 +57,170 @@ def bool_or_toggle(current, new): return None -class Setting: - """A setting descriptor. - Needs to be instantiated for each specific device.""" - __slots__ = ( - 'name', 'label', 'description', 'kind', 'device_kind', 'feature', 'persist', '_rw', '_validator', '_callback', - '_device', '_value' - ) +# moved first for dependency reasons +class Validator: + @classmethod + def build(cls, setting_class, device, **kwargs): + return cls(**kwargs) - def __init__(self, name, rw, validator=None, callback=None, kind=None, device_kind=None, feature=None, persist=True): - assert name - self.name = name[0] - self.label = name[1] - self.description = name[2] - self.device_kind = device_kind - self.feature = getattr(rw, 'feature', None) - self.persist = persist - self._rw = rw - assert (validator and not callback) or (not validator and callback) - self._validator = validator - self._callback = callback - assert kind is None or validator is None or kind & validator.kind != 0 - self.kind = kind or getattr(validator, 'kind', None) +class BooleanValidator(Validator): + __slots__ = ('true_value', 'false_value', 'read_skip_byte_count', 'write_prefix_bytes', 'mask', 'needs_current_value') - def __call__(self, device): - assert not hasattr(self, '_value') - # combined keyboards and touchpads (e.g., K400) break this assertion so don't use it - # assert self.device_kind is None or device.kind in self.device_kind - p = device.protocol - if p == 1.0: - # HID++ 1.0 devices do not support features - assert self._rw.kind == RegisterRW.kind - elif p >= 2.0: - # HID++ 2.0 devices do not support registers - assert self._rw.kind == FeatureRW.kind - o = _copy(self) - if o._callback: - o._validator = o._callback(device) - if o._validator is None: + kind = KIND.toggle + default_true = 0x01 + default_false = 0x00 + # mask specifies all the affected bits in the value + default_mask = 0xFF + + def __init__( + self, + true_value=default_true, + false_value=default_false, + mask=default_mask, + read_skip_byte_count=0, + write_prefix_bytes=b'' + ): + if isinstance(true_value, int): + assert isinstance(false_value, int) + if mask is None: + mask = self.default_mask + else: + assert isinstance(mask, int) + assert true_value & false_value == 0 + assert true_value & mask == true_value + assert false_value & mask == false_value + self.needs_current_value = (mask != self.default_mask) + elif isinstance(true_value, bytes): + if false_value is None or false_value == self.default_false: + false_value = b'\x00' * len(true_value) + else: + assert isinstance(false_value, bytes) + if mask is None or mask == self.default_mask: + mask = b'\xFF' * len(true_value) + else: + assert isinstance(mask, bytes) + assert len(mask) == len(true_value) == len(false_value) + tv = _bytes2int(true_value) + fv = _bytes2int(false_value) + mv = _bytes2int(mask) + assert tv != fv # true and false might be something other than bit values + assert tv & mv == tv + assert fv & mv == fv + self.needs_current_value = any(m != b'\xFF' for m in mask) + else: + raise Exception("invalid mask '%r', type %s" % (mask, type(mask))) + + self.true_value = true_value + self.false_value = false_value + self.mask = mask + self.read_skip_byte_count = read_skip_byte_count + self.write_prefix_bytes = write_prefix_bytes + + def validate_read(self, reply_bytes): + reply_bytes = reply_bytes[self.read_skip_byte_count:] + if isinstance(self.mask, int): + reply_value = ord(reply_bytes[:1]) & self.mask + if _log.isEnabledFor(_DEBUG): + _log.debug('BooleanValidator: validate read %r => %02X', reply_bytes, reply_value) + if reply_value == self.true_value: + return True + if reply_value == self.false_value: + return False + _log.warn( + 'BooleanValidator: reply %02X mismatched %02X/%02X/%02X', reply_value, self.true_value, self.false_value, + self.mask + ) + return False + + count = len(self.mask) + mask = _bytes2int(self.mask) + reply_value = _bytes2int(reply_bytes[:count]) & mask + + true_value = _bytes2int(self.true_value) + if reply_value == true_value: + return True + + false_value = _bytes2int(self.false_value) + if reply_value == false_value: + return False + + _log.warn('BooleanValidator: reply %r mismatched %r/%r/%r', reply_bytes, self.true_value, self.false_value, self.mask) + return False + + def prepare_write(self, new_value, current_value=None): + if new_value is None: + new_value = False + else: + assert isinstance(new_value, bool), 'New value %s for boolean setting is not a boolean' % new_value + + to_write = self.true_value if new_value else self.false_value + + if isinstance(self.mask, int): + if current_value is not None and self.needs_current_value: + to_write |= ord(current_value[:1]) & (0xFF ^ self.mask) + if current_value is not None and to_write == ord(current_value[:1]): return None - assert o.kind is None or o.kind & o._validator.kind != 0 - o.kind = o.kind or o._validator.kind - o._value = None - o._device = device - return o + to_write = bytes([to_write]) + else: + to_write = bytearray(to_write) + count = len(self.mask) + for i in range(0, count): + b = ord(to_write[i:i + 1]) + m = ord(self.mask[i:i + 1]) + assert b & m == b + # b &= m + if current_value is not None and self.needs_current_value: + b |= ord(current_value[i:i + 1]) & (0xFF ^ m) + to_write[i] = b + to_write = bytes(to_write) + + if current_value is not None and to_write == current_value[:len(to_write)]: + return None + + if _log.isEnabledFor(_DEBUG): + _log.debug('BooleanValidator: prepare_write(%s, %s) => %r', new_value, current_value, to_write) + + return self.write_prefix_bytes + to_write + + def acceptable(self, args, current): + if len(args) != 1: + return None + val = bool_or_toggle(current, args[0]) + return [val] if val is not None else None + + +class Setting: + """A setting descriptor. Needs to be instantiated for each specific device.""" + name = label = description = '' + feature = register = kind = None + persist = True + rw_options = {} + validator_class = BooleanValidator + validator_options = {} + + def __init__(self, device, rw, validator): + self._device = device + self._rw = rw + self._validator = validator + self.kind = getattr(self._validator, 'kind', None) + self._value = None + + @classmethod + def build(cls, device): + assert cls.feature or cls.register, 'Settings require either a feature or a register' + rw_class = cls.rw_class if hasattr(cls, 'rw_class') else FeatureRW if cls.feature else RegisterRW + rw = rw_class(cls.feature if cls.feature else cls.register, **cls.rw_options) + p = device.protocol + if p == 1.0: # HID++ 1.0 devices do not support features + assert rw.kind == RegisterRW.kind + elif p >= 2.0: # HID++ 2.0 devices do not support registers + assert rw.kind == FeatureRW.kind + validator_class = cls.validator_class + validator = validator_class.build(cls, device, **cls.validator_options) + if validator: + assert cls.kind is None or cls.kind & validator.kind != 0 + return cls(device, rw, validator) @property def choices(self): @@ -173,7 +291,7 @@ class Setting: current_value = None if self._validator.needs_current_value: - # the validator needs the current value, possibly to merge flag values + # the _validator needs the current value, possibly to merge flag values current_value = self._rw.read(self._device) data_bytes = self._validator.prepare_write(value, current_value) @@ -614,140 +732,7 @@ class FeatureRWMap(FeatureRW): return reply if not self.no_reply else True -# -# value validators -# handle the conversion from read bytes, to setting value, and back -# - - -class BooleanValidator: - __slots__ = ('true_value', 'false_value', 'read_skip_byte_count', 'write_prefix_bytes', 'mask', 'needs_current_value') - - kind = KIND.toggle - default_true = 0x01 - default_false = 0x00 - # mask specifies all the affected bits in the value - default_mask = 0xFF - - def __init__( - self, - true_value=default_true, - false_value=default_false, - mask=default_mask, - read_skip_byte_count=0, - write_prefix_bytes=b'' - ): - if isinstance(true_value, int): - assert isinstance(false_value, int) - if mask is None: - mask = self.default_mask - else: - assert isinstance(mask, int) - assert true_value & false_value == 0 - assert true_value & mask == true_value - assert false_value & mask == false_value - self.needs_current_value = (mask != self.default_mask) - elif isinstance(true_value, bytes): - if false_value is None or false_value == self.default_false: - false_value = b'\x00' * len(true_value) - else: - assert isinstance(false_value, bytes) - if mask is None or mask == self.default_mask: - mask = b'\xFF' * len(true_value) - else: - assert isinstance(mask, bytes) - assert len(mask) == len(true_value) == len(false_value) - tv = _bytes2int(true_value) - fv = _bytes2int(false_value) - mv = _bytes2int(mask) - assert tv != fv # true and false might be something other than bit values - assert tv & mv == tv - assert fv & mv == fv - self.needs_current_value = any(m != b'\xFF' for m in mask) - else: - raise Exception("invalid mask '%r', type %s" % (mask, type(mask))) - - self.true_value = true_value - self.false_value = false_value - self.mask = mask - self.read_skip_byte_count = read_skip_byte_count - self.write_prefix_bytes = write_prefix_bytes - - def validate_read(self, reply_bytes): - reply_bytes = reply_bytes[self.read_skip_byte_count:] - if isinstance(self.mask, int): - reply_value = ord(reply_bytes[:1]) & self.mask - if _log.isEnabledFor(_DEBUG): - _log.debug('BooleanValidator: validate read %r => %02X', reply_bytes, reply_value) - if reply_value == self.true_value: - return True - if reply_value == self.false_value: - return False - _log.warn( - 'BooleanValidator: reply %02X mismatched %02X/%02X/%02X', reply_value, self.true_value, self.false_value, - self.mask - ) - return False - - count = len(self.mask) - mask = _bytes2int(self.mask) - reply_value = _bytes2int(reply_bytes[:count]) & mask - - true_value = _bytes2int(self.true_value) - if reply_value == true_value: - return True - - false_value = _bytes2int(self.false_value) - if reply_value == false_value: - return False - - _log.warn('BooleanValidator: reply %r mismatched %r/%r/%r', reply_bytes, self.true_value, self.false_value, self.mask) - return False - - def prepare_write(self, new_value, current_value=None): - if new_value is None: - new_value = False - else: - assert isinstance(new_value, bool), 'New value %s for boolean setting is not a boolean' % new_value - - to_write = self.true_value if new_value else self.false_value - - if isinstance(self.mask, int): - if current_value is not None and self.needs_current_value: - to_write |= ord(current_value[:1]) & (0xFF ^ self.mask) - if current_value is not None and to_write == ord(current_value[:1]): - return None - to_write = bytes([to_write]) - else: - to_write = bytearray(to_write) - count = len(self.mask) - for i in range(0, count): - b = ord(to_write[i:i + 1]) - m = ord(self.mask[i:i + 1]) - assert b & m == b - # b &= m - if current_value is not None and self.needs_current_value: - b |= ord(current_value[i:i + 1]) & (0xFF ^ m) - to_write[i] = b - to_write = bytes(to_write) - - if current_value is not None and to_write == current_value[:len(to_write)]: - return None - - if _log.isEnabledFor(_DEBUG): - _log.debug('BooleanValidator: prepare_write(%s, %s) => %r', new_value, current_value, to_write) - - return self.write_prefix_bytes + to_write - - def acceptable(self, args, current): - if len(args) != 1: - return None - - val = bool_or_toggle(current, args[0]) - return [val] if val is not None else None - - -class BitFieldValidator: +class BitFieldValidator(Validator): __slots__ = ('byte_count', 'options') kind = KIND.multiple_toggle @@ -791,7 +776,7 @@ class BitFieldValidator: return None if val is None else [str(int(key)), val] -class BitFieldWithOffsetAndMaskValidator: +class BitFieldWithOffsetAndMaskValidator(Validator): __slots__ = ('byte_count', 'options', '_option_from_key', '_mask_from_offset', '_option_from_offset_mask') kind = KIND.multiple_toggle @@ -882,13 +867,16 @@ class BitFieldWithOffsetAndMaskValidator: return None if val is None else [str(key), val] -class ChoicesValidator: - kind = KIND.choice +class ChoicesValidator(Validator): """Translates between NamedInts and a byte sequence. :param choices: a list of NamedInts :param byte_count: the size of the derived byte sequence. If None, it will be calculated from the choices.""" - def __init__(self, choices, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b''): + kind = KIND.choice + choices_universe = None # the possible choices, or an empty sequence for anything + choices_extra = None # an extra choice, so as not to require extending a large NamedInts + + def __init__(self, choices=None, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b''): assert choices is not None assert isinstance(choices, _NamedInts) assert len(choices) > 1 @@ -943,6 +931,8 @@ class ChoicesValidator: class ChoicesMapValidator(ChoicesValidator): kind = KIND.map_choice + keys_universe = None # the possible keys, or an empty sequence for anything + choices_universe = None # the possible choices, or an empty sequence for anything def __init__( self, @@ -1013,16 +1003,23 @@ class ChoicesMapValidator(ChoicesValidator): return [str(int(key)), int(choice)] if choice is not None else None -class RangeValidator: - __slots__ = ('min_value', 'max_value', 'flag', '_byte_count', 'needs_current_value') - +class RangeValidator(Validator): kind = KIND.range """Translates between integers and a byte sequence. :param min_value: minimum accepted value (inclusive) :param max_value: maximum accepted value (inclusive) :param byte_count: the size of the derived byte sequence. If None, it will be calculated from the range.""" - def __init__(self, min_value, max_value, byte_count=None): + min_value = 0 + max_value = 255 + + @classmethod + def build(cls, setting_class, device, **kwargs): + kwargs['min_value'] = setting_class.min_value + kwargs['max_value'] = setting_class.max_value + return cls(**kwargs) + + def __init__(self, min_value=0, max_value=255, byte_count=1): assert max_value > min_value self.min_value = min_value self.max_value = max_value @@ -1051,7 +1048,7 @@ class RangeValidator: return None if len(args) != 1 or type(arg) != int or arg < self.min_value or arg > self.max_value else args -class MultipleRangeValidator: +class MultipleRangeValidator(Validator): kind = KIND.multiple_range @@ -1126,7 +1123,8 @@ class MultipleRangeValidator: class ActionSettingRW: """Special RW class for settings that turn on and off special processing when a key or button is depressed""" - def __init__(self, name, divert_setting_name): + def __init__(self, feature, name='', divert_setting_name='divert-keys'): + self.feature = feature # not used? self.name = name self.divert_setting_name = divert_setting_name self.kind = FeatureRW.kind # pretend to be FeatureRW as required for HID++ 2.0 devices diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 3d43a543..a2208946 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -16,7 +16,6 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from collections import namedtuple from logging import DEBUG as _DEBUG from logging import INFO as _INFO from logging import getLogger @@ -38,14 +37,12 @@ from .settings import BitFieldSetting as _BitFieldSetting from .settings import BitFieldValidator as _BitFieldV from .settings import BitFieldWithOffsetAndMaskSetting as _BitFieldOMSetting from .settings import BitFieldWithOffsetAndMaskValidator as _BitFieldOMV -from .settings import BooleanValidator as _BooleanV from .settings import ChoicesMapValidator as _ChoicesMapV from .settings import ChoicesValidator as _ChoicesV from .settings import FeatureRW as _FeatureRW from .settings import LongSettings as _LongSettings from .settings import MultipleRangeValidator as _MultipleRangeV from .settings import RangeValidator as _RangeV -from .settings import RegisterRW as _RegisterRW from .settings import Setting as _Setting from .settings import Settings as _Settings from .special_keys import DISABLE as _DKEY @@ -60,314 +57,482 @@ _F = _hidpp20.FEATURE _GG = _hidpp20.GESTURE _GP = _hidpp20.PARAM +# Setting classes are used to control the settings that the Solaar GUI shows and manipulates. +# Each setting class has to several class variables: +# name, which is used as a key when storing information about the setting, +# setting classes can have the same name, as long as devices only have one setting with the same name; +# label, which is shown in the Solaar main window; +# description, which is shown when the mouse hovers over the setting in the main window; +# either register or feature, the register or feature that the setting uses; +# rw_class, the class of the reader/writer (if it is not the standard one), +# rw_options, a dictionary of options for the reader/writer. +# validator_class, the class of the validator (default settings.BooleanValidator) +# validator_options, a dictionary of options for the validator +# persist (inherited True), which is whether to store the value and apply it when setting up the device. # -# common strings for settings - name, string to display in main window, tool tip for main window +# The different setting classes imported from settings.py are for different numbers and kinds of arguments. +# _Setting is for settings with a single value (boolean, number in a range, and symbolic choice). +# _Settings is for settings that are maps from keys to values +# and permit reading or writing the entire map or just one key/value pair. +# The _BitFieldSetting class is for settings that have multiple boolean values packed into a bit field. +# _BitFieldOMSetting is similar. +# _LongSettings is for settings that have an even more complex structure. # +# When settings are created a reader/writer and a validator are created. -# yapf: disable -_HAND_DETECTION = ('hand-detection', _('Hand Detection'), _('Turn on illumination when the hands hover over the keyboard.')) -_SMOOTH_SCROLL = ('smooth-scroll', _('Scroll Wheel Smooth Scrolling'), - _('High-sensitivity mode for vertical scroll with the wheel.')) -_SIDE_SCROLL = ('side-scroll', _('Side Scrolling'), - _('When disabled, pushing the wheel sideways sends custom button events\n' - 'instead of the standard side-scrolling events.')) -_HI_RES_SCROLL = ('hi-res-scroll', _('Scroll Wheel High Resolution'), - _('High-sensitivity mode for vertical scroll with the wheel.')) -_LOW_RES_SCROLL = ('lowres-smooth-scroll', _('Scroll Wheel Diversion'), - _('HID++ mode for vertical scroll with the wheel.') + '\n' + - _('Effectively turns off wheel scrolling in Linux.')) -_HIRES_INV = ('hires-smooth-invert', _('Scroll Wheel Direction'), - _('Invert direction for vertical scroll with wheel.')) -_HIRES_RES = ('hires-smooth-resolution', _('Scroll Wheel Resolution'), - _('High-sensitivity mode for vertical scroll with the wheel.')) -_REPORT_RATE = ('report_rate', _('Polling Rate (ms)'), _('Frequency of device polling, in milliseconds')) -_FN_SWAP = ('fn-swap', _('Swap Fx function'), - _('When set, the F1..F12 keys will activate their special function,\n' - 'and you must hold the FN key to activate their standard function.') + '\n\n' + - _('When unset, the F1..F12 keys will activate their standard function,\n' - 'and you must hold the FN key to activate their special function.')) -_DPI = ('dpi', _('Sensitivity (DPI)'), _('Mouse movement sensitivity')) -_POINTER_SPEED = ('pointer_speed', _('Sensitivity (Pointer Speed)'), - _('Speed multiplier for mouse (256 is normal multiplier).')) -_SMART_SHIFT = ('smart-shift', _('Scroll Wheel Rachet'), - _('Automatically switch the mouse wheel between ratchet and freespin mode.\n' - 'The mouse wheel is always free at 0, and always ratcheted at 50')) -_BACKLIGHT = ('backlight', _('Backlight'), _('Turn illumination on or off on keyboard.')) -_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _('Key/Button Actions'), - _('Change the action for the key or button.') + '\n' + - _('Changing important actions (such as for the left mouse button) can result in an unusable system.')) -_DIVERT_KEYS = ('divert-keys', _('Key/Button Diversion'), - _('Make the key or button send HID++ notifications (which trigger Solaar rules but are otherwise ignored).')) -_DISABLE_KEYS = ('disable-keyboard-keys', _('Disable keys'), _('Disable specific keyboard keys.')) -_PLATFORM = ('multiplatform', _('Set OS'), _('Change keys to match OS.')) -_CHANGE_HOST = ('change-host', _('Change Host'), _('Switch connection to a different host')) -_THUMB_SCROLL_MODE = ('thumb-scroll-mode', _('Thumb Wheel Diversion'), - _('HID++ mode for horizontal scroll with the thumb wheel.') + '\n' + - _('Effectively turns off thumb scrolling in Linux.')) -_THUMB_SCROLL_INVERT = ('thumb-scroll-invert', _('Thumb Wheel Direction'), _('Invert thumb wheel scroll direction.')) -_GESTURE2_GESTURES = ('gesture2-gestures', _('Gestures'), _('Tweak the mouse/touchpad behaviour.')) -_GESTURE2_PARAMS = ('gesture2-params', _('Gesture params'), _('Change numerical parameters of a mouse/touchpad.')) -_DPI_SLIDING = ('dpi-sliding', _('DPI Sliding Adjustment'), - _('Adjust the DPI by sliding the mouse horizontally while holding the button down.')) -_MOUSE_GESTURES = ('mouse-gestures', _('Mouse Gestures'), - _('Send a gesture by sliding the mouse while holding the button down.')) -_DIVERT_CROWN = ('divert-crown', _('Divert crown events'), - _('Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).')) -_CROWN_SMOOTH = ('crown-smooth', _('Crown smooth scroll'), - _('Set crown smooth scroll')) -_DIVERT_GKEYS = ('divert-gkeys', _('Divert G Keys'), - _('Make G keys send GKEY HID++ notifications (which trigger Solaar rules but are otherwise ignored).')) -_SPEED_CHANGE = ('speed-change', _('Sensitivity Switching'), - _('Switch the current sensitivity and the remembered sensitivity when the key or button is pressed.\n' - 'If there is no remembered sensitivity, just remember the current sensitivity')) - -_GESTURE2_GESTURES_LABELS = { - _GG['Tap1Finger']: (_('Single tap'), _('Performs a left click.')), - _GG['Tap2Finger']: (_('Single tap with two fingers'), _('Performs a right click.')), - _GG['Tap3Finger']: (_('Single tap with three fingers'), None), - _GG['Click1Finger']: (None, None), - _GG['Click2Finger']: (None, None), - _GG['Click3Finger']: (None, None), - _GG['DoubleTap1Finger']: (_('Double tap'), _('Performs a double click.')), - _GG['DoubleTap2Finger']: (_('Double tap with two fingers'), None), - _GG['DoubleTap3Finger']: (_('Double tap with three fingers'), None), - _GG['Track1Finger']: (None, None), - _GG['TrackingAcceleration']: (None, None), - _GG['TapDrag1Finger']: (_('Tap and drag'), _('Drags items by dragging the finger after double tapping.')), - _GG['TapDrag2Finger']: (_('Tap and drag with two fingers'), - _('Drags items by dragging the fingers after double tapping.')), - _GG['Drag3Finger']: (_('Tap and drag with three fingers'), None), - _GG['TapGestures']: (None, None), - _GG['FnClickGestureSuppression']: (_('Suppress tap and edge gestures'), - _('Disables tap and edge gestures (equivalent to pressing Fn+LeftClick).')), - _GG['Scroll1Finger']: (_('Scroll with one finger'), _('Scrolls.')), - _GG['Scroll2Finger']: (_('Scroll with two fingers'), _('Scrolls.')), - _GG['Scroll2FingerHoriz']: (_('Scroll horizontally with two fingers'), _('Scrolls horizontally.')), - _GG['Scroll2FingerVert']: (_('Scroll vertically with two fingers'), _('Scrolls vertically.')), - _GG['Scroll2FingerStateless']: (_('Scroll with two fingers'), _('Scrolls.')), - _GG['NaturalScrolling']: (_('Natural scrolling'), _('Inverts the scrolling direction.')), - _GG['Thumbwheel']: (_('Thumbwheel'), _('Enables the thumbwheel.')), - _GG['VScrollInertia']: (None, None), - _GG['VScrollBallistics']: (None, None), - _GG['Swipe2FingerHoriz']: (None, None), - _GG['Swipe3FingerHoriz']: (None, None), - _GG['Swipe4FingerHoriz']: (None, None), - _GG['Swipe3FingerVert']: (None, None), - _GG['Swipe4FingerVert']: (None, None), - _GG['LeftEdgeSwipe1Finger']: (None, None), - _GG['RightEdgeSwipe1Finger']: (None, None), - _GG['BottomEdgeSwipe1Finger']: (None, None), - _GG['TopEdgeSwipe1Finger']: (_('Swipe from the top edge'), None), - _GG['LeftEdgeSwipe1Finger2']: (_('Swipe from the left edge'), None), - _GG['RightEdgeSwipe1Finger2']: (_('Swipe from the right edge'), None), - _GG['BottomEdgeSwipe1Finger2']: (_('Swipe from the bottom edge'), None), - _GG['TopEdgeSwipe1Finger2']: (_('Swipe from the top edge'), None), - _GG['LeftEdgeSwipe2Finger']: (_('Swipe two fingers from the left edge'), None), - _GG['RightEdgeSwipe2Finger']: (_('Swipe two fingers from the right edge'), None), - _GG['BottomEdgeSwipe2Finger']: (_('Swipe two fingers from the bottom edge'), None), - _GG['TopEdgeSwipe2Finger']: (_('Swipe two fingers from the top edge'), None), - _GG['Zoom2Finger']: (_('Zoom with two fingers.'), _('Pinch to zoom out; spread to zoom in.')), - _GG['Zoom2FingerPinch']: (_('Pinch to zoom out.'), _('Pinch to zoom out.')), - _GG['Zoom2FingerSpread']: (_('Spread to zoom in.'), _('Spread to zoom in.')), - _GG['Zoom3Finger']: (_('Zoom with three fingers.'), None), - _GG['Zoom2FingerStateless']: (_('Zoom with two fingers'), _('Pinch to zoom out; spread to zoom in.')), - _GG['TwoFingersPresent']: (None, None), - _GG['Rotate2Finger']: (None, None), - _GG['Finger1']: (None, None), - _GG['Finger2']: (None, None), - _GG['Finger3']: (None, None), - _GG['Finger4']: (None, None), - _GG['Finger5']: (None, None), - _GG['Finger6']: (None, None), - _GG['Finger7']: (None, None), - _GG['Finger8']: (None, None), - _GG['Finger9']: (None, None), - _GG['Finger10']: (None, None), - _GG['DeviceSpecificRawData']: (None, None), -} - -_GESTURE2_PARAMS_LABELS = { - _GP['ExtraCapabilities']: (None, None), # not supported - _GP['PixelZone']: (_('Pixel zone'), None), # TO DO: replace None with a short description - _GP['RatioZone']: (_('Ratio zone'), None), # TO DO: replace None with a short description - _GP['ScaleFactor']: (_('Scale factor'), _('Sets the cursor speed.')), -} - -_GESTURE2_PARAMS_LABELS_SUB = { - 'left': (_('Left'), _('Left-most coordinate.')), - 'top': (_('top'), _('Top-most coordinate.')), - 'width': (_('width'), _('Width.')), - 'height': (_('height'), _('Height.')), - 'scale': (_('Scale'), _('Cursor speed.')), -} - -_DISABLE_KEYS_LABEL_SUB = _('Disables the %s key.') - -# yapf: enable - -# Setting template functions need to set up the setting itself, the validator, and the reader/writer. -# The reader/writer is responsible for reading raw values from the device and writing values to it. -# The validator is responsible for turning read raw values into Python data and Python data into raw values to be written. -# The setting keeps everything together and provides control. -# -# The _Setting class is for settings with simple values (booleans, numbers in a range, and symbolic choices). -# Its positional arguments are the strings for the setting and the reader/writer. -# The validator keyword (or third) argument is the validator, if the validator does not depend on information from the device. -# The callback keyword argument is a function that given a device as argument returns the validator or None. -# If the callback function returns None the setting is not created for the device. -# Either a validator or callback must be specified, but not both. -# The device_kind keyword argument (default None) says what kinds of devices can use the setting. -# (This argument is currently not used because keyboards with integrated trackpads break its abstraction.) -# The persist keyword argument (default True) says whether to store the value and apply it when setting up the device. -# -# There are two simple reader/writers - _RegisterRW and _FeatureRW. -# _RegisterRW is for register-based settings and takes the register name as argument. -# _FeatureRW is for feature-based settings and takes the feature name as positional argument plus the following: -# read_fnid is the feature function (times 16) to read the value (default 0x00), -# write_fnid is the feature function (times 16) to write the value (default 0x10), -# prefix is a prefix to add to the data being written and the read request (default b''), used for features +# If the setting class has a value for rw_class then an instance of that class is created. +# Otherwise if the setting has a register then an instance of settings.RegisterRW is created. +# and if the setting has a feature then then an instance of _FeatureRW is created. +# The instance is created with the register or feature as the first argument and rw_options as keyword arguments. +# _RegisterRW doesn't use any options. +# _FeatureRW options include +# read_fnid - the feature function (times 16) to read the value (default 0x00), +# write_fnid - the feature function (times 16) to write the value (default 0x10), +# prefix - a prefix to add to the data being written and the read request (default b''), used for features # that provide and set multiple settings (e.g., to read and write function key inversion for current host) -# no_reply is whether to wait for a reply (default false) (USE WITH EXTREME CAUTION). +# no_reply - whether to wait for a reply (default false) (USE WITH EXTREME CAUTION). # -# There are three simple validators - _BooleanV, _RangeV, and _ChoicesV -# _BooleanV is for boolean values. It takes five keyword arguments +# There are three simple validator classes - _BooleanV, _RangeV, and _ChoicesV +# _BooleanV is for boolean values and is the default. It takes # true_value is the raw value for true (default 0x01), this can be an integer or a byte string, # false_value is the raw value for false (default 0x00), this can be an integer or a byte string, # mask is used to keep only some bits from a sequence of bits, this can be an integer or a byte string, # read_skip_byte_count is the number of bytes to ignore at the beginning of the read value (default 0), # write_prefix_bytes is a byte string to write before the value (default empty). -# _RangeV is for an integer in a range. It takes three keyword arguments: -# min_value is the minimum value for the setting, -# max_value is the maximum value for the setting, +# _RangeV is for an integer in a range. It takes # byte_count is number of bytes that the value is stored in (defaults to size of max_value). +# _RangeV uses min_value and max_value from the setting class as minimum and maximum. + # _ChoicesV is for symbolic choices. It takes one positional and three keyword arguments: -# the positional argument is a list of named integers that are the valid choices, +# choices is a list of named integers that are the valid choices, # byte_count is the number of bytes for the integer (default size of largest choice integer), # read_skip_byte_count is as for _BooleanV, # write_prefix_bytes is as for _BooleanV. -# -# The _Settings class is for settings that are maps from keys to values. -# The _BitFieldSetting class is for settings that have multiple boolean values packed into a bit field. -# They have has same arguments as the _Setting class. -# -# _ChoicesMapV validator is for map settings that map onto symbolic choices. It takes four keyword arguments: -# the positional argument is the choices map +# Settings that use _ChoicesV should have a choices_universe class variable of the potential choices, +# or None for no limitation and optionally a choices_extra class variable with an extra choice. +# The choices_extra is so that there is no need to specially extend a large existing NamedInts. +# _ChoicesMapV validator is for map settings that map onto symbolic choices. It takes +# choices_map is a map from keys to possible values # byte_count is as for _ChoicesV, # read_skip_byte_count is as for _ChoicesV, # write_prefix_bytes is as for _ChoicesV, # key_byte_count is the number of bytes for the key integer (default size of largest key), # extra_default is an extra raw value that is used as a default value (default None). +# Settings that use _ChoicesV should have keys_universe and choices_universe class variable of +# the potential keys and potential choices or None for no limitation. + # _BitFieldV validator is for bit field settings. It takes one positional and one keyword argument # the positional argument is the number of bits in the bit field # byte_count is the size of the bit field (default size of the bit field) +# +# A few settings work very differently. They divert a key, which is then used to start and stop some special action. +# These settings have reader/writer classes that perform special processing instead of sending commands to the device. -def _register_hand_detection(): - validator = _BooleanV(true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF') - return _Setting(_HAND_DETECTION, _RegisterRW(_R.keyboard_hand_detection), validator, device_kind=(_DK.keyboard, )) +# yapf: disable +class FnSwapVirtual(_Setting): # virtual setting to hold fn swap strings + name = 'fn-swap' + label = _('Swap Fx function') + description = (_('When set, the F1..F12 keys will activate their special function,\n' + 'and you must hold the FN key to activate their standard function.') + '\n\n' + + _('When unset, the F1..F12 keys will activate their standard function,\n' + 'and you must hold the FN key to activate their special function.')) +# yapf: enable -def _register_fn_swap(): - validator = _BooleanV(true_value=b'\x00\x01', mask=b'\x00\x01') - return _Setting(_FN_SWAP, _RegisterRW(_R.keyboard_fn_swap), validator, device_kind=(_DK.keyboard, )) +class RegisterHandDetection(_Setting): + name = 'hand-detection' + label = _('Hand Detection') + description = _('Turn on illumination when the hands hover over the keyboard.') + register = _R.keyboard_hand_detection + validator_options = {'true_value': b'\x00\x00\x00', 'false_value': b'\x00\x00\x30', 'mask': b'\x00\x00\xFF'} -def _register_smooth_scroll(): - validator = _BooleanV(true_value=0x40, mask=0x40) - return _Setting(_SMOOTH_SCROLL, _RegisterRW(_R.mouse_button_flags), validator, device_kind=(_DK.mouse, _DK.trackball)) +class RegisterSmoothScroll(_Setting): + name = 'smooth-scroll' + label = _('Scroll Wheel Smooth Scrolling') + description = _('High-sensitivity mode for vertical scroll with the wheel.') + register = _R.mouse_button_flags + validator_options = {'true_value': 0x40, 'mask': 0x40} -def _register_side_scroll(): - validator = _BooleanV(true_value=0x02, mask=0x02) - return _Setting(_SIDE_SCROLL, _RegisterRW(_R.mouse_button_flags), validator, device_kind=(_DK.mouse, _DK.trackball)) +class RegisterSideScroll(_Setting): + name = 'side-scroll' + label = _('Side Scrolling') + description = _( + 'When disabled, pushing the wheel sideways sends custom button events\n' + 'instead of the standard side-scrolling events.' + ) + register = _R.mouse_button_flags + validator_options = {'true_value': 0x02, 'mask': 0x02} -def _register_dpi(choices=None): - return _Setting(_DPI, _RegisterRW(_R.mouse_dpi), _ChoicesV(choices), device_kind=(_DK.mouse, _DK.trackball)) +# different devices have different sets of permissable dpis, so this should be subclassed +class RegisterDpi(_Setting): + name = 'dpi' + label = _('Sensitivity (DPI)') + description = _('Mouse movement sensitivity') + register = _R.mouse_dpi + choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) + validator_options = {'choices': choices_universe} -def _feature_fn_swap(): - return _Setting(_FN_SWAP, _FeatureRW(_F.FN_INVERSION), _BooleanV(), device_kind=(_DK.keyboard, )) +class RegisterFnSwap(FnSwapVirtual): + register = _R.keyboard_fn_swap + validator_options = {'true_value': b'\x00\x01', 'mask': b'\x00\x01'} -def _feature_new_fn_swap(): - return _Setting(_FN_SWAP, _FeatureRW(_F.NEW_FN_INVERSION), _BooleanV(), device_kind=(_DK.keyboard, )) +class FnSwap(FnSwapVirtual): + feature = _F.FN_INVERSION + + +class NewFnSwap(FnSwapVirtual): + feature = _F.NEW_FN_INVERSION # ignore the capabilities part of the feature - all devices should be able to swap Fn state # just use the current host (first byte = 0xFF) part of the feature to read and set the Fn state -def _feature_k375s_fn_swap(): - validator = _BooleanV(true_value=b'\x01', false_value=b'\x00', read_skip_byte_count=1) - return _Setting(_FN_SWAP, _FeatureRW(_F.K375S_FN_INVERSION, prefix=b'\xFF'), validator, device_kind=(_DK.keyboard, )) +class K375sFnSwap(FnSwapVirtual): + feature = _F.K375S_FN_INVERSION + rw_options = {'prefix': b'\xFF'} + validator_options = {'true_value': b'\x01', 'false_value': b'\x00', 'read_skip_byte_count': 1} -# FIXME: This will enable all supported backlight settings, # we should allow the users to select which settings they want to enable. -def _feature_backlight2(): - return _Setting(_BACKLIGHT, _FeatureRW(_F.BACKLIGHT2), _BooleanV(), device_kind=(_DK.keyboard, )) +class Backlight2(_Setting): + name = 'backlight' + label = _('Backlight') + description = _('Turn illumination on or off on keyboard.') + feature = _F.BACKLIGHT2 -def _feature_hi_res_scroll(): - return _Setting(_HI_RES_SCROLL, _FeatureRW(_F.HI_RES_SCROLLING), _BooleanV(), device_kind=(_DK.mouse, _DK.trackball)) +class HiResScroll(_Setting): + name = 'hi-res-scroll' + label = _('Scroll Wheel High Resolution') + description = _('High-sensitivity mode for vertical scroll with the wheel.') + feature = _F.HI_RES_SCROLLING -def _feature_lowres_smooth_scroll(): - return _Setting(_LOW_RES_SCROLL, _FeatureRW(_F.LOWRES_WHEEL), _BooleanV(), device_kind=(_DK.mouse, _DK.trackball)) +class LowresSmoothScroll(_Setting): + name = 'lowres-smooth-scroll' + label = _('Scroll Wheel Diversion') + description = ( + _('HID++ mode for vertical scroll with the wheel.') + '\n' + _('Effectively turns off wheel scrolling in Linux.') + ) + feature = _F.LOWRES_WHEEL -def _feature_hires_smooth_invert(): - rw = _FeatureRW(_F.HIRES_WHEEL, read_fnid=0x10, write_fnid=0x20) - validator = _BooleanV(true_value=0x04, mask=0x04) - return _Setting(_HIRES_INV, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) +class HiresSmoothInvert(_Setting): + name = 'hires-smooth-invert' + label = _('Scroll Wheel Direction') + description = _('Invert direction for vertical scroll with wheel.') + feature = _F.HIRES_WHEEL + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': 0x04, 'mask': 0x04} -def _feature_hires_smooth_resolution(): - rw = _FeatureRW(_F.HIRES_WHEEL, read_fnid=0x10, write_fnid=0x20) - validator = _BooleanV(true_value=0x02, mask=0x02) - return _Setting(_HIRES_RES, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) +class HiresSmoothResolution(_Setting): + name = 'hires-smooth-resolution' + label = _('Scroll Wheel Resolution') + description = _('High-sensitivity mode for vertical scroll with the wheel.') + feature = _F.HIRES_WHEEL + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': 0x02, 'mask': 0x02} -class _SmartShiftRW(_FeatureRW): - MIN_VALUE = 0 - MAX_VALUE = 50 - - def __init__(self, feature, read_fnid, write_fnid): - super().__init__(feature, read_fnid, write_fnid) - - def read(self, device): - value = super().read(device) - if _bytes2int(value[0:1]) == 1: - # Mode = Freespin, map to minimum - return _int2bytes(_SmartShiftRW.MIN_VALUE, count=1) - else: - # Mode = smart shift, map to the value, capped at maximum - threshold = min(_bytes2int(value[1:2]), _SmartShiftRW.MAX_VALUE) - return _int2bytes(threshold, count=1) - - def write(self, device, data_bytes): - threshold = _bytes2int(data_bytes) - # Freespin at minimum - mode = 1 if threshold == _SmartShiftRW.MIN_VALUE else 2 - # Ratchet at maximum - if threshold == _SmartShiftRW.MAX_VALUE: - threshold = 255 - data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) - return super().write(device, data) +class PointerSpeed(_Setting): + name = 'pointer_speed' + label = _('Sensitivity (Pointer Speed)') + description = _('Speed multiplier for mouse (256 is normal multiplier).') + feature = _F.POINTER_SPEED + validator_class = _RangeV + min_value = 0x002e + max_value = 0x01ff + validator_options = {'byte_count': 2} -def _feature_smart_shift(): - validator = _RangeV(_SmartShiftRW.MIN_VALUE, _SmartShiftRW.MAX_VALUE, 1) - rw = _SmartShiftRW(_F.SMART_SHIFT, read_fnid=0x00, write_fnid=0x10) - return _Setting(_SMART_SHIFT, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) +class ThumbMode(_Setting): + name = 'thumb-scroll-mode' + label = _('Thumb Wheel Diversion') + description = _('HID++ mode for horizontal scroll with the thumb wheel.') + '\n' + \ + _('Effectively turns off thumb scrolling in Linux.') + feature = _F.THUMB_WHEEL + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': b'\x01\x00', 'false_value': b'\x00\x00', 'mask': b'\x01\x00'} -def _feature_smart_shift_enhanced(): - validator = _RangeV(_SmartShiftRW.MIN_VALUE, _SmartShiftRW.MAX_VALUE, 1) - rw = _SmartShiftRW(_F.SMART_SHIFT_ENHANCED, read_fnid=0x10, write_fnid=0x20) - return _Setting(_SMART_SHIFT, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) +class ThumbInvert(_Setting): + name = 'thumb-scroll-invert' + label = _('Thumb Wheel Direction') + description = _('Invert thumb wheel scroll direction.') + feature = _F.THUMB_WHEEL + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': b'\x00\x01', 'false_value': b'\x00\x00', 'mask': b'\x00\x01'} -def _feature_dpi_sliding(): +class ReportRate(_Setting): + name = 'report_rate' + label = _('Polling Rate (ms)') + description = _('Frequency of device polling, in milliseconds') + feature = _F.REPORT_RATE + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + choices_universe = _NamedInts.range(1, 8) + + class rw_class(_FeatureRW): + def write(self, device, data_bytes): + # Host mode is required for report rate to be adjustable + if _hidpp20.get_onboard_mode(device) != _hidpp20.ONBOARD_MODES.MODE_HOST: + _hidpp20.set_onboard_mode(device, _hidpp20.ONBOARD_MODES.MODE_HOST) + return super().write(device, data_bytes) + + class validator_class(_ChoicesV): + @classmethod + def build(cls, setting_class, device): + if device.wpid == '408E': + return None # host mode borks the function keys on the G915 TKL keyboard + reply = device.feature_request(_F.REPORT_RATE, 0x00) + assert reply, 'Oops, report rate choices cannot be retrieved!' + rate_list = [] + rate_flags = _bytes2int(reply[0:1]) + for i in range(0, 8): + if (rate_flags >> i) & 0x01: + rate_list.append(i + 1) + return cls(choices=_NamedInts.list(rate_list), byte_count=1) if rate_list else None + + +class DivertCrown(_Setting): + name = 'divert-crown' + label = _('Divert crown events') + description = _('Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).') + feature = _F.CROWN + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': 0x02, 'false_value': 0x01, 'mask': 0xff} + + +class CrownSmooth(_Setting): + name = 'crown-smooth' + label = _('Crown smooth scroll') + description = _('Set crown smooth scroll') + feature = _F.CROWN + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + validator_options = {'true_value': 0x01, 'false_value': 0x02, 'read_skip_byte_count': 1, 'write_prefix_bytes': b'\x00'} + + +class DivertGkeys(_Setting): + name = 'divert-gkeys' + label = _('Divert G Keys') + description = _('Make G keys send GKEY HID++ notifications (which trigger Solaar rules but are otherwise ignored).') + feature = _F.GKEY + validator_options = {'true_value': 0x01, 'false_value': 0x00, 'mask': 0xff} + + class rw_class(_FeatureRW): + def __init__(self, feature): + super().__init__(feature, write_fnid=0x20) + + def read(self, device): # no way to read, so just assume not diverted + return b'\x00' + + +class SmartShift(_Setting): + name = 'smart-shift' + label = _('Scroll Wheel Rachet') + description = _( + 'Automatically switch the mouse wheel between ratchet and freespin mode.\n' + 'The mouse wheel is always free at 0, and always ratcheted at 50' + ) + feature = _F.SMART_SHIFT + rw_options = {'read_fnid': 0x00, 'write_fnid': 0x10} + + class rw_class(_FeatureRW): + MIN_VALUE = 0 + MAX_VALUE = 50 + + def __init__(self, feature, read_fnid, write_fnid): + super().__init__(feature, read_fnid, write_fnid) + + def read(self, device): + value = super().read(device) + if _bytes2int(value[0:1]) == 1: + # Mode = Freespin, map to minimum + return _int2bytes(self.MIN_VALUE, count=1) + else: + # Mode = smart shift, map to the value, capped at maximum + threshold = min(_bytes2int(value[1:2]), self.MAX_VALUE) + return _int2bytes(threshold, count=1) + + def write(self, device, data_bytes): + threshold = _bytes2int(data_bytes) + # Freespin at minimum + mode = 1 if threshold == self.MIN_VALUE else 2 + # Ratchet at maximum + if threshold == self.MAX_VALUE: + threshold = 255 + data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) + return super().write(device, data) + + min_value = rw_class.MIN_VALUE + max_value = rw_class.MAX_VALUE + validator_class = _RangeV + + +class SmartShiftEnhanced(SmartShift): + feature = _F.SMART_SHIFT_ENHANCED + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + + +# the keys for the choice map are Logitech controls (from special_keys) +# each choice value is a NamedInt with the string from a task (to be shown to the user) +# and the integer being the control number for that task (to be written to the device) +# Solaar only remaps keys (controlled by key gmask and group), not other key reprogramming +class ReprogrammableKeys(_Settings): + name = 'reprogrammable-keys' + label = _('Key/Button Actions') + description = ( + _('Change the action for the key or button.') + '\n' + + _('Changing important actions (such as for the left mouse button) can result in an unusable system.') + ) + feature = _F.REPROG_CONTROLS_V4 + keys_universe = _special_keys.CONTROL + choices_universe = _special_keys.CONTROL + + class rw_class: + def __init__(self, feature): + self.feature = feature + self.kind = _FeatureRW.kind + + def read(self, device, key): + key_index = device.keys.index(key) + key_struct = device.keys[key_index] + return b'\x00\x00' + _int2bytes(int(key_struct.mapped_to), 2) + + def write(self, device, key, data_bytes): + key_index = device.keys.index(key) + key_struct = device.keys[key_index] + key_struct.remap(_special_keys.CONTROL[_bytes2int(data_bytes)]) + return True + + class validator_class(_ChoicesMapV): + @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 not choices: + return None + return cls(choices, key_byte_count=2, byte_count=2, extra_default=0) + + +class DivertKeys(_Settings): + name = 'divert-keys' + label = _('Key/Button Diversion') + description = _('Make the key or button send HID++ notifications (which trigger Solaar rules but are otherwise ignored).') + feature = _F.REPROG_CONTROLS_V4 + keys_universe = _special_keys.CONTROL + choices_universe = [_NamedInt(0x00, _('Regular')), _NamedInt(0x01, _('Diverted'))] + + class rw_class: + def __init__(self, feature): + self.feature = feature + self.kind = _FeatureRW.kind + + def read(self, device, key): + key_index = device.keys.index(key) + key_struct = device.keys[key_index] + return b'\x00\x00\x01' if 'diverted' in key_struct.mapping_flags else b'\x00\x00\x00' + + def write(self, device, key, data_bytes): + key_index = device.keys.index(key) + key_struct = device.keys[key_index] + key_struct.set_diverted(data_bytes == b'\x01') + return True + + class validator_class(_ChoicesMapV): + @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 not choices: + return None + return cls(choices, key_byte_count=2, byte_count=1, mask=0x01) + + +class AdjustableDpi(_Setting): + """Pointer Speed feature""" + # Assume sensorIdx 0 (there is only one sensor) + # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB + # [3] setSensorDpi(sensorIdx, dpi) + name = 'dpi' + label = _('Sensitivity (DPI)') + description = _('Mouse movement sensitivity') + feature = _F.ADJUSTABLE_DPI + rw_options = {'read_fnid': 0x20, 'write_fnid': 0x30} + + class validator_class(_ChoicesV): + @classmethod + def build(cls, setting_class, device): + # [1] getSensorDpiList(sensorIdx) + reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) + assert reply, 'Oops, DPI list cannot be retrieved!' + dpi_list = [] + step = None + for val in _unpack('!7H', reply[1:1 + 14]): + if val == 0: + break + if val >> 13 == 0b111: + assert step is None and len(dpi_list) == 1, \ + 'Invalid DPI list item: %r' % val + step = val & 0x1fff + else: + dpi_list.append(val) + if step: + assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list + dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) + return cls(choices=_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None + + +class SpeedChange(_Setting): + """Implements the ability to switch Sensitivity by clicking on the DPI_Change button.""" + name = 'speed-change' + label = _('Sensitivity Switching') + description = _( + 'Switch the current sensitivity and the remembered sensitivity when the key or button is pressed.\n' + 'If there is no remembered sensitivity, just remember the current sensitivity' + ) + choices_universe = _special_keys.CONTROL + choices_extra = _NamedInt(0, _('Off')) + feature = _F.POINTER_SPEED + rw_options = {'name': 'speed change'} + + class _SpeedChangeRW(_ActionSettingRW): + def press_action(self): # switch sensitivity + currentSpeed = self.device.persister.get('pointer_speed', None) if self.device.persister else None + newSpeed = self.device.persister.get('_speed-change', None) if self.device.persister else None + speed_setting = next(filter(lambda s: s.name == 'pointer_speed', self.device.settings), None) + if newSpeed is not None: + if speed_setting: + speed_setting.write(newSpeed) + else: + _log.error('cannot save sensitivity setting on %s', self.device) + from solaar.ui import status_changed as _status_changed + _status_changed(self.device, refresh=True) # update main window + if self.device.persister: + self.device.persister['_speed-change'] = currentSpeed + + class validator_class(_ChoicesV): + @classmethod + def build(cls, setting_class, device): + key_index = device.keys.index(_special_keys.CONTROL.DPI_Change) + key = device.keys[key_index] if key_index is not None else None + if key is not None and 'divertable' in key.flags: + keys = [setting_class.choices_extra, key.key] + return cls(choices=_NamedInts.list(keys), byte_count=2) + + +class DpiSliding(_Setting): """ Implements the ability to smoothly modify the DPI by sliding a mouse horizontally while holding the DPI button. Abides by the following FSM: When the button is pressed, go into `pressed` state and begin accumulating displacement. @@ -375,10 +540,18 @@ def _feature_dpi_sliding(): If the state is `pressed` and the mouse moves enough to switch DPI go into the `moved` state. When the button is released in this state the DPI is set according to the total displacement. """ - class _DPISlidingRW(_ActionSettingRW): + name = 'dpi-sliding' + label = _('DPI Sliding Adjustment') + description = _('Adjust the DPI by sliding the mouse horizontally while holding the button down.') + choices_universe = _special_keys.CONTROL + choices_extra = _NamedInt(0, _('Off')) + feature = _F.REPROG_CONTROLS_V4 + rw_options = {'name': 'dpi sliding'} + + class rw_class(_ActionSettingRW): def activate_action(self): self.key.set_rawXY_reporting(True) - self.dpiSetting = next(filter(lambda s: s.name == _DPI[0], self.device.settings), None) + self.dpiSetting = next(filter(lambda s: s.name == 'dpi', self.device.settings), None) self.dpiChoices = list(self.dpiSetting.choices) self.otherDpiIdx = self.device.persister.get('_dpi-sliding', -1) if self.device.persister else -1 if not isinstance(self.otherDpiIdx, int) or self.otherDpiIdx < 0 or self.otherDpiIdx >= len(self.dpiChoices): @@ -437,97 +610,61 @@ def _feature_dpi_sliding(): self.movingDpiIdx = newMovingDpiIdx self.displayNewDpi(newMovingDpiIdx) - DPISlidingKeys = [_special_keys.CONTROL.DPI_Switch] + class validator_class(_ChoicesV): + sliding_keys = [_special_keys.CONTROL.DPI_Switch] - def callback(device): - # need _F.REPROG_CONTROLS_V4 feature and a DPI Switch that can send raw XY - # and _F.ADJUSTABLE_DPI so that the DPI can be adjusted - if device.kind == _DK.mouse and _F.ADJUSTABLE_DPI in device.features: - keys = [] - for key in DPISlidingKeys: - key_index = device.keys.index(key) - dkey = device.keys[key_index] if key_index is not None else None - if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags: - keys.append(dkey.key) - if not keys: # none of the keys designed for this, so look for any key with correct flags - for key in device.keys: - if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags: - keys.append(key.key) - if keys: - keys.insert(0, _NamedInt(0, _('Off'))) - return _ChoicesV(_NamedInts.list(keys), byte_count=2) - - rw = _DPISlidingRW('dpi sliding', _DIVERT_KEYS[0]) - return _Setting(_DPI_SLIDING, rw, callback=callback, device_kind=(_DK.mouse, )) + @classmethod + def build(cls, setting_class, device): + # need _F.REPROG_CONTROLS_V4 feature and a DPI Switch that can send raw XY + # and _F.ADJUSTABLE_DPI so that the DPI can be adjusted + if device.kind == _DK.mouse and _F.ADJUSTABLE_DPI in device.features: + keys = [] + for key in cls.sliding_keys: + key_index = device.keys.index(key) + dkey = device.keys[key_index] if key_index is not None else None + if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags: + keys.append(dkey.key) + if not keys: # none of the keys designed for this, so look for any key with correct flags + for key in device.keys: + if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags: + keys.append(key.key) + if keys: + keys.insert(0, setting_class.choices_extra) + return cls(choices=_NamedInts.list(keys), byte_count=2) -def _feature_speed_change(): - """Implements the ability to switch Sensitivity by clicking on the DPI_Change button.""" - class _SpeedChangeRW(_ActionSettingRW): - def press_action(self): # switch sensitivity - currentSpeed = self.device.persister.get('pointer_speed', None) if self.device.persister else None - newSpeed = self.device.persister.get('_speed-change', None) if self.device.persister else None - speed_setting = next(filter(lambda s: s.name == _POINTER_SPEED[0], self.device.settings), None) - if newSpeed is not None: - if speed_setting: - speed_setting.write(newSpeed) - else: - _log.error('cannot save sensitivity setting on %s', self.device) - from solaar.ui import status_changed as _status_changed - _status_changed(self.device, refresh=True) # update main window - if self.device.persister: - self.device.persister['_speed-change'] = currentSpeed +class DisableKeyboardKeys(_BitFieldSetting): + name = 'disable-keyboard-keys' + label = _('Disable keys') + description = _('Disable specific keyboard keys.') + feature = _F.KEYBOARD_DISABLE_KEYS + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + _labels = {k: (None, _('Disables the %s key.') % k) for k in _DKEY} + choices_universe = _DKEY - def callback(device): - key_index = device.keys.index(_special_keys.CONTROL.DPI_Change) - key = device.keys[key_index] if key_index is not None else None - if key is not None and 'divertable' in key.flags: - keys = [_NamedInt(0, _('Off')), key.key] - return _ChoicesV(_NamedInts.list(keys), byte_count=2) - - rw = _SpeedChangeRW('speed change', _DIVERT_KEYS[0]) - return _Setting(_SPEED_CHANGE, rw, callback=callback, device_kind=(_DK.mouse, _DK.trackball)) + class validator_class(_BitFieldV): + @classmethod + def build(cls, setting_class, device): + mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS, 0x00)[0] + options = [_DKEY[1 << i] for i in range(8) if mask & (1 << i)] + return cls(options) if options else None -def _feature_adjustable_dpi_callback(device): - # [1] getSensorDpiList(sensorIdx) - reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) - # Should not happen, but might happen when the user unplugs device while the - # query is being executed. TODO retry logic? - assert reply, 'Oops, DPI list cannot be retrieved!' - dpi_list = [] - step = None - for val in _unpack('!7H', reply[1:1 + 14]): - if val == 0: - break - if val >> 13 == 0b111: - assert step is None and len(dpi_list) == 1, \ - 'Invalid DPI list item: %r' % val - step = val & 0x1fff - else: - dpi_list.append(val) - if step: - assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list - dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) - return _ChoicesV(_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None - - -def _feature_adjustable_dpi(): - """Pointer Speed feature""" - # Assume sensorIdx 0 (there is only one sensor) - # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB - # [3] setSensorDpi(sensorIdx, dpi) - rw = _FeatureRW(_F.ADJUSTABLE_DPI, read_fnid=0x20, write_fnid=0x30) - return _Setting(_DPI, rw, callback=_feature_adjustable_dpi_callback, device_kind=(_DK.mouse, _DK.trackball)) - - -def _feature_mouse_gesture(): +class MouseGesture(_Setting): """Implements the ability to send mouse gestures by sliding a mouse horizontally or vertically while holding the App Switch button.""" - class MouseGestureRW(_ActionSettingRW): + name = 'mouse-gestures' + label = _('Mouse Gestures') + description = _('Send a gesture by sliding the mouse while holding the button down.') + feature = _F.REPROG_CONTROLS_V4 + rw_options = {'name': 'mouse gesture'} + choices_universe = _special_keys.CONTROL + choices_extra = _NamedInt(0, _('Off')) + + class rw_class(_ActionSettingRW): def activate_action(self): self.key.set_rawXY_reporting(True) - self.dpiSetting = next(filter(lambda s: s.name == _DPI[0], self.device.settings), None) + self.dpiSetting = next(filter(lambda s: s.name == 'dpi', self.device.settings), None) self.fsmState = 'idle' self.initialize_data() @@ -594,387 +731,311 @@ def _feature_mouse_gesture(): if _log.isEnabledFor(_DEBUG): _log.debug('mouse gesture move event %d %d %s', x, y, self.data) - MouseGestureKeys = [ - _special_keys.CONTROL.Mouse_Gesture_Button, - _special_keys.CONTROL.MultiPlatform_Gesture_Button, - ] + class validator_class(_ChoicesV): + MouseGestureKeys = [_special_keys.CONTROL.Mouse_Gesture_Button, _special_keys.CONTROL.MultiPlatform_Gesture_Button] - def callback(device): - if device.kind == _DK.mouse: - keys = [] - for key in MouseGestureKeys: - key_index = device.keys.index(key) - dkey = device.keys[key_index] if key_index is not None else None - if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags: - keys.append(dkey.key) - if not keys: # none of the keys designed for this, so look for any key with correct flags - for key in device.keys: - if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags: - keys.append(key.key) - if keys: - keys.insert(0, _NamedInt(0, _('Off'))) - return _ChoicesV(_NamedInts.list(keys), byte_count=2) - - rw = MouseGestureRW('mouse gesture', _DIVERT_KEYS[0]) - return _Setting(_MOUSE_GESTURES, rw, callback=callback, device_kind=(_DK.mouse, )) + @classmethod + def build(cls, setting_class, device): + if device.kind == _DK.mouse: + keys = [] + for key in cls.MouseGestureKeys: + key_index = device.keys.index(key) + dkey = device.keys[key_index] if key_index is not None else None + if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags: + keys.append(dkey.key) + if not keys: # none of the keys designed for this, so look for any key with correct flags + for key in device.keys: + if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags: + keys.append(key.key) + if keys: + keys.insert(0, setting_class.choices_extra) + return cls(choices=_NamedInts.list(keys), byte_count=2) -# Implemented based on code in libratrag -def _feature_report_rate_callback(device): - if device.wpid == '408E': - return None # host mode borks the function keys on the G915 TKL keyboard - reply = device.feature_request(_F.REPORT_RATE, 0x00) - assert reply, 'Oops, report rate choices cannot be retrieved!' - rate_list = [] - rate_flags = _bytes2int(reply[0:1]) - for i in range(0, 8): - if (rate_flags >> i) & 0x01: - rate_list.append(i + 1) - return _ChoicesV(_NamedInts.list(rate_list), byte_count=1) if rate_list else None +class Multiplatform(_Setting): + name = 'multiplatform' + label = _('Set OS') + description = _('Change keys to match OS.') + feature = _F.MULTIPLATFORM + rw_options = {'read_fnid': 0x00, 'write_fnid': 0x30} + # multiplatform OS bits + OSS = [('Linux', 0x0400), ('MacOS', 0x2000), ('Windows', 0x0100), ('iOS', 0x4000), ('Android', 0x1000), ('WebOS', 0x8000), + ('Chrome', 0x0800), ('WinEmb', 0x0200), ('Tizen', 0x0001)] + + # the problem here is how to construct the right values for the rules Set GUI, + # as, for example, the integer value for 'Windows' can be different on different devices + + class validator_class(_ChoicesV): + @classmethod + def build(cls, setting_class, device): + def _str_os_versions(low, high): + def _str_os_version(version): + if version == 0: + return '' + elif version & 0xFF: + return str(version >> 8) + '.' + str(version & 0xFF) + else: + return str(version >> 8) + + return '' if low == 0 and high == 0 else ' ' + _str_os_version(low) + '-' + _str_os_version(high) + + infos = device.feature_request(_F.MULTIPLATFORM) + assert infos, 'Oops, multiplatform count cannot be retrieved!' + flags, _ignore, num_descriptors = _unpack('!BBB', infos[:3]) + if not (flags & 0x02): # can't set platform so don't create setting + return [] + descriptors = [] + for index in range(0, num_descriptors): + descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index) + platform, _ignore, os_flags, low, high = _unpack('!BBHHH', descriptor[:8]) + descriptors.append((platform, os_flags, low, high)) + choices = _NamedInts() + for os_name, os_bit in setting_class.OSS: + for platform, os_flags, low, high in descriptors: + os = os_name + _str_os_versions(low, high) + if os_bit & os_flags and platform not in choices and os not in choices: + choices[platform] = os + return cls(choices=choices, read_skip_byte_count=6, write_prefix_bytes=b'\xff') if choices else None -class FeatureReportRateRW(_FeatureRW): - def write(self, device, data_bytes): - # Host mode is required for report rate to be adjustable - if _hidpp20.get_onboard_mode(device) != _hidpp20.ONBOARD_MODES.MODE_HOST: - _hidpp20.set_onboard_mode(device, _hidpp20.ONBOARD_MODES.MODE_HOST) - return super().write(device, data_bytes) +class DualPlatform(_Setting): + name = 'multiplatform' + label = _('Set OS') + description = _('Change keys to match OS.') + choices_universe = _NamedInts() + choices_universe[0x00] = 'iOS, MacOS' + choices_universe[0x01] = 'Android, Windows' + feature = _F.DUALPLATFORM + rw_options = {'read_fnid': 0x00, 'write_fnid': 0x20} + validator_class = _ChoicesV + validator_options = {'choices': choices_universe} -def _feature_report_rate(): - """Report Rate feature""" - rw = FeatureReportRateRW(_F.REPORT_RATE, read_fnid=0x10, write_fnid=0x20) - return _Setting(_REPORT_RATE, rw, callback=_feature_report_rate_callback, device_kind=(_DK.mouse, )) +class ChangeHost(_Setting): + name = 'change-host' + label = _('Change Host') + description = _('Switch connection to a different host') + persist = False # persisting this setting is harmful + feature = _F.CHANGE_HOST + rw_options = {'read_fnid': 0x00, 'write_fnid': 0x10, 'no_reply': True} + choices_universe = _NamedInts(**{'Host ' + str(i + 1): i for i in range(3)}) + + class validator_class(_ChoicesV): + @classmethod + def build(cls, setting_class, device): + infos = device.feature_request(_F.CHANGE_HOST) + assert infos, 'Oops, host count cannot be retrieved!' + numHosts, currentHost = _unpack('!BB', infos[:2]) + hostNames = _hidpp20.get_host_names(device) + hostNames = hostNames if hostNames is not None else {} + if currentHost not in hostNames or hostNames[currentHost][1] == '': + import socket # find name of current host and use it + hostNames[currentHost] = (True, socket.gethostname().partition('.')[0]) + choices = _NamedInts() + for host in range(0, numHosts): + paired, hostName = hostNames.get(host, (True, '')) + choices[host] = str(host + 1) + ':' + hostName if hostName else str(host + 1) + return cls(choices=choices, read_skip_byte_count=1) if choices and len(choices) > 1 else None -def _feature_pointer_speed(): - """Pointer Speed feature""" - # min and max values taken from usb traces of Win software - validator = _RangeV(0x002e, 0x01ff, 2) - rw = _FeatureRW(_F.POINTER_SPEED) - return _Setting(_POINTER_SPEED, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) +_GESTURE2_GESTURES_LABELS = { + _GG['Tap1Finger']: (_('Single tap'), _('Performs a left click.')), + _GG['Tap2Finger']: (_('Single tap with two fingers'), _('Performs a right click.')), + _GG['Tap3Finger']: (_('Single tap with three fingers'), None), + _GG['Click1Finger']: (None, None), + _GG['Click2Finger']: (None, None), + _GG['Click3Finger']: (None, None), + _GG['DoubleTap1Finger']: (_('Double tap'), _('Performs a double click.')), + _GG['DoubleTap2Finger']: (_('Double tap with two fingers'), None), + _GG['DoubleTap3Finger']: (_('Double tap with three fingers'), None), + _GG['Track1Finger']: (None, None), + _GG['TrackingAcceleration']: (None, None), + _GG['TapDrag1Finger']: (_('Tap and drag'), _('Drags items by dragging the finger after double tapping.')), + _GG['TapDrag2Finger']: + (_('Tap and drag with two fingers'), _('Drags items by dragging the fingers after double tapping.')), + _GG['Drag3Finger']: (_('Tap and drag with three fingers'), None), + _GG['TapGestures']: (None, None), + _GG['FnClickGestureSuppression']: + (_('Suppress tap and edge gestures'), _('Disables tap and edge gestures (equivalent to pressing Fn+LeftClick).')), + _GG['Scroll1Finger']: (_('Scroll with one finger'), _('Scrolls.')), + _GG['Scroll2Finger']: (_('Scroll with two fingers'), _('Scrolls.')), + _GG['Scroll2FingerHoriz']: (_('Scroll horizontally with two fingers'), _('Scrolls horizontally.')), + _GG['Scroll2FingerVert']: (_('Scroll vertically with two fingers'), _('Scrolls vertically.')), + _GG['Scroll2FingerStateless']: (_('Scroll with two fingers'), _('Scrolls.')), + _GG['NaturalScrolling']: (_('Natural scrolling'), _('Inverts the scrolling direction.')), + _GG['Thumbwheel']: (_('Thumbwheel'), _('Enables the thumbwheel.')), + _GG['VScrollInertia']: (None, None), + _GG['VScrollBallistics']: (None, None), + _GG['Swipe2FingerHoriz']: (None, None), + _GG['Swipe3FingerHoriz']: (None, None), + _GG['Swipe4FingerHoriz']: (None, None), + _GG['Swipe3FingerVert']: (None, None), + _GG['Swipe4FingerVert']: (None, None), + _GG['LeftEdgeSwipe1Finger']: (None, None), + _GG['RightEdgeSwipe1Finger']: (None, None), + _GG['BottomEdgeSwipe1Finger']: (None, None), + _GG['TopEdgeSwipe1Finger']: (_('Swipe from the top edge'), None), + _GG['LeftEdgeSwipe1Finger2']: (_('Swipe from the left edge'), None), + _GG['RightEdgeSwipe1Finger2']: (_('Swipe from the right edge'), None), + _GG['BottomEdgeSwipe1Finger2']: (_('Swipe from the bottom edge'), None), + _GG['TopEdgeSwipe1Finger2']: (_('Swipe from the top edge'), None), + _GG['LeftEdgeSwipe2Finger']: (_('Swipe two fingers from the left edge'), None), + _GG['RightEdgeSwipe2Finger']: (_('Swipe two fingers from the right edge'), None), + _GG['BottomEdgeSwipe2Finger']: (_('Swipe two fingers from the bottom edge'), None), + _GG['TopEdgeSwipe2Finger']: (_('Swipe two fingers from the top edge'), None), + _GG['Zoom2Finger']: (_('Zoom with two fingers.'), _('Pinch to zoom out; spread to zoom in.')), + _GG['Zoom2FingerPinch']: (_('Pinch to zoom out.'), _('Pinch to zoom out.')), + _GG['Zoom2FingerSpread']: (_('Spread to zoom in.'), _('Spread to zoom in.')), + _GG['Zoom3Finger']: (_('Zoom with three fingers.'), None), + _GG['Zoom2FingerStateless']: (_('Zoom with two fingers'), _('Pinch to zoom out; spread to zoom in.')), + _GG['TwoFingersPresent']: (None, None), + _GG['Rotate2Finger']: (None, None), + _GG['Finger1']: (None, None), + _GG['Finger2']: (None, None), + _GG['Finger3']: (None, None), + _GG['Finger4']: (None, None), + _GG['Finger5']: (None, None), + _GG['Finger6']: (None, None), + _GG['Finger7']: (None, None), + _GG['Finger8']: (None, None), + _GG['Finger9']: (None, None), + _GG['Finger10']: (None, None), + _GG['DeviceSpecificRawData']: (None, None), +} + +_GESTURE2_PARAMS_LABELS = { + _GP['ExtraCapabilities']: (None, None), # not supported + _GP['PixelZone']: (_('Pixel zone'), None), # TO DO: replace None with a short description + _GP['RatioZone']: (_('Ratio zone'), None), # TO DO: replace None with a short description + _GP['ScaleFactor']: (_('Scale factor'), _('Sets the cursor speed.')), +} + +_GESTURE2_PARAMS_LABELS_SUB = { + 'left': (_('Left'), _('Left-most coordinate.')), + 'top': (_('top'), _('Top-most coordinate.')), + 'width': (_('width'), _('Width.')), + 'height': (_('height'), _('Height.')), + 'scale': (_('Scale'), _('Cursor speed.')), +} -# the keys for the choice map are Logitech controls (from special_keys) -# each choice value is a NamedInt with the string from a task (to be shown to the user) -# and the integer being the control number for that task (to be written to the device) -# Solaar only remaps keys (controlled by key gmask and group), not other key reprogramming -class ReprogrammableKeysRW: - def __init__(self): - self.feature = _F.REPROG_CONTROLS_V4 - self.kind = _FeatureRW.kind +class Gesture2Gestures(_BitFieldOMSetting): + name = 'gesture2-gestures' + label = _('Gestures') + description = _('Tweak the mouse/touchpad behaviour.') + feature = _F.GESTURE_2 + rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} + choices_universe = _hidpp20.GESTURE + _labels = _GESTURE2_GESTURES_LABELS - def read(self, device, key): - key_index = device.keys.index(key) - key_struct = device.keys[key_index] - return b'\x00\x00' + _int2bytes(int(key_struct.mapped_to), 2) - - def write(self, device, key, data_bytes): - key_index = device.keys.index(key) - key_struct = device.keys[key_index] - key_struct.remap(_special_keys.CONTROL[_bytes2int(data_bytes)]) - return True + 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 _feature_reprogrammable_keys_callback(device): - choices = {} - for k in device.keys: - tgts = k.remappable_to - if len(tgts) > 1: - choices[k.key] = tgts - if not choices: - return None - return _ChoicesMapV(choices, key_byte_count=2, byte_count=2, extra_default=0) - - -def _feature_reprogrammable_keys(): - rw = ReprogrammableKeysRW() - return _Settings(_REPROGRAMMABLE_KEYS, rw, callback=_feature_reprogrammable_keys_callback, device_kind=(_DK.keyboard, )) - - -class DivertKeysRW: - def __init__(self): - self.feature = _F.REPROG_CONTROLS_V4 - self.kind = _FeatureRW.kind - - def read(self, device, key): - key_index = device.keys.index(key) - key_struct = device.keys[key_index] - return b'\x00\x00\x01' if 'diverted' in key_struct.mapping_flags else b'\x00\x00\x00' - - def write(self, device, key, data_bytes): - key_index = device.keys.index(key) - key_struct = device.keys[key_index] - key_struct.set_diverted(data_bytes == b'\x01') - return True - - -def _feature_divert_keys_callback(device): - choices = {} - for k in device.keys: - if 'divertable' in k.flags and 'virtual' not in k.flags: - choices[k.key] = [_NamedInt(0x00, _('Regular')), _NamedInt(0x01, _('Diverted'))] - if not choices: - return None - return _ChoicesMapV(choices, key_byte_count=2, byte_count=1, mask=0x01) - - -def _feature_divert_keys(): - rw = DivertKeysRW() - return _Settings(_DIVERT_KEYS, rw, callback=_feature_divert_keys_callback, device_kind=(_DK.keyboard, )) - - -def _feature_disable_keyboard_keys_callback(device): - mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS)[0] - options = [_special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)] - return _BitFieldV(options) if options else None - - -def _feature_disable_keyboard_keys(): - rw = _FeatureRW(_F.KEYBOARD_DISABLE_KEYS, read_fnid=0x10, write_fnid=0x20) - s = _BitFieldSetting(_DISABLE_KEYS, rw, callback=_feature_disable_keyboard_keys_callback, device_kind=(_DK.keyboard, )) - s._labels = {k: (None, _DISABLE_KEYS_LABEL_SUB % k) for k in _DKEY} - return s - - -# muultiplatform OS bits -OSS = [('Linux', 0x0400), ('MacOS', 0x2000), ('Windows', 0x0100), ('iOS', 0x4000), ('Android', 0x1000), ('WebOS', 0x8000), - ('Chrome', 0x0800), ('WinEmb', 0x0200), ('Tizen', 0x0001)] - - -def _feature_multiplatform_callback(device): - def _str_os_versions(low, high): - def _str_os_version(version): - if version == 0: - return '' - elif version & 0xFF: - return str(version >> 8) + '.' + str(version & 0xFF) - else: - return str(version >> 8) - - return '' if low == 0 and high == 0 else ' ' + _str_os_version(low) + '-' + _str_os_version(high) - - infos = device.feature_request(_F.MULTIPLATFORM) - assert infos, 'Oops, multiplatform count cannot be retrieved!' - flags, _ignore, num_descriptors = _unpack('!BBB', infos[:3]) - if not (flags & 0x02): # can't set platform so don't create setting - return [] - descriptors = [] - for index in range(0, num_descriptors): - descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index) - platform, _ignore, os_flags, low, high = _unpack('!BBHHH', descriptor[:8]) - descriptors.append((platform, os_flags, low, high)) - choices = _NamedInts() - for os_name, os_bit in OSS: - for platform, os_flags, low, high in descriptors: - os = os_name + _str_os_versions(low, high) - if os_bit & os_flags and platform not in choices and os not in choices: - choices[platform] = os - return _ChoicesV(choices, read_skip_byte_count=6, write_prefix_bytes=b'\xff') if choices else None - - -def _feature_multiplatform(): - rw = _FeatureRW(_F.MULTIPLATFORM, read_fnid=0x00, write_fnid=0x30) - return _Setting(_PLATFORM, rw, callback=_feature_multiplatform_callback) - - -PLATFORMS = _NamedInts() -PLATFORMS[0x00] = 'iOS, MacOS' -PLATFORMS[0x01] = 'Android, Windows' - - -def _feature_dualplatform(): - validator = _ChoicesV(PLATFORMS) - rw = _FeatureRW(_F.DUALPLATFORM, read_fnid=0x00, write_fnid=0x20) - return _Setting(_PLATFORM, rw, validator) - - -def _feature_change_host_callback(device): - infos = device.feature_request(_F.CHANGE_HOST) - assert infos, 'Oops, host count cannot be retrieved!' - numHosts, currentHost = _unpack('!BB', infos[:2]) - hostNames = _hidpp20.get_host_names(device) - hostNames = hostNames if hostNames is not None else {} - if currentHost not in hostNames or hostNames[currentHost][1] == '': - import socket # find name of current host and use it - hostNames[currentHost] = (True, socket.gethostname().partition('.')[0]) - choices = _NamedInts() - for host in range(0, numHosts): - paired, hostName = hostNames.get(host, (True, '')) - choices[host] = str(host + 1) + ':' + hostName if hostName else str(host + 1) - return _ChoicesV(choices, read_skip_byte_count=1) if choices and len(choices) > 1 else None - - -def _feature_change_host(): - rw = _FeatureRW(_F.CHANGE_HOST, read_fnid=0x00, write_fnid=0x10, no_reply=True) - return _Setting(_CHANGE_HOST, rw, callback=_feature_change_host_callback, persist=False) - - -def _feature_thumb_mode(): - rw = _FeatureRW(_F.THUMB_WHEEL, read_fnid=0x10, write_fnid=0x20) - validator = _BooleanV(true_value=b'\x01\x00', false_value=b'\x00\x00', mask=b'\x01\x00') - return _Setting(_THUMB_SCROLL_MODE, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) - - -def _feature_thumb_invert(): - rw = _FeatureRW(_F.THUMB_WHEEL, read_fnid=0x10, write_fnid=0x20) - validator = _BooleanV(true_value=b'\x00\x01', false_value=b'\x00\x00', mask=b'\x00\x01') - return _Setting(_THUMB_SCROLL_INVERT, rw, validator, device_kind=(_DK.mouse, _DK.trackball)) - - -def _feature_gesture2_gestures_callback(device): - options = [g for g in _hidpp20.get_gestures(device).gestures.values() if g.can_be_enabled or g.default_enabled] - return _BitFieldOMV(options) if options else None - - -def _feature_gesture2_gestures(): - rw = _FeatureRW(_F.GESTURE_2, read_fnid=0x10, write_fnid=0x20) - s = _BitFieldOMSetting( - _GESTURE2_GESTURES, rw, callback=_feature_gesture2_gestures_callback, device_kind=(_DK.touchpad, _DK.mouse) - ) - s._labels = _GESTURE2_GESTURES_LABELS - return s - - -def _feature_gesture2_params_callback(device): - params = _hidpp20.get_gestures(device).params.values() - items = [i for i in params if i.sub_params] - if not items: - return None - sub_items = {i: i.sub_params for i in items} - return _MultipleRangeV(items, sub_items) - - -def _feature_gesture2_params(): - rw = _FeatureRW(_F.GESTURE_2, read_fnid=0x70, write_fnid=0x80) - s = _LongSettings(_GESTURE2_PARAMS, rw, callback=_feature_gesture2_params_callback, device_kind=(_DK.touchpad, _DK.mouse)) +class Gesture2Params(_LongSettings): + name = 'gesture2-params' + label = _('Gesture params') + description = _('Change numerical parameters of a mouse/touchpad.') + feature = _F.GESTURE_2 + rw_options = {'read_fnid': 0x70, 'write_fnid': 0x80} + choices_universe = None class ParamWrapper: def get(self, name, default=None): return _GESTURE2_PARAMS_LABELS.get(name.param, default) - s._labels = ParamWrapper() - s._labels_sub = _GESTURE2_PARAMS_LABELS_SUB - return s + _labels = ParamWrapper() + _labels_sub = _GESTURE2_PARAMS_LABELS_SUB + + class validator_class(_MultipleRangeV): + @classmethod + def build(cls, setting_class, device): + params = _hidpp20.get_gestures(device).params.values() + items = [i for i in params if i.sub_params] + if not items: + return None + sub_items = {i: i.sub_params for i in items} + return cls(items, sub_items) -def _feature_divert_crown(): - rw = _FeatureRW(_F.CROWN, read_fnid=0x10, write_fnid=0x20) - return _Setting(_DIVERT_CROWN, rw, _BooleanV(true_value=0x02, false_value=0x01, mask=0xff), device_kind=(_DK.keyboard, )) - - -def _feature_crown_smooth(): - rw = _FeatureRW(_F.CROWN, read_fnid=0x10, write_fnid=0x20) - validator = _BooleanV(true_value=0x01, false_value=0x02, read_skip_byte_count=1, write_prefix_bytes=b'\x00') - return _Setting(_CROWN_SMOOTH, rw, validator, device_kind=(_DK.keyboard, )) - - -def _feature_divert_gkeys(): - class _DivertGkeysRW(_FeatureRW): - def __init__(self, feature): - super().__init__(feature, write_fnid=0x20) - - def read(self, device): # no way to read, so just assume not diverted - return b'\x00' - - rw = _DivertGkeysRW(_F.GKEY) - return _Setting(_DIVERT_GKEYS, rw, _BooleanV(true_value=0x01, false_value=0x00, mask=0xff), device_kind=(_DK.keyboard, )) - - -# -# -# - - -def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None): - return (name[0], featureID, featureFn, registerFn, identifier if identifier else name[0].replace('-', '_')) - - -# The order of settings here is the order they are displayed in the GUI -_SETTINGS_TABLE = [ - _S(_HAND_DETECTION, registerFn=_register_hand_detection), - _S(_SMOOTH_SCROLL, registerFn=_register_smooth_scroll), - _S(_SIDE_SCROLL, registerFn=_register_side_scroll), - _S(_HI_RES_SCROLL, _F.HI_RES_SCROLLING, _feature_hi_res_scroll), - _S(_LOW_RES_SCROLL, _F.LOWRES_WHEEL, _feature_lowres_smooth_scroll), - _S(_HIRES_INV, _F.HIRES_WHEEL, _feature_hires_smooth_invert), - _S(_HIRES_RES, _F.HIRES_WHEEL, _feature_hires_smooth_resolution), # Recent Linux drivers depend on this not changing - _S(_SMART_SHIFT, _F.SMART_SHIFT, _feature_smart_shift), - _S(_SMART_SHIFT, _F.SMART_SHIFT_ENHANCED, _feature_smart_shift_enhanced, identifier='smart_shift_enhanced'), - _S(_THUMB_SCROLL_MODE, _F.THUMB_WHEEL, _feature_thumb_mode), - _S(_THUMB_SCROLL_INVERT, _F.THUMB_WHEEL, _feature_thumb_invert), - _S(_DPI, _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi), - _S(_DPI_SLIDING, _F.REPROG_CONTROLS_V4, _feature_dpi_sliding), - _S(_MOUSE_GESTURES, _F.REPROG_CONTROLS_V4, _feature_mouse_gesture), - _S(_POINTER_SPEED, _F.POINTER_SPEED, _feature_pointer_speed), - _S(_SPEED_CHANGE, _F.POINTER_SPEED, _feature_speed_change), - _S(_BACKLIGHT, _F.BACKLIGHT2, _feature_backlight2), - _S(_FN_SWAP, _F.FN_INVERSION, _feature_fn_swap, registerFn=_register_fn_swap), - _S(_FN_SWAP, _F.NEW_FN_INVERSION, _feature_new_fn_swap, identifier='new_fn_swap'), - _S(_FN_SWAP, _F.K375S_FN_INVERSION, _feature_k375s_fn_swap, identifier='k375s_fn_swap'), - _S(_REPROGRAMMABLE_KEYS, _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys), - _S(_DIVERT_KEYS, _F.REPROG_CONTROLS_V4, _feature_divert_keys), - _S(_DISABLE_KEYS, _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys), - _S(_REPORT_RATE, _F.REPORT_RATE, _feature_report_rate), - _S(_DIVERT_CROWN, _F.CROWN, _feature_divert_crown), - _S(_CROWN_SMOOTH, _F.CROWN, _feature_crown_smooth), - _S(_DIVERT_GKEYS, _F.GKEY, _feature_divert_gkeys), - _S(_PLATFORM, _F.MULTIPLATFORM, _feature_multiplatform), - _S(_PLATFORM, _F.DUALPLATFORM, _feature_dualplatform, identifier='dualplatform'), - _S(_CHANGE_HOST, _F.CHANGE_HOST, _feature_change_host), - _S(_GESTURE2_GESTURES, _F.GESTURE_2, _feature_gesture2_gestures), - _S(_GESTURE2_PARAMS, _F.GESTURE_2, _feature_gesture2_params), +SETTINGS = [ + RegisterHandDetection, # simple + RegisterSmoothScroll, # simple + RegisterSideScroll, # simple + RegisterDpi, + RegisterFnSwap, # working + HiResScroll, # simple + LowresSmoothScroll, # simple + HiresSmoothInvert, # working + HiresSmoothResolution, # working + SmartShift, # working + SmartShiftEnhanced, # simple + ThumbMode, # working + ThumbInvert, # working + ReportRate, # working + PointerSpeed, # simple + AdjustableDpi, # working + DpiSliding, # working + SpeedChange, + MouseGesture, # working + Backlight2, # working + FnSwap, # simple + NewFnSwap, # simple + K375sFnSwap, # working + ReprogrammableKeys, # working + DivertKeys, # working + DisableKeyboardKeys, # working + DivertCrown, # working + CrownSmooth, # working + DivertGkeys, + Multiplatform, # working + DualPlatform, # simple + ChangeHost, # working + Gesture2Gestures, # working + Gesture2Params, # working ] -_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE]) -RegisterSettings = _SETTINGS_LIST._make([s[3] for s in _SETTINGS_TABLE]) -FeatureSettings = _SETTINGS_LIST._make([s[2] for s in _SETTINGS_TABLE]) - -del _SETTINGS_LIST - # # # -def check_feature(device, name, featureId, featureFn): - """ - :param name: name for the setting - :param featureId: the numeric Feature ID for this setting implementation - :param featureFn: the function for this setting implementation - """ - if featureId not in device.features: +def check_feature(device, sclass): + if sclass.feature not in device.features: return try: - detected = featureFn()(device) + detected = sclass.build(device) if _log.isEnabledFor(_INFO): - _log.info('check_feature %s [%s] detected %s', name, featureId, detected) + _log.info('check_feature %s [%s] detected %s', sclass.name, sclass.feature, detected) return detected except Exception: from traceback import format_exc - _log.error('check_feature %s [%s] error %s', name, featureId, format_exc()) + _log.error('check_feature %s [%s] error %s', sclass.name, sclass.feature, format_exc()) # Returns True if device was queried to find features, False otherwise def check_feature_settings(device, already_known): - """Try to auto-detect device settings by the HID++ 2.0 features they have.""" + """Auto-detect device settings by the HID++ 2.0 features they have.""" if device.features is None or not device.online: return False if device.protocol and device.protocol < 2.0: return False absent = device.persister.get('_absent', []) if device.persister else [] newAbsent = [] - for name, featureId, featureFn, __, __ in _SETTINGS_TABLE: - if featureId and featureFn: - if name not in absent and not any(s.name == name for s in already_known): - setting = check_feature(device, name, featureId, featureFn) + for sclass in SETTINGS: + if sclass.feature: + if sclass.name not in absent and not any(s.name == sclass.name for s in already_known): + setting = check_feature(device, sclass) if setting: already_known.append(setting) - if name in newAbsent: - newAbsent.remove(name) + if sclass.name in newAbsent: + newAbsent.remove(sclass.name) else: - if not any(s.name == name for s in already_known) and name not in newAbsent: - newAbsent.append(name) + if not any(s.name == sclass.name for s in already_known) and sclass.name not in newAbsent: + newAbsent.append(sclass.name) if device.persister and newAbsent: absent.extend(newAbsent) device.persister['_absent'] = absent @@ -982,8 +1043,8 @@ def check_feature_settings(device, already_known): def check_feature_setting(device, setting_name): - for name, featureId, featureFn, __, __ in _SETTINGS_TABLE: - if name == setting_name and featureId and featureFn: - feature = check_feature(device, name, featureId, featureFn) + for setting in SETTINGS: + if setting.name == setting_name and getattr(setting.rw, 'feature', None): + feature = check_feature(device, setting.name, getattr(setting.rw, 'feature', None)) if feature: return feature