Merge pull request #259 from javitonino/support-mx-master

Support mx master
This commit is contained in:
Peter Wu 2016-07-24 23:09:28 +02:00 committed by GitHub
commit 28e43395ab
5 changed files with 149 additions and 22 deletions

View File

@ -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 | | |

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)