1265 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1265 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			Python
		
	
	
	
| # -*- python-mode -*-
 | |
| 
 | |
| ## Copyright (C) 2012-2013  Daniel Pavel
 | |
| ##
 | |
| ## This program is free software; you can redistribute it and/or modify
 | |
| ## it under the terms of the GNU General Public License as published by
 | |
| ## the Free Software Foundation; either version 2 of the License, or
 | |
| ## (at your option) any later version.
 | |
| ##
 | |
| ## This program is distributed in the hope that it will be useful,
 | |
| ## but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| ## GNU General Public License for more details.
 | |
| ##
 | |
| ## You should have received a copy of the GNU General Public License along
 | |
| ## with this program; if not, write to the Free Software Foundation, Inc.,
 | |
| ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | |
| 
 | |
| from logging import DEBUG as _DEBUG
 | |
| from logging import INFO as _INFO
 | |
| from logging import WARN as _WARN
 | |
| from logging import getLogger
 | |
| from struct import pack as _pack
 | |
| from struct import unpack as _unpack
 | |
| from time import time as _time
 | |
| 
 | |
| from . import hidpp10 as _hidpp10
 | |
| from . import hidpp20 as _hidpp20
 | |
| from . import special_keys as _special_keys
 | |
| from .common import NamedInt as _NamedInt
 | |
| from .common import NamedInts as _NamedInts
 | |
| from .common import bytes2int as _bytes2int
 | |
| from .common import int2bytes as _int2bytes
 | |
| from .i18n import _
 | |
| from .settings import ActionSettingRW as _ActionSettingRW
 | |
| 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 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 PackedRangeValidator as _PackedRangeV
 | |
| from .settings import RangeFieldSetting as _RangeFieldSetting
 | |
| from .settings import RangeValidator as _RangeV
 | |
| from .settings import RawXYProcessing as _RawXYProcessing
 | |
| from .settings import Setting as _Setting
 | |
| from .settings import Settings as _Settings
 | |
| from .special_keys import DISABLE as _DKEY
 | |
| 
 | |
| _log = getLogger(__name__)
 | |
| del getLogger
 | |
| 
 | |
| _DK = _hidpp10.DEVICE_KIND
 | |
| _R = _hidpp10.REGISTERS
 | |
| _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.
 | |
| #
 | |
| # 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.
 | |
| # The _RangeFieldSetting class is for settings that have multiple ranges packed into a byte string.
 | |
| # _LongSettings is for settings that have an even more complex structure.
 | |
| #
 | |
| # When settings are created a reader/writer and a validator are created.
 | |
| 
 | |
| # 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 - whether to wait for a reply (default false) (USE WITH EXTREME CAUTION).
 | |
| #
 | |
| # 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
 | |
| #   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:
 | |
| #   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.
 | |
| # 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.
 | |
| 
 | |
| 
 | |
| # 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
 | |
| 
 | |
| 
 | |
| 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'}
 | |
| 
 | |
| 
 | |
| 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}
 | |
| 
 | |
| 
 | |
| 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}
 | |
| 
 | |
| 
 | |
| # different devices have different sets of permissible dpis, so this should be subclassed
 | |
| class RegisterDpi(_Setting):
 | |
|     name = 'dpi-old'
 | |
|     label = _('Sensitivity (DPI - older mice)')
 | |
|     description = _('Mouse movement sensitivity')
 | |
|     register = _R.mouse_dpi
 | |
|     choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
 | |
|     validator_class = _ChoicesV
 | |
|     validator_options = {'choices': choices_universe}
 | |
| 
 | |
| 
 | |
| class RegisterFnSwap(FnSwapVirtual):
 | |
|     register = _R.keyboard_fn_swap
 | |
|     validator_options = {'true_value': b'\x00\x01', 'mask': b'\x00\x01'}
 | |
| 
 | |
| 
 | |
| 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
 | |
| 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}
 | |
| 
 | |
| 
 | |
| class Backlight(_Setting):
 | |
|     name = 'backlight-qualitative'
 | |
|     label = _('Backlight')
 | |
|     description = _('Set illumination time for keyboard.')
 | |
|     feature = _F.BACKLIGHT
 | |
|     choices_universe = _NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180)
 | |
|     validator_class = _ChoicesV
 | |
|     validator_options = {'choices': choices_universe}
 | |
| 
 | |
| 
 | |
| class Backlight2(_Setting):
 | |
|     name = 'backlight'
 | |
|     label = _('Backlight')
 | |
|     description = _('Turn illumination on or off on keyboard.')
 | |
|     feature = _F.BACKLIGHT2
 | |
| 
 | |
| 
 | |
| class Backlight3(_Setting):
 | |
|     name = 'backlight-timed'
 | |
|     label = _('Backlight')
 | |
|     description = _('Set illumination time for keyboard.')
 | |
|     feature = _F.BACKLIGHT3
 | |
|     rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20, 'suffix': 0x09}
 | |
|     validator_class = _RangeV
 | |
|     min_value = 0
 | |
|     max_value = 1000
 | |
|     validator_options = {'byte_count': 2}
 | |
| 
 | |
| 
 | |
| class HiResScroll(_Setting):
 | |
|     name = 'hi-res-scroll'
 | |
|     label = _('Scroll Wheel High Resolution')
 | |
|     description = (
 | |
|         _('High-sensitivity mode for vertical scroll with the wheel.') + '\n' +
 | |
|         _('Set to ignore if scrolling is abnormally fast or slow')
 | |
|     )
 | |
|     feature = _F.HI_RES_SCROLLING
 | |
| 
 | |
| 
 | |
| class LowresMode(_Setting):
 | |
|     name = 'lowres-scroll-mode'
 | |
|     label = _('Scroll Wheel Diversion')
 | |
|     description = _(
 | |
|         'Make scroll wheel send LOWRES_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored).'
 | |
|     )
 | |
|     feature = _F.LOWRES_WHEEL
 | |
| 
 | |
| 
 | |
| 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}
 | |
| 
 | |
| 
 | |
| class HiresSmoothResolution(_Setting):
 | |
|     name = 'hires-smooth-resolution'
 | |
|     label = _('Scroll Wheel Resolution')
 | |
|     description = (
 | |
|         _('High-sensitivity mode for vertical scroll with the wheel.') + '\n' +
 | |
|         _('Set to ignore if scrolling is abnormally fast or slow')
 | |
|     )
 | |
|     feature = _F.HIRES_WHEEL
 | |
|     rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20}
 | |
|     validator_options = {'true_value': 0x02, 'mask': 0x02}
 | |
| 
 | |
| 
 | |
| class HiresMode(_Setting):
 | |
|     name = 'hires-scroll-mode'
 | |
|     label = _('Scroll Wheel Diversion')
 | |
|     description = _(
 | |
|         'Make scroll wheel send HIRES_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored).'
 | |
|     )
 | |
|     feature = _F.HIRES_WHEEL
 | |
|     rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20}
 | |
|     validator_options = {'true_value': 0x01, 'mask': 0x01}
 | |
| 
 | |
| 
 | |
| 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}
 | |
| 
 | |
| 
 | |
| class ThumbMode(_Setting):
 | |
|     name = 'thumb-scroll-mode'
 | |
|     label = _('Thumb Wheel Diversion')
 | |
|     description = _(
 | |
|         'Make thumb wheel send THUMB_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored).'
 | |
|     )
 | |
|     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'}
 | |
| 
 | |
| 
 | |
| 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'}
 | |
| 
 | |
| 
 | |
| class OnboardProfiles(_Setting):
 | |
|     name = 'onboard_profiles'
 | |
|     label = _('Onboard Profiles')
 | |
|     description = _('Enable onboard profiles, which often control report rate and keyboard lighting')
 | |
|     feature = _F.ONBOARD_PROFILES
 | |
|     rw_options = {'read_fnid': 0x20, 'write_fnid': 0x10}
 | |
|     choices_universe = _NamedInts(Enable=1, Disable=2)
 | |
|     validator_class = _ChoicesV
 | |
|     validator_options = {'choices': choices_universe}
 | |
| 
 | |
| 
 | |
| class ReportRate(_Setting):
 | |
|     name = 'report_rate'
 | |
|     label = _('Polling Rate (ms)')
 | |
|     description = (
 | |
|         _('Frequency of device polling, in milliseconds') + '\n' +
 | |
|         _('May need Onboard Profiles set to Disable to be effective.')
 | |
|     )
 | |
|     feature = _F.REPORT_RATE
 | |
|     rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20}
 | |
|     choices_universe = _NamedInts.range(1, 8)
 | |
| 
 | |
|     class _rw_class(_FeatureRW):  # no longer needed - set Onboard Profiles to disable
 | |
|         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).') + '\n' +
 | |
|         _('May also make M keys and MR key send HID++ notifications')
 | |
|     )
 | |
|     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 ScrollRatchet(_Setting):
 | |
|     name = 'scroll-ratchet'
 | |
|     label = _('Scroll Wheel Ratcheted')
 | |
|     description = _('Switch the mouse wheel between speed-controlled ratcheting and always freespin.')
 | |
|     feature = _F.SMART_SHIFT
 | |
|     choices_universe = _NamedInts(**{_('Freespinning'): 1, _('Ratcheted'): 2})
 | |
|     validator_class = _ChoicesV
 | |
|     validator_options = {'choices': choices_universe}
 | |
| 
 | |
| 
 | |
| class SmartShift(_Setting):
 | |
|     name = 'smart-shift'
 | |
|     label = _('Scroll Wheel Ratchet Speed')
 | |
|     description = _(
 | |
|         'Use the mouse wheel speed to switch between ratcheted and freespinning.\n'
 | |
|         'The mouse wheel is always ratcheted at 50.'
 | |
|     )
 | |
|     feature = _F.SMART_SHIFT
 | |
|     rw_options = {'read_fnid': 0x00, 'write_fnid': 0x10}
 | |
| 
 | |
|     class rw_class(_FeatureRW):
 | |
|         MIN_VALUE = 1
 | |
|         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 = 0  # 1 if threshold <= self.MIN_VALUE else 2
 | |
|             # Ratchet at maximum
 | |
|             if threshold >= self.MAX_VALUE:
 | |
|                 threshold = 255
 | |
|             data = _int2bytes(mode, count=1) + _int2bytes(max(0, 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.') + '  ' + _('Overridden by diversion.') + '\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 = {}
 | |
|             if device.keys:
 | |
|                 for k in device.keys:
 | |
|                     tgts = k.remappable_to
 | |
|                     if len(tgts) > 1:
 | |
|                         choices[k.key] = tgts
 | |
|             return cls(choices, key_byte_count=2, byte_count=2, extra_default=0) if choices else None
 | |
| 
 | |
| 
 | |
| class DpiSlidingXY(_RawXYProcessing):
 | |
|     def activate_action(self):
 | |
|         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):
 | |
|             self.otherDpiIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|         self.fsmState = 'idle'
 | |
|         self.dx = 0.
 | |
|         self.movingDpiIdx = None
 | |
| 
 | |
|     def setNewDpi(self, newDpiIdx):
 | |
|         newDpi = self.dpiChoices[newDpiIdx]
 | |
|         self.dpiSetting.write(newDpi)
 | |
|         from solaar.ui import status_changed as _status_changed
 | |
|         _status_changed(self.device, refresh=True)  # update main window
 | |
| 
 | |
|     def displayNewDpi(self, newDpiIdx):
 | |
|         from solaar.ui import notify as _notify  # import here to avoid circular import when running `solaar show`,
 | |
|         if _notify.available:
 | |
|             reason = 'DPI %d [min %d, max %d]' % (self.dpiChoices[newDpiIdx], self.dpiChoices[0], self.dpiChoices[-1])
 | |
|             # if there is a progress percentage then the reason isn't shown
 | |
|             # asPercentage = int(float(newDpiIdx) / float(len(self.dpiChoices) - 1) * 100.)
 | |
|             # _notify.show(self.device, reason=reason, progress=asPercentage)
 | |
|             _notify.show(self.device, reason=reason)
 | |
| 
 | |
|     def press_action(self, key):  # start tracking
 | |
|         self.starting = True
 | |
|         if self.fsmState == 'idle':
 | |
|             self.fsmState = 'pressed'
 | |
|             self.dx = 0.
 | |
|             # While in 'moved' state, the index into 'dpiChoices' of the currently selected DPI setting
 | |
|             self.movingDpiIdx = None
 | |
| 
 | |
|     def release_action(self):  # adjust DPI and stop tracking
 | |
|         if self.fsmState == 'pressed':  # Swap with other DPI
 | |
|             thisIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|             newDpiIdx, self.otherDpiIdx = self.otherDpiIdx, thisIdx
 | |
|             if self.device.persister:
 | |
|                 self.device.persister['_dpi-sliding'] = self.otherDpiIdx
 | |
|             self.setNewDpi(newDpiIdx)
 | |
|             self.displayNewDpi(newDpiIdx)
 | |
|         elif self.fsmState == 'moved':  # Set DPI according to displacement
 | |
|             self.setNewDpi(self.movingDpiIdx)
 | |
|         self.fsmState = 'idle'
 | |
| 
 | |
|     def move_action(self, dx, dy):
 | |
|         if self.device.features.get_feature_version(_F.REPROG_CONTROLS_V4) >= 5 and self.starting:
 | |
|             self.starting = False  # hack to ignore strange first movement report from MX Master 3S
 | |
|             return
 | |
|         currDpi = self.dpiSetting.read()
 | |
|         self.dx += float(dx) / float(currDpi) * 15.  # yields a more-or-less DPI-independent dx of about 5/cm
 | |
|         if self.fsmState == 'pressed':
 | |
|             if abs(self.dx) >= 1.:
 | |
|                 self.fsmState = 'moved'
 | |
|                 self.movingDpiIdx = self.dpiChoices.index(currDpi)
 | |
|         elif self.fsmState == 'moved':
 | |
|             currIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|             newMovingDpiIdx = min(max(currIdx + int(self.dx), 0), len(self.dpiChoices) - 1)
 | |
|             if newMovingDpiIdx != self.movingDpiIdx:
 | |
|                 self.movingDpiIdx = newMovingDpiIdx
 | |
|                 self.displayNewDpi(newMovingDpiIdx)
 | |
| 
 | |
| 
 | |
| class MouseGesturesXY(_RawXYProcessing):
 | |
|     def activate_action(self):
 | |
|         self.dpiSetting = next(filter(lambda s: s.name == 'dpi', self.device.settings), None)
 | |
|         self.fsmState = 'idle'
 | |
|         self.initialize_data()
 | |
| 
 | |
|     def initialize_data(self):
 | |
|         self.dx = 0.
 | |
|         self.dy = 0.
 | |
|         self.lastEv = None
 | |
|         self.data = []
 | |
| 
 | |
|     def press_action(self, key):
 | |
|         self.starting = True
 | |
|         if self.fsmState == 'idle':
 | |
|             self.fsmState = 'pressed'
 | |
|             self.initialize_data()
 | |
|             self.data = [key.key]
 | |
| 
 | |
|     def release_action(self):
 | |
|         if self.fsmState == 'pressed':
 | |
|             # emit mouse gesture notification
 | |
|             from .base import _HIDPP_Notification as _HIDPP_Notification
 | |
|             from .diversion import process_notification as _process_notification
 | |
|             self.push_mouse_event()
 | |
|             if _log.isEnabledFor(_INFO):
 | |
|                 _log.info('mouse gesture notification %s', self.data)
 | |
|             payload = _pack('!' + (len(self.data) * 'h'), *self.data)
 | |
|             notification = _HIDPP_Notification(0, 0, 0, 0, payload)
 | |
|             _process_notification(self.device, self.device.status, notification, _hidpp20.FEATURE.MOUSE_GESTURE)
 | |
|             self.fsmState = 'idle'
 | |
| 
 | |
|     def move_action(self, dx, dy):
 | |
|         if self.fsmState == 'pressed':
 | |
|             now = _time() * 1000  # _time_ns() / 1e6
 | |
|             if self.device.features.get_feature_version(_F.REPROG_CONTROLS_V4) >= 5 and self.starting:
 | |
|                 self.starting = False  # hack to ignore strange first movement report from MX Master 3S
 | |
|                 return
 | |
|             if self.lastEv is not None and now - self.lastEv > 200.:
 | |
|                 self.push_mouse_event()
 | |
|             dpi = self.dpiSetting.read() if self.dpiSetting else 1000
 | |
|             dx = float(dx) / float(dpi) * 15.  # This multiplier yields a more-or-less DPI-independent dx of about 5/cm
 | |
|             self.dx += dx
 | |
|             dy = float(dy) / float(dpi) * 15.  # This multiplier yields a more-or-less DPI-independent dx of about 5/cm
 | |
|             self.dy += dy
 | |
|             self.lastEv = now
 | |
| 
 | |
|     def key_action(self, key):
 | |
|         self.push_mouse_event()
 | |
|         self.data.append(1)
 | |
|         self.data.append(key)
 | |
|         self.lastEv = _time() * 1000  # _time_ns() / 1e6
 | |
|         if _log.isEnabledFor(_DEBUG):
 | |
|             _log.debug('mouse gesture key event %d %s', key, self.data)
 | |
| 
 | |
|     def push_mouse_event(self):
 | |
|         x = int(self.dx)
 | |
|         y = int(self.dy)
 | |
|         if x == 0 and y == 0:
 | |
|             return
 | |
|         self.data.append(0)
 | |
|         self.data.append(x)
 | |
|         self.data.append(y)
 | |
|         self.dx = 0.
 | |
|         self.dy = 0.
 | |
|         if _log.isEnabledFor(_DEBUG):
 | |
|             _log.debug('mouse gesture move event %d %d %s', x, y, self.data)
 | |
| 
 | |
| 
 | |
| class DivertKeys(_Settings):
 | |
|     name = 'divert-keys'
 | |
|     label = _('Key/Button Diversion')
 | |
|     description = _('Make the key or button send HID++ notifications (Diverted) or initiate Mouse Gestures or Sliding DPI')
 | |
|     feature = _F.REPROG_CONTROLS_V4
 | |
|     keys_universe = _special_keys.CONTROL
 | |
|     choices_universe = _NamedInts(**{_('Regular'): 0, _('Diverted'): 1, _('Mouse Gestures'): 2, _('Sliding DPI'): 3})
 | |
|     choices_gesture = _NamedInts(**{_('Regular'): 0, _('Diverted'): 1, _('Mouse Gestures'): 2})
 | |
|     choices_divert = _NamedInts(**{_('Regular'): 0, _('Diverted'): 1})
 | |
| 
 | |
|     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(_bytes2int(data_bytes) != 0)  # not regular
 | |
|             return True
 | |
| 
 | |
|     class validator_class(_ChoicesMapV):
 | |
|         def __init__(self, choices, key_byte_count=2, byte_count=1, mask=0x01):
 | |
|             super().__init__(choices, key_byte_count, byte_count, mask)
 | |
| 
 | |
|         def prepare_write(self, key, new_value):
 | |
|             if self.gestures and new_value != 2:  # mouse gestures
 | |
|                 self.gestures.stop(key)
 | |
|             if self.sliding and new_value != 3:  # sliding DPI
 | |
|                 self.sliding.stop(key)
 | |
|             if self.gestures and new_value == 2:  # mouse gestures
 | |
|                 self.gestures.start(key)
 | |
|             if self.sliding and new_value == 3:  # sliding DPI
 | |
|                 self.sliding.start(key)
 | |
|             return super().prepare_write(key, new_value)
 | |
| 
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             sliding = gestures = None
 | |
|             choices = {}
 | |
|             if device.keys:
 | |
|                 for k in device.keys:
 | |
|                     if 'divertable' in k.flags and 'virtual' not in k.flags:
 | |
|                         if 'raw XY' in k.flags:
 | |
|                             choices[k.key] = setting_class.choices_gesture
 | |
|                             if gestures is None:
 | |
|                                 gestures = MouseGesturesXY(device, name='MouseGestures')
 | |
|                             if _F.ADJUSTABLE_DPI in device.features:
 | |
|                                 choices[k.key] = setting_class.choices_universe
 | |
|                                 if sliding is None:
 | |
|                                     sliding = DpiSlidingXY(device, name='DpiSlding')
 | |
|                         else:
 | |
|                             choices[k.key] = setting_class.choices_divert
 | |
|             if not choices:
 | |
|                 return None
 | |
|             validator = cls(choices, key_byte_count=2, byte_count=1, mask=0x01)
 | |
|             validator.sliding = sliding
 | |
|             validator.gestures = gestures
 | |
|             return validator
 | |
| 
 | |
| 
 | |
| 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}
 | |
|     choices_universe = _NamedInts.range(200, 4000, str, 50)
 | |
| 
 | |
|     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
 | |
| 
 | |
|         def validate_read(self, reply_bytes):  # special validator to use default DPI if needed
 | |
|             reply_value = _bytes2int(reply_bytes[1:3])
 | |
|             if reply_value == 0:  # use default value instead
 | |
|                 reply_value = _bytes2int(reply_bytes[3:5])
 | |
|             valid_value = self.choices[reply_value]
 | |
|             assert valid_value is not None, '%s: failed to validate read value %02X' % (self.__class__.__name__, reply_value)
 | |
|             return valid_value
 | |
| 
 | |
| 
 | |
| 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 rw_class(_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 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
 | |
| 
 | |
|     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
 | |
| 
 | |
| 
 | |
| 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}
 | |
|     choices_universe = _NamedInts(**{'OS ' + str(i + 1): i for i in range(8)})
 | |
| 
 | |
|     # 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 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}
 | |
| 
 | |
| 
 | |
| 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
 | |
| 
 | |
| 
 | |
| _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.')),
 | |
|     'bottom': (_('Bottom'), _('Bottom coordinate.')),
 | |
|     'width': (_('Width'), _('Width.')),
 | |
|     'height': (_('Height'), _('Height.')),
 | |
|     'scale': (_('Scale'), _('Cursor speed.')),
 | |
| }
 | |
| 
 | |
| 
 | |
| 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}
 | |
|     validator_options = {'om_method': _hidpp20.Gesture.enable_offset_mask}
 | |
|     choices_universe = _hidpp20.GESTURE
 | |
|     _labels = _GESTURE2_GESTURES_LABELS
 | |
| 
 | |
|     class validator_class(_BitFieldOMV):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device, om_method=None):
 | |
|             options = [g for g in device.gestures.gestures.values() if g.can_be_enabled or g.default_enabled]
 | |
|             return cls(options, om_method=om_method) if options else None
 | |
| 
 | |
| 
 | |
| class Gesture2Divert(_BitFieldOMSetting):
 | |
|     name = 'gesture2-divert'
 | |
|     label = _('Gestures Diversion')
 | |
|     description = _('Divert mouse/touchpad gestures.')
 | |
|     feature = _F.GESTURE_2
 | |
|     rw_options = {'read_fnid': 0x30, 'write_fnid': 0x40}
 | |
|     validator_options = {'om_method': _hidpp20.Gesture.diversion_offset_mask}
 | |
|     choices_universe = _hidpp20.GESTURE
 | |
|     _labels = _GESTURE2_GESTURES_LABELS
 | |
| 
 | |
|     class validator_class(_BitFieldOMV):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device, om_method=None):
 | |
|             options = [g for g in device.gestures.gestures.values() if g.can_be_diverted]
 | |
|             return cls(options, om_method=om_method) if options else None
 | |
| 
 | |
| 
 | |
| class Gesture2Params(_LongSettings):
 | |
|     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 = _hidpp20.PARAM
 | |
|     sub_items_universe = _hidpp20.SUB_PARAM
 | |
|     # item (NamedInt) -> list/tuple of objects that have the following attributes
 | |
|     # .id (sub-item text), .length (in bytes), .minimum and .maximum
 | |
| 
 | |
|     _labels = _GESTURE2_PARAMS_LABELS
 | |
|     _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)
 | |
| 
 | |
| 
 | |
| class MKeyLEDs(_BitFieldSetting):
 | |
|     name = 'm-key-leds'
 | |
|     label = _('M-Key LEDs')
 | |
|     description = (
 | |
|         _('Control the M-Key LEDs.') + '\n' + _('May need Onboard Profiles set to Disable to be effective.') + '\n' +
 | |
|         _('May need G Keys diverted to be effective.')
 | |
|     )
 | |
|     feature = _F.MKEYS
 | |
|     choices_universe = _NamedInts()
 | |
|     for i in range(8):
 | |
|         choices_universe[1 << i] = 'M' + str(i + 1)
 | |
|     _labels = {k: (None, _('Lights up the %s key.') % k) for k in choices_universe}
 | |
| 
 | |
|     class rw_class(_FeatureRW):
 | |
|         def __init__(self, feature):
 | |
|             super().__init__(feature, write_fnid=0x10)
 | |
| 
 | |
|         def read(self, device):  # no way to read, so just assume off
 | |
|             return b'\x00'
 | |
| 
 | |
|     class validator_class(_BitFieldV):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             number = device.feature_request(setting_class.feature, 0x00)[0]
 | |
|             options = [setting_class.choices_universe[1 << i] for i in range(number)]
 | |
|             return cls(options) if options else None
 | |
| 
 | |
| 
 | |
| class MRKeyLED(_Setting):
 | |
|     name = 'mr-key-led'
 | |
|     label = _('MR-Key LED')
 | |
|     description = (
 | |
|         _('Control the MR-Key LED.') + '\n' + _('May need Onboard Profiles set to Disable to be effective.') + '\n' +
 | |
|         _('May need G Keys diverted to be effective.')
 | |
|     )
 | |
|     feature = _F.MR
 | |
| 
 | |
|     class rw_class(_FeatureRW):
 | |
|         def __init__(self, feature):
 | |
|             super().__init__(feature, write_fnid=0x00)
 | |
| 
 | |
|         def read(self, device):  # no way to read, so just assume off
 | |
|             return b'\x00'
 | |
| 
 | |
| 
 | |
| ## Only implemented for devices that can produce Key and Consumer Codes (e.g., Craft)
 | |
| ## and devices that can produce Key, Mouse, and Horizontal Scroll (e.g., M720)
 | |
| ## Only interested in current host, so use 0xFF for it
 | |
| class PersistentRemappableAction(_Settings):
 | |
|     name = 'persistent-remappable-keys'
 | |
|     label = _('Persistent Key/Button Mapping')
 | |
|     description = (
 | |
|         _('Permanently change the mapping for the key or button.') + '\n' +
 | |
|         _('Changing important keys or buttons (such as for the left mouse button) can result in an unusable system.')
 | |
|     )
 | |
|     persist = False  # This setting is persistent in the device so no need to persist it here
 | |
|     feature = _F.PERSISTENT_REMAPPABLE_ACTION
 | |
|     keys_universe = _special_keys.CONTROL
 | |
|     choices_universe = _special_keys.KEYS
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = _FeatureRW.kind
 | |
| 
 | |
|         def read(self, device, key):
 | |
|             ks = device.remap_keys[device.remap_keys.index(key)]
 | |
|             return b'\x00\x00' + ks.data_bytes
 | |
| 
 | |
|         def write(self, device, key, data_bytes):
 | |
|             ks = device.remap_keys[device.remap_keys.index(key)]
 | |
|             v = ks.remap(data_bytes)
 | |
|             return v
 | |
| 
 | |
|     class validator_class(_ChoicesMapV):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             remap_keys = device.remap_keys
 | |
|             if not remap_keys:
 | |
|                 return None
 | |
|             capabilities = device.remap_keys.capabilities
 | |
|             if capabilities & 0x0041 == 0x0041:  # Key and Consumer Codes
 | |
|                 keys = _special_keys.KEYS_KEYS_CONSUMER
 | |
|             elif capabilities & 0x0023 == 0x0023:  # Key, Mouse, and HScroll Codes
 | |
|                 keys = _special_keys.KEYS_KEYS_MOUSE_HSCROLL
 | |
|             else:
 | |
|                 if _log.isEnabledFor(_WARN):
 | |
|                     _log.warn('%s: unimplemented Persistent Remappable capability %s', device.name, hex(capabilities))
 | |
|                 return None
 | |
|             choices = {}
 | |
|             for k in remap_keys:
 | |
|                 if k is not None:
 | |
|                     key = _special_keys.CONTROL[k.key]
 | |
|                     choices[key] = keys  # TO RECOVER FROM BAD VALUES use _special_keys.KEYS
 | |
|             return cls(choices, key_byte_count=2, byte_count=4) if choices else None
 | |
| 
 | |
|         def validate_read(self, reply_bytes, key):
 | |
|             start = self._key_byte_count + self._read_skip_byte_count
 | |
|             end = start + self._byte_count
 | |
|             reply_value = _bytes2int(reply_bytes[start:end]) & self.mask
 | |
|             # Craft keyboard has a value that isn't valid so fudge these values
 | |
|             if reply_value not in self.choices[key]:
 | |
|                 if _log.isEnabledFor(_WARN):
 | |
|                     _log.warn('unusual persistent remappable action mapping %x: use Default', reply_value)
 | |
|                 reply_value = _special_keys.KEYS_Default
 | |
|             return reply_value
 | |
| 
 | |
| 
 | |
| class Sidetone(_Setting):
 | |
|     name = 'sidetone'
 | |
|     label = _('Sidetone')
 | |
|     description = _('Set sidetone level.')
 | |
|     feature = _F.SIDETONE
 | |
|     validator_class = _RangeV
 | |
|     min_value = 0
 | |
|     max_value = 100
 | |
| 
 | |
| 
 | |
| class Equalizer(_RangeFieldSetting):
 | |
|     name = 'equalizer'
 | |
|     label = _('Equalizer')
 | |
|     description = _('Set equalizer levels.')
 | |
|     feature = _F.EQUALIZER
 | |
|     rw_options = {'read_fnid': 0x20, 'write_fnid': 0x30, 'read_prefix': b'\x00'}
 | |
|     keys_universe = []
 | |
| 
 | |
|     class validator_class(_PackedRangeV):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             data = device.feature_request(_F.EQUALIZER, 0x00)
 | |
|             if not data:
 | |
|                 return None
 | |
|             count, dbRange, _x, dbMin, dbMax = _unpack('!BBBBB', data[:5])
 | |
|             if dbMin == 0:
 | |
|                 dbMin = -dbRange
 | |
|             if dbMax == 0:
 | |
|                 dbMax = dbRange
 | |
|             map = _NamedInts()
 | |
|             for g in range((count + 6) // 7):
 | |
|                 freqs = device.feature_request(_F.EQUALIZER, 0x10, g * 7)
 | |
|                 for b in range(7):
 | |
|                     if g * 7 + b >= count:
 | |
|                         break
 | |
|                     map[g * 7 + b] = str(int.from_bytes(freqs[2 * b + 1:2 * b + 3], 'big')) + _('Hz')
 | |
|             return cls(map, min_value=dbMin, max_value=dbMax, count=count, write_prefix_bytes=b'\x02')
 | |
| 
 | |
| 
 | |
| SETTINGS = [
 | |
|     RegisterHandDetection,  # simple
 | |
|     RegisterSmoothScroll,  # simple
 | |
|     RegisterSideScroll,  # simple
 | |
|     RegisterDpi,
 | |
|     RegisterFnSwap,  # working
 | |
|     HiResScroll,  # simple
 | |
|     LowresMode,  # simple
 | |
|     HiresSmoothInvert,  # working
 | |
|     HiresSmoothResolution,  # working
 | |
|     HiresMode,  # simple
 | |
|     ScrollRatchet,  # simple
 | |
|     SmartShift,  # working
 | |
|     SmartShiftEnhanced,  # simple
 | |
|     ThumbInvert,  # working
 | |
|     ThumbMode,  # working
 | |
|     OnboardProfiles,
 | |
|     ReportRate,  # working
 | |
|     PointerSpeed,  # simple
 | |
|     AdjustableDpi,  # working
 | |
|     SpeedChange,
 | |
|     #    Backlight,  # not working - disabled temporarily
 | |
|     Backlight2,  # working
 | |
|     Backlight3,
 | |
|     FnSwap,  # simple
 | |
|     NewFnSwap,  # simple
 | |
|     K375sFnSwap,  # working
 | |
|     ReprogrammableKeys,  # working
 | |
|     PersistentRemappableAction,
 | |
|     DivertKeys,  # working
 | |
|     DisableKeyboardKeys,  # working
 | |
|     CrownSmooth,  # working
 | |
|     DivertCrown,  # working
 | |
|     DivertGkeys,  # working
 | |
|     MKeyLEDs,  # working
 | |
|     MRKeyLED,  # working
 | |
|     Multiplatform,  # working
 | |
|     DualPlatform,  # simple
 | |
|     ChangeHost,  # working
 | |
|     Gesture2Gestures,  # working
 | |
|     Gesture2Divert,
 | |
|     Gesture2Params,  # working
 | |
|     Sidetone,
 | |
|     Equalizer,
 | |
| ]
 | |
| 
 | |
| #
 | |
| #
 | |
| #
 | |
| 
 | |
| 
 | |
| def check_feature(device, sclass):
 | |
|     if sclass.feature not in device.features:
 | |
|         return
 | |
|     try:
 | |
|         detected = sclass.build(device)
 | |
|         if _log.isEnabledFor(_DEBUG):
 | |
|             _log.debug('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', sclass.name, sclass.feature, format_exc())
 | |
| 
 | |
| 
 | |
| # Returns True if device was queried to find features, False otherwise
 | |
| def check_feature_settings(device, already_known):
 | |
|     """Auto-detect device settings by the HID++ 2.0 features they have."""
 | |
|     if not device.features 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 sclass in SETTINGS:
 | |
|         if sclass.feature:
 | |
|             known_present = device.persister and sclass.name in device.persister
 | |
|             if not any(s.name == sclass.name for s in already_known) and (known_present or sclass.name not in absent):
 | |
|                 setting = check_feature(device, sclass)
 | |
|                 if setting:
 | |
|                     already_known.append(setting)
 | |
|                     if sclass.name in newAbsent:
 | |
|                         newAbsent.remove(sclass.name)
 | |
|                 elif sclass.name not in newAbsent and sclass.name not in absent and sclass.name not in device.persister:
 | |
|                     newAbsent.append(sclass.name)
 | |
|     if device.persister and newAbsent:
 | |
|         absent.extend(newAbsent)
 | |
|         device.persister['_absent'] = absent
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def check_feature_setting(device, setting_name):
 | |
|     for sclass in SETTINGS:
 | |
|         if sclass.feature and sclass.name == setting_name and device.features:
 | |
|             setting = check_feature(device, sclass)
 | |
|             if setting:
 | |
|                 return setting
 |