settings: use new setting method for MOUSE GESTURE setting
This commit is contained in:
parent
2ca0bd9ac3
commit
f1d896ded3
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
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 {}
|
||||
|
|
|
@ -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,11 +519,88 @@ 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
|
||||
def _feature_mouse_gesture():
|
||||
"""Implements the ability to send mouse gestures
|
||||
by sliding a mouse horizontally or vertically while holding the App Switch button."""
|
||||
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:
|
||||
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:
|
||||
|
@ -535,17 +613,8 @@ def _feature_mouse_gesture_callback(device):
|
|||
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, )
|
||||
)
|
||||
rw = MouseGestureRW('mouse gesture', _DIVERT_KEYS[0])
|
||||
return _Setting(_MOUSE_GESTURES, rw, callback=callback, device_kind=(_DK.mouse, ))
|
||||
|
||||
|
||||
# Implemented based on code in libratrag
|
||||
|
|
Loading…
Reference in New Issue