From f1d896ded341fac39284d81f6433cb3dc487c7d7 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Wed, 13 Oct 2021 11:45:08 -0400 Subject: [PATCH] settings: use new setting method for MOUSE GESTURE setting --- docs/rules.md | 8 + lib/logitech_receiver/settings.py | 178 ++------------------ lib/logitech_receiver/settings_templates.py | 121 ++++++++++--- 3 files changed, 117 insertions(+), 190 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index 7fb05a62..7518cbb5 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -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 diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index e073d379..8fa4dc16 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -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 {} diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 686ebabb..029ca261 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -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