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 | | |
|
||||
| Performance 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 | | |
|
||||
|
||||
|
||||
|
|
|
@ -24,19 +24,20 @@ _log = getLogger(__name__)
|
|||
del getLogger
|
||||
|
||||
from copy import copy as _copy
|
||||
|
||||
import math
|
||||
|
||||
from .common import (
|
||||
NamedInt as _NamedInt,
|
||||
NamedInts as _NamedInts,
|
||||
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):
|
||||
"""A setting descriptor.
|
||||
|
@ -81,6 +82,14 @@ class Setting(object):
|
|||
|
||||
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):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
|
@ -356,3 +365,36 @@ class ChoicesValidator(object):
|
|||
raise ValueError("invalid choice %r" % new_value)
|
||||
assert isinstance(choice, _NamedInt)
|
||||
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 .common import (
|
||||
bytes2int as _bytes2int,
|
||||
int2bytes as _int2bytes,
|
||||
NamedInts as _NamedInts,
|
||||
unpack as _unpack,
|
||||
)
|
||||
|
@ -35,6 +36,7 @@ from .settings import (
|
|||
FeatureRW as _FeatureRW,
|
||||
BooleanValidator as _BooleanV,
|
||||
ChoicesValidator as _ChoicesV,
|
||||
RangeValidator as _RangeV,
|
||||
)
|
||||
|
||||
_DK = _hidpp10.DEVICE_KIND
|
||||
|
@ -99,6 +101,17 @@ def feature_choices_dynamic(name, feature, choices_callback,
|
|||
return setting(device)
|
||||
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
|
||||
#
|
||||
|
@ -117,7 +130,9 @@ _FN_SWAP = ('fn-swap', _("Swap Fx function"),
|
|||
"and you must hold the FN key to activate their special function."))
|
||||
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
|
||||
_("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],
|
||||
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):
|
||||
# [1] getSensorDpiList(sensorIdx)
|
||||
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
|
||||
|
@ -212,6 +263,7 @@ _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [
|
|||
'dpi',
|
||||
'hand_detection',
|
||||
'typing_illumination',
|
||||
'smart_shift',
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
|
@ -223,6 +275,7 @@ RegisterSettings = _SETTINGS_LIST(
|
|||
dpi=_register_dpi,
|
||||
hand_detection=_register_hand_detection,
|
||||
typing_illumination=None,
|
||||
smart_shift=None,
|
||||
)
|
||||
FeatureSettings = _SETTINGS_LIST(
|
||||
fn_swap=_feature_fn_swap,
|
||||
|
@ -232,6 +285,7 @@ FeatureSettings = _SETTINGS_LIST(
|
|||
dpi=_feature_adjustable_dpi,
|
||||
hand_detection=None,
|
||||
typing_illumination=None,
|
||||
smart_shift=_feature_smart_shift,
|
||||
)
|
||||
|
||||
del _SETTINGS_LIST
|
||||
|
@ -269,3 +323,4 @@ def check_feature_settings(device, already_known):
|
|||
check_feature(_FN_SWAP[0], _F.FN_INVERSION)
|
||||
check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap')
|
||||
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)))
|
||||
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:
|
||||
raise NotImplemented
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
from threading import Timer as _Timer
|
||||
|
||||
from solaar.i18n import _
|
||||
from solaar.ui import async as _ui_async
|
||||
|
@ -76,15 +76,37 @@ def _create_choice_control(setting):
|
|||
c.connect('changed', _combo_notify, setting)
|
||||
return c
|
||||
|
||||
# def _create_slider_control(setting):
|
||||
# def _slider_notify(slider, s):
|
||||
# if slider.get_sensitive():
|
||||
# _apply_queue.put(('write', s, slider.get_value(), slider.get_parent()))
|
||||
#
|
||||
# c = Gtk.Scale(setting.choices)
|
||||
# c.connect('value-changed', _slider_notify, setting)
|
||||
#
|
||||
# return c
|
||||
def _create_slider_control(setting):
|
||||
class SliderControl:
|
||||
__slots__ = ('gtk_range', 'timer', 'setting')
|
||||
def __init__(self, setting):
|
||||
self.setting = setting
|
||||
self.timer = None
|
||||
|
||||
self.gtk_range = Gtk.Scale()
|
||||
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:
|
||||
control = _create_toggle_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.choice:
|
||||
control = _create_choice_control(s)
|
||||
# elif s.kind == _SETTING_KIND.range:
|
||||
# control = _create_slider_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.range:
|
||||
control = _create_slider_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
else:
|
||||
raise NotImplemented
|
||||
|
||||
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(failed, False, False, 0)
|
||||
|
||||
|
@ -140,8 +164,8 @@ def _update_setting_item(sbox, value, is_online=True):
|
|||
control.set_active(value)
|
||||
elif isinstance(control, Gtk.ComboBoxText):
|
||||
control.set_active_id(str(value))
|
||||
# elif isinstance(control, Gtk.Scale):
|
||||
# control.set_value(int(value))
|
||||
elif isinstance(control, Gtk.Scale):
|
||||
control.set_value(int(value))
|
||||
else:
|
||||
raise NotImplemented
|
||||
control.set_sensitive(True)
|
||||
|
|
Loading…
Reference in New Issue