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
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
rules. A `Rule` is a sequence of components, which are either sub-rules,
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 WARNING as _WARNING
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 special_keys as _special_keys
from .common import NamedInt as _NamedInt
from .common import NamedInts as _NamedInts
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
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
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.address == 0x00:
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.press_action()
elif self.pressed and int(self.key.key) not in cids:
self.pressed = False
self.release_action()
elif self.pressed:
if int(self.key.key) not in cids: # trigger key released
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:
if self.pressed:
dx, dy = _unpack('!hh', n.data[:4])
@ -1123,162 +1129,6 @@ class ActionSettingRW(object):
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):
persister = getattr(device, 'persister', None)
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 logging import DEBUG as _DEBUG
from logging import INFO as _INFO
from logging import getLogger
from time import time as _time
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 FeatureRWMap as _FeatureRWMap
from .settings import LongSettings as _LongSettings
from .settings import MouseGestureKeys as _MouseGestureKeys
from .settings import MultipleRangeValidator as _MultipleRangeV
from .settings import RangeValidator as _RangeV
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))
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():
"""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], _DIVERT_KEYS[0]),
callback=_feature_mouse_gesture_callback,
device_kind=(_DK.mouse, )
)
class MouseGestureRW(_ActionSettingRW):
def activate_action(self):
self.key.set_rawXY_reporting(True)
self.dpiSetting = next(filter(lambda s: s.name == _DPI[0], self.device.settings), None)
self.fsmState = 'idle'
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