Merge pull request #259 from javitonino/support-mx-master
Support mx master
This commit is contained in:
commit
28e43395ab
|
@ -89,7 +89,7 @@ Mice (Unifying):
|
||||||
| T620 Touch | 2.0 | yes | | |
|
| T620 Touch | 2.0 | yes | | |
|
||||||
| Performance MX | 1.0 | yes | R/W | smooth scrolling |
|
| Performance MX | 1.0 | yes | R/W | smooth scrolling |
|
||||||
| Anywhere MX | 1.0 | yes | R/W | smooth scrolling |
|
| Anywhere MX | 1.0 | yes | R/W | smooth scrolling |
|
||||||
| MX Master | 4.5 | yes | TODO | |
|
| MX Master | 4.5 | yes | R/W | smart shift |
|
||||||
| Cube | 2.0 | yes | | |
|
| Cube | 2.0 | yes | | |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,19 +24,20 @@ _log = getLogger(__name__)
|
||||||
del getLogger
|
del getLogger
|
||||||
|
|
||||||
from copy import copy as _copy
|
from copy import copy as _copy
|
||||||
|
import math
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
NamedInt as _NamedInt,
|
NamedInt as _NamedInt,
|
||||||
NamedInts as _NamedInts,
|
NamedInts as _NamedInts,
|
||||||
bytes2int as _bytes2int,
|
bytes2int as _bytes2int,
|
||||||
|
int2bytes as _int2bytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x12)
|
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04)
|
||||||
|
|
||||||
class Setting(object):
|
class Setting(object):
|
||||||
"""A setting descriptor.
|
"""A setting descriptor.
|
||||||
|
@ -81,6 +82,14 @@ class Setting(object):
|
||||||
|
|
||||||
return self._validator.choices if self._validator.kind & KIND.choice else None
|
return self._validator.choices if self._validator.kind & KIND.choice else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def range(self):
|
||||||
|
assert hasattr(self, '_value')
|
||||||
|
assert hasattr(self, '_device')
|
||||||
|
|
||||||
|
if self._validator.kind == KIND.range:
|
||||||
|
return (self._validator.min_value, self._validator.max_value)
|
||||||
|
|
||||||
def read(self, cached=True):
|
def read(self, cached=True):
|
||||||
assert hasattr(self, '_value')
|
assert hasattr(self, '_value')
|
||||||
assert hasattr(self, '_device')
|
assert hasattr(self, '_device')
|
||||||
|
@ -356,3 +365,36 @@ class ChoicesValidator(object):
|
||||||
raise ValueError("invalid choice %r" % new_value)
|
raise ValueError("invalid choice %r" % new_value)
|
||||||
assert isinstance(choice, _NamedInt)
|
assert isinstance(choice, _NamedInt)
|
||||||
return choice.bytes(self._bytes_count)
|
return choice.bytes(self._bytes_count)
|
||||||
|
|
||||||
|
class RangeValidator(object):
|
||||||
|
__slots__ = ('min_value', 'max_value', 'flag', '_bytes_count', 'needs_current_value')
|
||||||
|
|
||||||
|
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 bytes_count: the size of the derived byte sequence. If None, it
|
||||||
|
will be calculated from the range."""
|
||||||
|
def __init__(self, min_value, max_value, bytes_count=None):
|
||||||
|
assert max_value > min_value
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
self.needs_current_value = False
|
||||||
|
|
||||||
|
self._bytes_count = math.ceil(math.log(max_value + 1, 256))
|
||||||
|
if bytes_count:
|
||||||
|
assert self._bytes_count <= bytes_count
|
||||||
|
self._bytes_count = bytes_count
|
||||||
|
assert self._bytes_count < 8
|
||||||
|
|
||||||
|
def validate_read(self, reply_bytes):
|
||||||
|
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
|
||||||
|
assert reply_value >= self.min_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
|
||||||
|
assert reply_value <= self.max_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
|
||||||
|
return reply_value
|
||||||
|
|
||||||
|
def prepare_write(self, new_value, current_value=None):
|
||||||
|
if new_value < self.min_value or new_value > self.max_value:
|
||||||
|
raise ValueError("invalid choice %r" % new_value)
|
||||||
|
return _int2bytes(new_value, self._bytes_count)
|
||||||
|
|
|
@ -25,6 +25,7 @@ from . import hidpp10 as _hidpp10
|
||||||
from . import hidpp20 as _hidpp20
|
from . import hidpp20 as _hidpp20
|
||||||
from .common import (
|
from .common import (
|
||||||
bytes2int as _bytes2int,
|
bytes2int as _bytes2int,
|
||||||
|
int2bytes as _int2bytes,
|
||||||
NamedInts as _NamedInts,
|
NamedInts as _NamedInts,
|
||||||
unpack as _unpack,
|
unpack as _unpack,
|
||||||
)
|
)
|
||||||
|
@ -35,6 +36,7 @@ from .settings import (
|
||||||
FeatureRW as _FeatureRW,
|
FeatureRW as _FeatureRW,
|
||||||
BooleanValidator as _BooleanV,
|
BooleanValidator as _BooleanV,
|
||||||
ChoicesValidator as _ChoicesV,
|
ChoicesValidator as _ChoicesV,
|
||||||
|
RangeValidator as _RangeV,
|
||||||
)
|
)
|
||||||
|
|
||||||
_DK = _hidpp10.DEVICE_KIND
|
_DK = _hidpp10.DEVICE_KIND
|
||||||
|
@ -99,6 +101,17 @@ def feature_choices_dynamic(name, feature, choices_callback,
|
||||||
return setting(device)
|
return setting(device)
|
||||||
return instantiate
|
return instantiate
|
||||||
|
|
||||||
|
def feature_range(name, feature, min_value, max_value,
|
||||||
|
read_function_id=_FeatureRW.default_read_fnid,
|
||||||
|
write_function_id=_FeatureRW.default_write_fnid,
|
||||||
|
rw=None,
|
||||||
|
bytes_count=None,
|
||||||
|
label=None, description=None, device_kind=None):
|
||||||
|
validator = _RangeV(min_value, max_value, bytes_count=bytes_count)
|
||||||
|
if rw is None:
|
||||||
|
rw = _FeatureRW(feature, read_function_id, write_function_id)
|
||||||
|
return _Setting(name, rw, validator, kind=_KIND.range, label=label, description=description, device_kind=device_kind)
|
||||||
|
|
||||||
#
|
#
|
||||||
# common strings for settings
|
# common strings for settings
|
||||||
#
|
#
|
||||||
|
@ -117,7 +130,9 @@ _FN_SWAP = ('fn-swap', _("Swap Fx function"),
|
||||||
"and you must hold the FN key to activate their special function."))
|
"and you must hold the FN key to activate their special function."))
|
||||||
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
|
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
|
||||||
_("Turn on illumination when the hands hover over the keyboard."))
|
_("Turn on illumination when the hands hover over the keyboard."))
|
||||||
|
_SMART_SHIFT = ('smart-shift', _("Smart Shift"),
|
||||||
|
_("Automatically switch the mouse wheel between ratchet and freespin mode.\n"
|
||||||
|
"The mouse wheel is always free at 0, and always locked at 50"))
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -164,6 +179,42 @@ def _feature_smooth_scroll():
|
||||||
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
|
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
|
||||||
device_kind=_DK.mouse)
|
device_kind=_DK.mouse)
|
||||||
|
|
||||||
|
def _feature_smart_shift():
|
||||||
|
_MIN_SMART_SHIFT_VALUE = 0
|
||||||
|
_MAX_SMART_SHIFT_VALUE = 50
|
||||||
|
class _SmartShiftRW(_FeatureRW):
|
||||||
|
def __init__(self, feature):
|
||||||
|
super(_SmartShiftRW, self).__init__(feature)
|
||||||
|
|
||||||
|
def read(self, device):
|
||||||
|
value = super(_SmartShiftRW, self).read(device)
|
||||||
|
if _bytes2int(value[0:1]) == 1:
|
||||||
|
# Mode = Freespin, map to minimum
|
||||||
|
return _int2bytes(_MIN_SMART_SHIFT_VALUE, count=1)
|
||||||
|
else:
|
||||||
|
# Mode = smart shift, map to the value, capped at maximum
|
||||||
|
threshold = min(_bytes2int(value[1:2]), _MAX_SMART_SHIFT_VALUE)
|
||||||
|
return _int2bytes(threshold, count=1)
|
||||||
|
|
||||||
|
def write(self, device, data_bytes):
|
||||||
|
threshold = _bytes2int(data_bytes)
|
||||||
|
# Freespin at minimum
|
||||||
|
mode = 1 if threshold == _MIN_SMART_SHIFT_VALUE else 2
|
||||||
|
|
||||||
|
# Ratchet at maximum
|
||||||
|
if threshold == _MAX_SMART_SHIFT_VALUE:
|
||||||
|
threshold = 255
|
||||||
|
|
||||||
|
data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) * 2
|
||||||
|
return super(_SmartShiftRW, self).write(device, data)
|
||||||
|
|
||||||
|
return feature_range(_SMART_SHIFT[0], _F.SMART_SHIFT,
|
||||||
|
_MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE,
|
||||||
|
bytes_count=1,
|
||||||
|
rw=_SmartShiftRW(_F.SMART_SHIFT),
|
||||||
|
label=_SMART_SHIFT[1], description=_SMART_SHIFT[2],
|
||||||
|
device_kind=_DK.mouse)
|
||||||
|
|
||||||
def _feature_adjustable_dpi_choices(device):
|
def _feature_adjustable_dpi_choices(device):
|
||||||
# [1] getSensorDpiList(sensorIdx)
|
# [1] getSensorDpiList(sensorIdx)
|
||||||
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
|
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
|
||||||
|
@ -212,6 +263,7 @@ _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [
|
||||||
'dpi',
|
'dpi',
|
||||||
'hand_detection',
|
'hand_detection',
|
||||||
'typing_illumination',
|
'typing_illumination',
|
||||||
|
'smart_shift',
|
||||||
])
|
])
|
||||||
del namedtuple
|
del namedtuple
|
||||||
|
|
||||||
|
@ -223,6 +275,7 @@ RegisterSettings = _SETTINGS_LIST(
|
||||||
dpi=_register_dpi,
|
dpi=_register_dpi,
|
||||||
hand_detection=_register_hand_detection,
|
hand_detection=_register_hand_detection,
|
||||||
typing_illumination=None,
|
typing_illumination=None,
|
||||||
|
smart_shift=None,
|
||||||
)
|
)
|
||||||
FeatureSettings = _SETTINGS_LIST(
|
FeatureSettings = _SETTINGS_LIST(
|
||||||
fn_swap=_feature_fn_swap,
|
fn_swap=_feature_fn_swap,
|
||||||
|
@ -232,6 +285,7 @@ FeatureSettings = _SETTINGS_LIST(
|
||||||
dpi=_feature_adjustable_dpi,
|
dpi=_feature_adjustable_dpi,
|
||||||
hand_detection=None,
|
hand_detection=None,
|
||||||
typing_illumination=None,
|
typing_illumination=None,
|
||||||
|
smart_shift=_feature_smart_shift,
|
||||||
)
|
)
|
||||||
|
|
||||||
del _SETTINGS_LIST
|
del _SETTINGS_LIST
|
||||||
|
@ -266,6 +320,7 @@ def check_feature_settings(device, already_known):
|
||||||
already_known.append(feature(device))
|
already_known.append(feature(device))
|
||||||
|
|
||||||
check_feature(_SMOOTH_SCROLL[0], _F.HI_RES_SCROLLING)
|
check_feature(_SMOOTH_SCROLL[0], _F.HI_RES_SCROLLING)
|
||||||
check_feature(_FN_SWAP[0], _F.FN_INVERSION)
|
check_feature(_FN_SWAP[0], _F.FN_INVERSION)
|
||||||
check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap')
|
check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap')
|
||||||
check_feature(_DPI[0], _F.ADJUSTABLE_DPI)
|
check_feature(_DPI[0], _F.ADJUSTABLE_DPI)
|
||||||
|
check_feature(_SMART_SHIFT[0], _F.SMART_SHIFT)
|
||||||
|
|
|
@ -112,6 +112,12 @@ def run(receivers, args, find_receiver, find_device):
|
||||||
raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices)))
|
raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices)))
|
||||||
value = setting.choices[value]
|
value = setting.choices[value]
|
||||||
|
|
||||||
|
elif setting.kind == _settings.KIND.range:
|
||||||
|
try:
|
||||||
|
value = int(args.value)
|
||||||
|
except ValueError:
|
||||||
|
raise Exception("can't interpret '%s' as integer" % args.value)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||||
|
|
||||||
from gi.repository import Gtk, GLib
|
from gi.repository import Gtk, GLib
|
||||||
|
from threading import Timer as _Timer
|
||||||
|
|
||||||
from solaar.i18n import _
|
from solaar.i18n import _
|
||||||
from solaar.ui import async as _ui_async
|
from solaar.ui import async as _ui_async
|
||||||
|
@ -76,15 +76,37 @@ def _create_choice_control(setting):
|
||||||
c.connect('changed', _combo_notify, setting)
|
c.connect('changed', _combo_notify, setting)
|
||||||
return c
|
return c
|
||||||
|
|
||||||
# def _create_slider_control(setting):
|
def _create_slider_control(setting):
|
||||||
# def _slider_notify(slider, s):
|
class SliderControl:
|
||||||
# if slider.get_sensitive():
|
__slots__ = ('gtk_range', 'timer', 'setting')
|
||||||
# _apply_queue.put(('write', s, slider.get_value(), slider.get_parent()))
|
def __init__(self, setting):
|
||||||
#
|
self.setting = setting
|
||||||
# c = Gtk.Scale(setting.choices)
|
self.timer = None
|
||||||
# c.connect('value-changed', _slider_notify, setting)
|
|
||||||
#
|
self.gtk_range = Gtk.Scale()
|
||||||
# return c
|
self.gtk_range.set_range(*self.setting.range)
|
||||||
|
self.gtk_range.set_round_digits(0)
|
||||||
|
self.gtk_range.set_digits(0)
|
||||||
|
self.gtk_range.set_increments(1, 5)
|
||||||
|
self.gtk_range.connect('value-changed',
|
||||||
|
lambda _, c: c._changed(),
|
||||||
|
self)
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
|
_write_async(self.setting,
|
||||||
|
int(self.gtk_range.get_value()),
|
||||||
|
self.gtk_range.get_parent())
|
||||||
|
self.timer.cancel()
|
||||||
|
|
||||||
|
def _changed(self):
|
||||||
|
if self.gtk_range.get_sensitive():
|
||||||
|
if self.timer:
|
||||||
|
self.timer.cancel()
|
||||||
|
self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write))
|
||||||
|
self.timer.start()
|
||||||
|
|
||||||
|
control = SliderControl(setting)
|
||||||
|
return control.gtk_range
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -102,15 +124,17 @@ def _create_sbox(s):
|
||||||
|
|
||||||
if s.kind == _SETTING_KIND.toggle:
|
if s.kind == _SETTING_KIND.toggle:
|
||||||
control = _create_toggle_control(s)
|
control = _create_toggle_control(s)
|
||||||
|
sbox.pack_end(control, False, False, 0)
|
||||||
elif s.kind == _SETTING_KIND.choice:
|
elif s.kind == _SETTING_KIND.choice:
|
||||||
control = _create_choice_control(s)
|
control = _create_choice_control(s)
|
||||||
# elif s.kind == _SETTING_KIND.range:
|
sbox.pack_end(control, False, False, 0)
|
||||||
# control = _create_slider_control(s)
|
elif s.kind == _SETTING_KIND.range:
|
||||||
|
control = _create_slider_control(s)
|
||||||
|
sbox.pack_end(control, True, True, 0)
|
||||||
else:
|
else:
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
control.set_sensitive(False) # the first read will enable it
|
control.set_sensitive(False) # the first read will enable it
|
||||||
sbox.pack_end(control, False, False, 0)
|
|
||||||
sbox.pack_end(spinner, False, False, 0)
|
sbox.pack_end(spinner, False, False, 0)
|
||||||
sbox.pack_end(failed, False, False, 0)
|
sbox.pack_end(failed, False, False, 0)
|
||||||
|
|
||||||
|
@ -140,8 +164,8 @@ def _update_setting_item(sbox, value, is_online=True):
|
||||||
control.set_active(value)
|
control.set_active(value)
|
||||||
elif isinstance(control, Gtk.ComboBoxText):
|
elif isinstance(control, Gtk.ComboBoxText):
|
||||||
control.set_active_id(str(value))
|
control.set_active_id(str(value))
|
||||||
# elif isinstance(control, Gtk.Scale):
|
elif isinstance(control, Gtk.Scale):
|
||||||
# control.set_value(int(value))
|
control.set_value(int(value))
|
||||||
else:
|
else:
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
control.set_sensitive(True)
|
control.set_sensitive(True)
|
||||||
|
|
Loading…
Reference in New Issue