settings: add mouse gesture handling
This commit is contained in:
parent
6c62f975d0
commit
f7488f67c1
|
@ -158,6 +158,20 @@ def signed(bytes):
|
||||||
return int.from_bytes(bytes, 'big', signed=True)
|
return int.from_bytes(bytes, 'big', signed=True)
|
||||||
|
|
||||||
|
|
||||||
|
def xy_direction(d):
|
||||||
|
x, y = _unpack('!2h', d[:4])
|
||||||
|
if x > 0 and x >= abs(y):
|
||||||
|
return 'right'
|
||||||
|
elif x < 0 and abs(x) >= abs(y):
|
||||||
|
return 'left'
|
||||||
|
elif y > 0:
|
||||||
|
return 'down'
|
||||||
|
elif y < 0:
|
||||||
|
return 'up'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
TESTS = {
|
TESTS = {
|
||||||
'crown_right': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] < 128 and d[1],
|
'crown_right': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] < 128 and d[1],
|
||||||
'crown_left': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] >= 128 and 256 - d[1],
|
'crown_left': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] >= 128 and 256 - d[1],
|
||||||
|
@ -173,6 +187,10 @@ TESTS = {
|
||||||
'lowres_wheel_down': lambda f, r, d: f == _F.LOWRES_WHEEL and r == 0 and signed(d[0:1]) < 0 and signed(d[0:1]),
|
'lowres_wheel_down': lambda f, r, d: f == _F.LOWRES_WHEEL and r == 0 and signed(d[0:1]) < 0 and signed(d[0:1]),
|
||||||
'hires_wheel_up': lambda f, r, d: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) > 0 and signed(d[1:3]),
|
'hires_wheel_up': lambda f, r, d: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) > 0 and signed(d[1:3]),
|
||||||
'hires_wheel_down': lambda f, r, d: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) < 0 and signed(d[1:3]),
|
'hires_wheel_down': lambda f, r, d: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) < 0 and signed(d[1:3]),
|
||||||
|
'mouse-down': lambda f, r, d: f == _F.MOUSE_GESTURE and xy_direction(d) == 'down',
|
||||||
|
'mouse-up': lambda f, r, d: f == _F.MOUSE_GESTURE and xy_direction(d) == 'up',
|
||||||
|
'mouse-left': lambda f, r, d: f == _F.MOUSE_GESTURE and xy_direction(d) == 'left',
|
||||||
|
'mouse-right': lambda f, r, d: f == _F.MOUSE_GESTURE and xy_direction(d) == 'right',
|
||||||
'False': lambda f, r, d: False,
|
'False': lambda f, r, d: False,
|
||||||
'True': lambda f, r, d: True,
|
'True': lambda f, r, d: True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,6 +164,8 @@ FEATURE = _NamedInts(
|
||||||
SIDETONE=0x8300,
|
SIDETONE=0x8300,
|
||||||
EQUALIZER=0x8310,
|
EQUALIZER=0x8310,
|
||||||
HEADSET_OUT=0x8320,
|
HEADSET_OUT=0x8320,
|
||||||
|
# Fake features for Solaar internal use
|
||||||
|
MOUSE_GESTURE=0xFE00,
|
||||||
)
|
)
|
||||||
FEATURE._fallback = lambda x: 'unknown:%04X' % x
|
FEATURE._fallback = lambda x: 'unknown:%04X' % x
|
||||||
|
|
||||||
|
@ -525,13 +527,12 @@ class ReprogrammableKeyV4(ReprogrammableKey):
|
||||||
"""
|
"""
|
||||||
flags = flags if flags else {} # See flake8 B006
|
flags = flags if flags else {} # See flake8 B006
|
||||||
|
|
||||||
if special_keys.MAPPING_FLAG.raw_XY_diverted in flags and flags[special_keys.MAPPING_FLAG.raw_XY_diverted]:
|
# if special_keys.MAPPING_FLAG.raw_XY_diverted in flags and flags[special_keys.MAPPING_FLAG.raw_XY_diverted]:
|
||||||
# We need diversion to report raw XY, so divert temporarily
|
# We need diversion to report raw XY, so divert temporarily
|
||||||
# (since XY reporting is also temporary)
|
# (since XY reporting is also temporary)
|
||||||
flags[special_keys.MAPPING_FLAG.diverted] = True
|
# flags[special_keys.MAPPING_FLAG.diverted] = True
|
||||||
|
# if special_keys.MAPPING_FLAG.diverted in flags and not flags[special_keys.MAPPING_FLAG.diverted]:
|
||||||
if special_keys.MAPPING_FLAG.diverted in flags and not flags[special_keys.MAPPING_FLAG.diverted]:
|
# flags[special_keys.MAPPING_FLAG.raw_XY_diverted] = False
|
||||||
flags[special_keys.MAPPING_FLAG.raw_XY_diverted] = False
|
|
||||||
|
|
||||||
# The capability required to set a given reporting flag.
|
# The capability required to set a given reporting flag.
|
||||||
FLAG_TO_CAPABILITY = {
|
FLAG_TO_CAPABILITY = {
|
||||||
|
|
|
@ -25,10 +25,13 @@ from copy import copy as _copy
|
||||||
from logging import DEBUG as _DEBUG
|
from logging import DEBUG as _DEBUG
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
from . import hidpp20 as _hidpp20
|
||||||
|
from . import special_keys as _special_keys
|
||||||
from .common import NamedInt as _NamedInt
|
from .common import NamedInt as _NamedInt
|
||||||
from .common import NamedInts as _NamedInts
|
from .common import NamedInts as _NamedInts
|
||||||
from .common import bytes2int as _bytes2int
|
from .common import bytes2int as _bytes2int
|
||||||
from .common import int2bytes as _int2bytes
|
from .common import int2bytes as _int2bytes
|
||||||
|
from .common import unpack as _unpack
|
||||||
|
|
||||||
_log = getLogger(__name__)
|
_log = getLogger(__name__)
|
||||||
del getLogger
|
del getLogger
|
||||||
|
@ -1032,6 +1035,119 @@ class MultipleRangeValidator:
|
||||||
return w + b'\xFF'
|
return w + b'\xFF'
|
||||||
|
|
||||||
|
|
||||||
|
# Turn diverted mouse movement events into a mouse gesture
|
||||||
|
#
|
||||||
|
# Uses the following FSM.
|
||||||
|
# At initialization, we go into `start` state and begin accumulating displacement.
|
||||||
|
# If terminated in this state, we report back no movement.
|
||||||
|
# If the mouse moves enough, we go into the `moved` state run the progress function.
|
||||||
|
# If terminated in this state, we report back how much movement.
|
||||||
|
class DivertedMouseMovement(object):
|
||||||
|
def __init__(self, device, dpi_name, key):
|
||||||
|
self.device = device
|
||||||
|
self.key = key
|
||||||
|
self.dx = 0.
|
||||||
|
self.dy = 0.
|
||||||
|
self.fsmState = 'idle'
|
||||||
|
self.dpiSetting = next(filter(lambda s: s.name == dpi_name, device.settings), None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def notification_handler(device, n):
|
||||||
|
"""Called on notification events from the mouse."""
|
||||||
|
if n.sub_id < 0x40 and device.features[n.sub_id] == _hidpp20.FEATURE.REPROG_CONTROLS_V4:
|
||||||
|
state = device._divertedMMState
|
||||||
|
assert state
|
||||||
|
if n.address == 0x00:
|
||||||
|
cid1, cid2, cid3, cid4 = _unpack('!HHHH', n.data[:8])
|
||||||
|
state.handle_keys_event({cid1, cid2, cid3, cid4})
|
||||||
|
elif n.address == 0x10:
|
||||||
|
dx, dy = _unpack('!hh', n.data[:4])
|
||||||
|
state.handle_move_event(dx, dy)
|
||||||
|
|
||||||
|
def handle_move_event(self, dx, dy):
|
||||||
|
# This multiplier yields a more-or-less DPI-independent dx of about 5/cm
|
||||||
|
# The multiplier could be configurable to allow adjusting dx
|
||||||
|
dpi = self.dpiSetting.read() if self.dpiSetting else 1000
|
||||||
|
dx = float(dx) / float(dpi) * 15.
|
||||||
|
self.dx += dx
|
||||||
|
dy = float(dy) / float(dpi) * 15.
|
||||||
|
self.dy += dy
|
||||||
|
if self.fsmState == 'pressed':
|
||||||
|
if abs(self.dx) >= 1. or abs(self.dy) >= 1.:
|
||||||
|
self.fsmState = 'moved'
|
||||||
|
|
||||||
|
def handle_keys_event(self, cids):
|
||||||
|
if self.fsmState == 'idle':
|
||||||
|
if self.key in cids:
|
||||||
|
self.fsmState = 'pressed'
|
||||||
|
self.dx = 0.
|
||||||
|
self.dy = 0.
|
||||||
|
elif self.fsmState == 'pressed' or self.fsmState == 'moved':
|
||||||
|
if self.key not in cids:
|
||||||
|
# emit mouse gesture notification
|
||||||
|
from .base import _HIDPP_Notification as _HIDPP_Notification
|
||||||
|
from .common import pack as _pack
|
||||||
|
from .diversion import process_notification as _process_notification
|
||||||
|
payload = _pack('!hh', int(self.dx), int(self.dy))
|
||||||
|
notification = _HIDPP_Notification(0, 0, 0, 0, payload)
|
||||||
|
_process_notification(self.device, self.device.status, notification, _hidpp20.FEATURE.MOUSE_GESTURE)
|
||||||
|
self.fsmState = 'idle'
|
||||||
|
self.dx = 0.
|
||||||
|
self.dy = 0.
|
||||||
|
|
||||||
|
|
||||||
|
MouseGestureKeys = [
|
||||||
|
_special_keys.CONTROL.App_Switch_Gesture,
|
||||||
|
_special_keys.CONTROL.MultiPlatform_Gesture_Button,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DivertedMouseMovementRW(object):
|
||||||
|
def __init__(self, dpi_name):
|
||||||
|
self.kind = FeatureRW.kind # pretend to be FeatureRW as required for HID++ 2.0 devices
|
||||||
|
self.dpi_name = dpi_name
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def read(self, device): # need to return bytes, not a boolean
|
||||||
|
return b'0x01' if '_divertedMMState' in device.__dict__ else b'0x00'
|
||||||
|
|
||||||
|
def write(self, device, data_bytes):
|
||||||
|
def handler(device, n):
|
||||||
|
"""Called on notification events from the mouse."""
|
||||||
|
if n.sub_id < 0x40 and device.features[n.sub_id] == _hidpp20.FEATURE.REPROG_CONTROLS_V4:
|
||||||
|
state = device._divertedMMState
|
||||||
|
if n.address == 0x00:
|
||||||
|
cid1, cid2, cid3, cid4 = _unpack('!HHHH', n.data[:8])
|
||||||
|
state.handle_keys_event({cid1, cid2, cid3, cid4})
|
||||||
|
elif n.address == 0x10:
|
||||||
|
dx, dy = _unpack('!hh', n.data[:4])
|
||||||
|
state.handle_move_event(dx, dy)
|
||||||
|
|
||||||
|
if bool(data_bytes): # enable
|
||||||
|
# Enable HID++ events on moving the mouse while button held
|
||||||
|
for key_number in MouseGestureKeys:
|
||||||
|
key_index = device.keys.index(key_number)
|
||||||
|
self.key = device.keys[key_index] if key_index is not None else None
|
||||||
|
if self.key and 'raw XY' in self.key.flags:
|
||||||
|
self.key.set_rawXY_reporting(True)
|
||||||
|
# Store our variables in the device object
|
||||||
|
device._divertedMMState = DivertedMouseMovement(device, self.dpi_name, self.key.key)
|
||||||
|
device.add_notification_handler('diverted-mouse-movement-handler', handler)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_log.error('cannot enable diverted mouse movement on %s', device)
|
||||||
|
else: # disable
|
||||||
|
try:
|
||||||
|
device.remove_notification_handler('diverted-mouse-movement-handler')
|
||||||
|
del device._divertedMMState
|
||||||
|
if self.key:
|
||||||
|
self.key.set_rawXY_reporting(False)
|
||||||
|
self.key = None
|
||||||
|
except Exception:
|
||||||
|
_log.error('cannot disable diverted mouse movement on %s', device)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def apply_all_settings(device):
|
def apply_all_settings(device):
|
||||||
persister = getattr(device, 'persister', None)
|
persister = getattr(device, 'persister', None)
|
||||||
sensitives = persister.get('_sensitive', {}) if persister else {}
|
sensitives = persister.get('_sensitive', {}) if persister else {}
|
||||||
|
|
|
@ -44,6 +44,7 @@ from .settings import ChoicesValidator as _ChoicesV
|
||||||
from .settings import FeatureRW as _FeatureRW
|
from .settings import FeatureRW as _FeatureRW
|
||||||
from .settings import FeatureRWMap as _FeatureRWMap
|
from .settings import FeatureRWMap as _FeatureRWMap
|
||||||
from .settings import LongSettings as _LongSettings
|
from .settings import LongSettings as _LongSettings
|
||||||
|
from .settings import MouseGestureKeys as _MouseGestureKeys
|
||||||
from .settings import MultipleRangeValidator as _MultipleRangeV
|
from .settings import MultipleRangeValidator as _MultipleRangeV
|
||||||
from .settings import RangeValidator as _RangeV
|
from .settings import RangeValidator as _RangeV
|
||||||
from .settings import RegisterRW as _RegisterRW
|
from .settings import RegisterRW as _RegisterRW
|
||||||
|
@ -110,6 +111,8 @@ _GESTURE2_GESTURES = ('gesture2-gestures', _('Gestures'), _('Tweak the mouse/tou
|
||||||
_GESTURE2_PARAMS = ('gesture2-params', _('Gesture params'), _('Change numerical parameters of a mouse/touchpad.'))
|
_GESTURE2_PARAMS = ('gesture2-params', _('Gesture params'), _('Change numerical parameters of a mouse/touchpad.'))
|
||||||
_DPI_SLIDING = ('dpi-sliding', _('DPI Sliding Adjustment'),
|
_DPI_SLIDING = ('dpi-sliding', _('DPI Sliding Adjustment'),
|
||||||
_('Adjust the DPI by sliding the mouse horizontally while holding the DPI button.'))
|
_('Adjust the DPI by sliding the mouse horizontally while holding the DPI button.'))
|
||||||
|
_MOUSE_GESTURES = ('mouse-gestures', _('Mouse Gestures'),
|
||||||
|
_('Send a gesture by sliding the mouse while holding the App Switch button.'))
|
||||||
_DIVERT_CROWN = ('divert-crown', _('Divert crown events'),
|
_DIVERT_CROWN = ('divert-crown', _('Divert crown events'),
|
||||||
_('Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).'))
|
_('Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).'))
|
||||||
_DIVERT_GKEYS = ('divert-gkeys', _('Divert G Keys'),
|
_DIVERT_GKEYS = ('divert-gkeys', _('Divert G Keys'),
|
||||||
|
@ -536,6 +539,27 @@ def _feature_adjustable_dpi():
|
||||||
return _Setting(_DPI, rw, callback=_feature_adjustable_dpi_callback, device_kind=(_DK.mouse, _DK.trackball))
|
return _Setting(_DPI, rw, callback=_feature_adjustable_dpi_callback, device_kind=(_DK.mouse, _DK.trackball))
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_mouse_gesture_callback(device):
|
||||||
|
# need a gesture button that can send raw XY
|
||||||
|
for key in _MouseGestureKeys:
|
||||||
|
key_index = device.keys.index(key)
|
||||||
|
if key_index is not None and 'raw XY' in device.keys[key_index].flags:
|
||||||
|
return _BooleanV()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_mouse_gesture():
|
||||||
|
"""Implements the ability to send mouse gestures
|
||||||
|
by sliding a mouse horizontally or vertically while holding the App Switch button."""
|
||||||
|
from .settings import DivertedMouseMovementRW as _DivertedMouseMovementRW
|
||||||
|
return _Setting(
|
||||||
|
_MOUSE_GESTURES,
|
||||||
|
_DivertedMouseMovementRW(_DPI[0]),
|
||||||
|
callback=_feature_mouse_gesture_callback,
|
||||||
|
device_kind=(_DK.mouse, )
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Implemented based on code in libratrag
|
# Implemented based on code in libratrag
|
||||||
def _feature_report_rate_callback(device):
|
def _feature_report_rate_callback(device):
|
||||||
if device.wpid == '408E':
|
if device.wpid == '408E':
|
||||||
|
@ -784,6 +808,7 @@ _SETTINGS_TABLE = [
|
||||||
_S(_THUMB_SCROLL_INVERT, _F.THUMB_WHEEL, _feature_thumb_invert),
|
_S(_THUMB_SCROLL_INVERT, _F.THUMB_WHEEL, _feature_thumb_invert),
|
||||||
_S(_DPI, _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi),
|
_S(_DPI, _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi),
|
||||||
_S(_DPI_SLIDING, _F.REPROG_CONTROLS_V4, _feature_dpi_sliding),
|
_S(_DPI_SLIDING, _F.REPROG_CONTROLS_V4, _feature_dpi_sliding),
|
||||||
|
_S(_MOUSE_GESTURES, _F.REPROG_CONTROLS_V4, _feature_mouse_gesture),
|
||||||
_S(_POINTER_SPEED, _F.POINTER_SPEED, _feature_pointer_speed),
|
_S(_POINTER_SPEED, _F.POINTER_SPEED, _feature_pointer_speed),
|
||||||
_S(_BACKLIGHT, _F.BACKLIGHT2, _feature_backlight2),
|
_S(_BACKLIGHT, _F.BACKLIGHT2, _feature_backlight2),
|
||||||
_S(_FN_SWAP, _F.FN_INVERSION, _feature_fn_swap, registerFn=_register_fn_swap),
|
_S(_FN_SWAP, _F.FN_INVERSION, _feature_fn_swap, registerFn=_register_fn_swap),
|
||||||
|
|
Loading…
Reference in New Issue