settings: use new setting method for MOUSE GESTURE setting

This commit is contained in:
Peter F. Patel-Schneider 2021-10-13 11:45:08 -04:00
parent 2ca0bd9ac3
commit f1d896ded3
3 changed files with 117 additions and 190 deletions

View File

@ -26,6 +26,14 @@ diversion can be done with your devices. Runing Solaar with the `-dd`
option will show information about notifications, including their feature option will show information about notifications, including their feature
name, report number, and data. name, report number, and data.
Solaar can also create special notifications in response to mouse movements on some mice.
Setting the `Mouse Gestures` setting to a key enables special processing of mouse movements
while the key is depressed. Moving the mouse creates a mouse movement event.
Stopping the mouse for a little while and moving it again creates another mouse movement event.
Pressing a diverted key creates a key event.
When the key is released the sequence of events is sent as a synthetic notification
that can be matched with `Mouse Gesture` conditions.
In response to a feature-based HID++ notification Solaar runs a sequence of In response to a feature-based HID++ notification Solaar runs a sequence of
rules. A `Rule` is a sequence of components, which are either sub-rules, rules. A `Rule` is a sequence of components, which are either sub-rules,
conditions, or actions. Conditions and actions are dictionaries with one conditions, or actions. Conditions and actions are dictionaries with one

View File

@ -25,12 +25,8 @@ from copy import copy as _copy
from logging import DEBUG as _DEBUG from logging import DEBUG as _DEBUG
from logging import WARNING as _WARNING from logging import WARNING as _WARNING
from logging import getLogger from logging import getLogger
## use regular time instead of time_ns so as not to require Python 3.7
# from time import time_ns as _time_ns
from time import time as _time
from . import hidpp20 as _hidpp20 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
@ -1075,6 +1071,9 @@ class ActionSettingRW(object):
def move_action(self, dx, dy): # action to take when mouse is moved while key is down def move_action(self, dx, dy): # action to take when mouse is moved while key is down
pass pass
def key_action(self, key): # acction to take when some other diverted key is pressed
pass
def read(self, device): # need to return bytes, as if read from device def read(self, device): # need to return bytes, as if read from device
return _int2bytes(self.key, 2) if self.active else b'\x00\x00' return _int2bytes(self.key, 2) if self.active else b'\x00\x00'
@ -1083,12 +1082,19 @@ class ActionSettingRW(object):
if n.sub_id < 0x40 and device.features[n.sub_id] == _hidpp20.FEATURE.REPROG_CONTROLS_V4: if n.sub_id < 0x40 and device.features[n.sub_id] == _hidpp20.FEATURE.REPROG_CONTROLS_V4:
if n.address == 0x00: if n.address == 0x00:
cids = _unpack('!HHHH', n.data[:8]) cids = _unpack('!HHHH', n.data[:8])
if not self.pressed and int(self.key.key) in cids: if not self.pressed and int(self.key.key) in cids: # trigger key pressed
self.pressed = True self.pressed = True
self.press_action() self.press_action()
elif self.pressed and int(self.key.key) not in cids: elif self.pressed:
self.pressed = False if int(self.key.key) not in cids: # trigger key released
self.release_action() self.pressed = False
self.release_action()
else:
print(self.key.key, cids)
for key in cids:
if key and not key == self.key.key: # some other diverted key pressed
print(key, self.key, cids)
self.key_action(key)
elif n.address == 0x10: elif n.address == 0x10:
if self.pressed: if self.pressed:
dx, dy = _unpack('!hh', n.data[:4]) dx, dy = _unpack('!hh', n.data[:4])
@ -1123,162 +1129,6 @@ class ActionSettingRW(object):
return True return True
# 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)
self.data = [0]
self.lastEv = 0.
self.skip = False
@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 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.data[0] += 1
self.dx = 0.
self.dy = 0.
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
now = _time() * 1000 # _time_ns() / 1e6
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 now - self.lastEv > 50. and not self.skip:
self.push_mouse_event()
self.lastEv = now
self.skip = False
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.
self.lastEv = _time() * 1000 # _time_ns() / 1e6
self.skip = True
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
self.push_mouse_event()
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.data.clear()
self.data.append(0)
self.fsmState = 'idle'
else:
last = (cids - {self.key, 0})
if len(last) != 0:
self.push_mouse_event()
self.data.append(1)
self.data.append(list(last)[0])
self.data[0] += 1
self.lastEv = _time() * 1000 # _time_ns() / 1e6
return True
MouseGestureKeys = [
_special_keys.CONTROL.Mouse_Gesture_Button,
_special_keys.CONTROL.MultiPlatform_Gesture_Button,
]
class DivertedMouseMovementRW(object):
def __init__(self, dpi_name, divert_name):
self.kind = FeatureRW.kind # pretend to be FeatureRW as required for HID++ 2.0 devices
self.dpi_name = dpi_name
self.divert_name = divert_name
self.key = None
def read(self, device): # need to return bytes, as if read from device
return _int2bytes(device._divertedMMState.key, 2) if '_divertedMMState' in device.__dict__ else b'\x00\x00'
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])
x = state.handle_keys_event({cid1, cid2, cid3, cid4})
if x:
return True
elif n.address == 0x10:
dx, dy = _unpack('!hh', n.data[:4])
state.handle_move_event(dx, dy)
key = _bytes2int(data_bytes)
if key: # enable
# Enable HID++ events on moving the mouse while button held
self.key = next((k for k in device.keys if k.key == key), None)
if self.key:
self.key.set_rawXY_reporting(True)
divertSetting = next(filter(lambda s: s.name == self.divert_name, device.settings), None)
divertSetting.write_key_value(int(self.key.key), 1)
from solaar.ui import status_changed as _status_changed
_status_changed(device, refresh=True) # update main window
# 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 for key %s', device.name, key)
else: # disable
if self.key:
self.key.set_rawXY_reporting(False)
divertSetting = next(filter(lambda s: s.name == self.divert_name, device.settings), None)
divertSetting.write_key_value(int(self.key.key), 0)
from solaar.ui import status_changed as _status_changed
_status_changed(device, refresh=True) # update main window
self.key = None
try:
device.remove_notification_handler('diverted-mouse-movement-handler')
except Exception:
pass
if hasattr(device, '_divertedMMState'):
del device._divertedMMState
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 {}

View File

@ -21,7 +21,9 @@ from __future__ import absolute_import, division, print_function, unicode_litera
from collections import namedtuple from collections import namedtuple
from logging import DEBUG as _DEBUG from logging import DEBUG as _DEBUG
from logging import INFO as _INFO
from logging import getLogger from logging import getLogger
from time import time as _time
from solaar.ui import notify as _notify from solaar.ui import notify as _notify
@ -45,7 +47,6 @@ 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
@ -518,34 +519,102 @@ 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
if device.kind == _DK.mouse:
keys = []
for key in _MouseGestureKeys:
key_index = device.keys.index(key)
dkey = device.keys[key_index] if key_index is not None else None
if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags:
keys.append(dkey.key)
if not keys: # none of the keys designed for this, so look for any key with correct flags
for key in device.keys:
if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags:
keys.append(key.key)
if keys:
keys.insert(0, _NamedInt(0, _('Off')))
return _ChoicesV(_NamedInts.list(keys), byte_count=2)
def _feature_mouse_gesture(): def _feature_mouse_gesture():
"""Implements the ability to send mouse gestures """Implements the ability to send mouse gestures
by sliding a mouse horizontally or vertically while holding the App Switch button.""" by sliding a mouse horizontally or vertically while holding the App Switch button."""
from .settings import DivertedMouseMovementRW as _DivertedMouseMovementRW class MouseGestureRW(_ActionSettingRW):
return _Setting( def activate_action(self):
_MOUSE_GESTURES, self.key.set_rawXY_reporting(True)
_DivertedMouseMovementRW(_DPI[0], _DIVERT_KEYS[0]), self.dpiSetting = next(filter(lambda s: s.name == _DPI[0], self.device.settings), None)
callback=_feature_mouse_gesture_callback, self.fsmState = 'idle'
device_kind=(_DK.mouse, ) self.initialize_data()
)
def deactivate_action(self):
self.key.set_rawXY_reporting(False)
def initialize_data(self):
self.dx = 0.
self.dy = 0.
self.lastEv = None
self.data = [0]
def press_action(self):
if self.fsmState == 'idle':
self.fsmState = 'pressed'
self.initialize_data()
def release_action(self):
if self.fsmState == 'pressed':
# 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
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.lastEv is not None and now - self.lastEv > 50.:
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.data[0] += 1
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.data[0] += 1
self.dx = 0.
self.dy = 0.
if _log.isEnabledFor(_DEBUG):
_log.debug('mouse gesture move event %d %d %s', x, y, self.data)
MouseGestureKeys = [
_special_keys.CONTROL.Mouse_Gesture_Button,
_special_keys.CONTROL.MultiPlatform_Gesture_Button,
]
def callback(device):
if device.kind == _DK.mouse:
keys = []
for key in MouseGestureKeys:
key_index = device.keys.index(key)
dkey = device.keys[key_index] if key_index is not None else None
if dkey is not None and 'raw XY' in dkey.flags and 'divertable' in dkey.flags:
keys.append(dkey.key)
if not keys: # none of the keys designed for this, so look for any key with correct flags
for key in device.keys:
if 'raw XY' in key.flags and 'divertable' in key.flags and 'virtual' not in key.flags:
keys.append(key.key)
if keys:
keys.insert(0, _NamedInt(0, _('Off')))
return _ChoicesV(_NamedInts.list(keys), byte_count=2)
rw = MouseGestureRW('mouse gesture', _DIVERT_KEYS[0])
return _Setting(_MOUSE_GESTURES, rw, callback=callback, device_kind=(_DK.mouse, ))
# Implemented based on code in libratrag # Implemented based on code in libratrag