rules: allow sequence of mouse moves as mouse gestures
* Add more robust mouse gesture support - Remove existing mouse-* Test types - Add new 'Mouse Gesture' Condition - Implement Rule Editor UI for it - Add support for diverted buttons - Added diagonal mouse gesture directions Allows you to chain multiple movements/buttons (for instance, moving the mouse up and then left) together into a single mappable gesture. * Update docs * Cleanup Fix inconsistent indenting Fix possible overwriting of built-in Fix 'Mouse Gesture' Condition rule not starting with an initial Action field * Make flake8 happy * yapf * Document no-op and make it more apparent * Make changes to Mouse Gesture UI suggested/submitted by viniciusbm. Co-authored-by: Apeiron <apeiron@none> Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
This commit is contained in:
parent
8854ca6f23
commit
011f3f556b
|
@ -89,11 +89,15 @@ A `thumb_wheel_up` test is the rotation amount of a `THUMB WHEEL` upward rotatio
|
||||||
A `thumb_wheel_down` test is the rotation amount of a `THUMB WHEEL` downward rotation.
|
A `thumb_wheel_down` test is the rotation amount of a `THUMB WHEEL` downward rotation.
|
||||||
`lowres_wheel_up`, `lowres_wheel_down`, `hires_wheel_up`, `hires_wheel_down` are the
|
`lowres_wheel_up`, `lowres_wheel_down`, `hires_wheel_up`, `hires_wheel_down` are the
|
||||||
same but for `LOWRES WHEEL` and `HIRES WHEEL`.
|
same but for `LOWRES WHEEL` and `HIRES WHEEL`.
|
||||||
A 'mouse-down' test is true for a mouse gesture mostly in the downward direction.
|
|
||||||
`mouse-up', 'mouse-left', and 'mouse-right' are the same but for gestures in the other directions.
|
|
||||||
A 'mouse-noop' test is true for a mouse gesture where the mouse doesn't move much.
|
|
||||||
`True` and `False` tests return True and False, respectively.
|
`True` and `False` tests return True and False, respectively.
|
||||||
|
|
||||||
|
`Mouse Gesture` conditions are true if the actions taken while the mouse's 'Gesture' button is held match the configured list when the 'Gesture' button is released.
|
||||||
|
The available actions are `Mouse Up`, `Mouse Down`, `Mouse Left`, `Mouse Right`, `Mouse Up-left`, `Mouse Up-Right`, `Mouse Down-left`, `Mouse Down-right`, and buttons that are diverted.
|
||||||
|
An example would be mapping `Mouse Up` -> `Mouse Up`. To perform this gesture, you would hold down the 'Gesture' button, move the mouse upwards, pause momentarily, move the mouse upwards, and release the 'Gesture' button.
|
||||||
|
Another example would be mapping `Back Button` -> `Back Button`. With this one, you would hold down the 'Gesture' button, double-tap the 'Back' button, and then release the 'Gesture' button.
|
||||||
|
Mouse movements and buttons can be mixed and chained together however you like.
|
||||||
|
It's possible to create a `No-op` gesture by clicking 'Delete' on the initial Action when you first create the rule. This gesture will trigger when you simply click the 'Gesture' button.
|
||||||
|
|
||||||
A `KeyPress` action takes a sequence of X11 key symbols and simulates a chorded keypress on the keyboard.
|
A `KeyPress` action takes a sequence of X11 key symbols and simulates a chorded keypress on the keyboard.
|
||||||
Any key symbols that correspond to modifier keys that are in the current keyboard modifiers are ignored.
|
Any key symbols that correspond to modifier keys that are in the current keyboard modifiers are ignored.
|
||||||
A `MouseScroll` action takes a sequence of two numbers and simulates a horizontal and vertical mouse scroll of these amounts.
|
A `MouseScroll` action takes a sequence of two numbers and simulates a horizontal and vertical mouse scroll of these amounts.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import sys as _sys
|
||||||
from logging import DEBUG as _DEBUG
|
from logging import DEBUG as _DEBUG
|
||||||
from logging import INFO as _INFO
|
from logging import INFO as _INFO
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from math import sqrt as _sqrt
|
||||||
|
|
||||||
import _thread
|
import _thread
|
||||||
import psutil
|
import psutil
|
||||||
|
@ -159,18 +160,31 @@ def signed(bytes):
|
||||||
return int.from_bytes(bytes, 'big', signed=True)
|
return int.from_bytes(bytes, 'big', signed=True)
|
||||||
|
|
||||||
|
|
||||||
def xy_direction(d):
|
def xy_direction(_x, _y):
|
||||||
x, y = _unpack('!2h', d[:4])
|
# normalize x and y
|
||||||
if x > 0 and x >= abs(y):
|
m = _sqrt((_x * _x) + (_y * _y))
|
||||||
return 'right'
|
if m == 0:
|
||||||
elif x < 0 and abs(x) >= abs(y):
|
return 'noop'
|
||||||
return 'left'
|
x = round(_x / m)
|
||||||
|
y = round(_y / m)
|
||||||
|
if x < 0 and y < 0:
|
||||||
|
return 'Mouse Up-left'
|
||||||
|
elif x > 0 and y < 0:
|
||||||
|
return 'Mouse Up-right'
|
||||||
|
elif x < 0 and y > 0:
|
||||||
|
return 'Mouse Down-left'
|
||||||
|
elif x > 0 and y > 0:
|
||||||
|
return 'Mouse Down-right'
|
||||||
|
elif x > 0:
|
||||||
|
return 'Mouse Right'
|
||||||
|
elif x < 0:
|
||||||
|
return 'Mouse Left'
|
||||||
elif y > 0:
|
elif y > 0:
|
||||||
return 'down'
|
return 'Mouse Down'
|
||||||
elif y < 0:
|
elif y < 0:
|
||||||
return 'up'
|
return 'Mouse Up'
|
||||||
else:
|
else:
|
||||||
return None
|
return 'noop'
|
||||||
|
|
||||||
|
|
||||||
TESTS = {
|
TESTS = {
|
||||||
|
@ -188,11 +202,6 @@ 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',
|
|
||||||
'mouse-noop': lambda f, r, d: f == _F.MOUSE_GESTURE and xy_direction(d) is None,
|
|
||||||
'False': lambda f, r, d: False,
|
'False': lambda f, r, d: False,
|
||||||
'True': lambda f, r, d: True,
|
'True': lambda f, r, d: True,
|
||||||
}
|
}
|
||||||
|
@ -445,6 +454,52 @@ class Test(Condition):
|
||||||
return {'Test': str(self.test)}
|
return {'Test': str(self.test)}
|
||||||
|
|
||||||
|
|
||||||
|
class MouseGesture(Condition):
|
||||||
|
MOVEMENTS = [
|
||||||
|
'Mouse Up', 'Mouse Down', 'Mouse Left', 'Mouse Right', 'Mouse Up-left', 'Mouse Up-right', 'Mouse Down-left',
|
||||||
|
'Mouse Down-right'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, movements):
|
||||||
|
if isinstance(movements, str):
|
||||||
|
movements = [movements]
|
||||||
|
for x in movements:
|
||||||
|
if x not in self.MOVEMENTS and x not in _CONTROL:
|
||||||
|
_log.warn('rule Key argument not name of a Logitech key: %s', x)
|
||||||
|
self.movements = movements
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'MouseGesture: ' + ' '.join(self.movements)
|
||||||
|
|
||||||
|
def evaluate(self, feature, notification, device, status, last_result):
|
||||||
|
if feature == _F.MOUSE_GESTURE:
|
||||||
|
d = notification.data
|
||||||
|
count = _unpack('!h', d[:2])[0]
|
||||||
|
data = _unpack('!' + ((int(len(d) / 2) - 1) * 'h'), d[2:])
|
||||||
|
if count != len(self.movements):
|
||||||
|
return False
|
||||||
|
x = 0
|
||||||
|
z = 0
|
||||||
|
while x < len(data):
|
||||||
|
if data[x] == 0:
|
||||||
|
direction = xy_direction(data[x + 1], data[x + 2])
|
||||||
|
if self.movements[z] != direction:
|
||||||
|
return False
|
||||||
|
x += 3
|
||||||
|
elif data[x] == 1:
|
||||||
|
if data[x + 1] not in _CONTROL:
|
||||||
|
return False
|
||||||
|
if self.movements[z] != str(_CONTROL[data[x + 1]]):
|
||||||
|
return False
|
||||||
|
x += 2
|
||||||
|
z += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def data(self):
|
||||||
|
return {'MouseGesture': [str(m) for m in self.movements]}
|
||||||
|
|
||||||
|
|
||||||
class Action(RuleComponent):
|
class Action(RuleComponent):
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
pass
|
pass
|
||||||
|
@ -622,6 +677,7 @@ COMPONENTS = {
|
||||||
'Modifiers': Modifiers,
|
'Modifiers': Modifiers,
|
||||||
'Key': Key,
|
'Key': Key,
|
||||||
'Test': Test,
|
'Test': Test,
|
||||||
|
'MouseGesture': MouseGesture,
|
||||||
'KeyPress': KeyPress,
|
'KeyPress': KeyPress,
|
||||||
'MouseScroll': MouseScroll,
|
'MouseScroll': MouseScroll,
|
||||||
'MouseClick': MouseClick,
|
'MouseClick': MouseClick,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import math
|
||||||
from copy import copy as _copy
|
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 time import time_ns as _time_ns
|
||||||
|
|
||||||
from . import hidpp20 as _hidpp20
|
from . import hidpp20 as _hidpp20
|
||||||
from . import special_keys as _special_keys
|
from . import special_keys as _special_keys
|
||||||
|
@ -1051,6 +1052,9 @@ class DivertedMouseMovement(object):
|
||||||
self.dy = 0.
|
self.dy = 0.
|
||||||
self.fsmState = 'idle'
|
self.fsmState = 'idle'
|
||||||
self.dpiSetting = next(filter(lambda s: s.name == dpi_name, device.settings), None)
|
self.dpiSetting = next(filter(lambda s: s.name == dpi_name, device.settings), None)
|
||||||
|
self.data = [0]
|
||||||
|
self.lastEv = 0.
|
||||||
|
self.skip = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def notification_handler(device, n):
|
def notification_handler(device, n):
|
||||||
|
@ -1065,14 +1069,31 @@ class DivertedMouseMovement(object):
|
||||||
dx, dy = _unpack('!hh', n.data[:4])
|
dx, dy = _unpack('!hh', n.data[:4])
|
||||||
state.handle_move_event(dx, dy)
|
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):
|
def handle_move_event(self, dx, dy):
|
||||||
# This multiplier yields a more-or-less DPI-independent dx of about 5/cm
|
# This multiplier yields a more-or-less DPI-independent dx of about 5/cm
|
||||||
# The multiplier could be configurable to allow adjusting dx
|
# The multiplier could be configurable to allow adjusting dx
|
||||||
|
now = _time_ns() / 1e6
|
||||||
dpi = self.dpiSetting.read() if self.dpiSetting else 1000
|
dpi = self.dpiSetting.read() if self.dpiSetting else 1000
|
||||||
dx = float(dx) / float(dpi) * 15.
|
dx = float(dx) / float(dpi) * 15.
|
||||||
self.dx += dx
|
self.dx += dx
|
||||||
dy = float(dy) / float(dpi) * 15.
|
dy = float(dy) / float(dpi) * 15.
|
||||||
self.dy += dy
|
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 self.fsmState == 'pressed':
|
||||||
if abs(self.dx) >= 1. or abs(self.dy) >= 1.:
|
if abs(self.dx) >= 1. or abs(self.dy) >= 1.:
|
||||||
self.fsmState = 'moved'
|
self.fsmState = 'moved'
|
||||||
|
@ -1083,18 +1104,30 @@ class DivertedMouseMovement(object):
|
||||||
self.fsmState = 'pressed'
|
self.fsmState = 'pressed'
|
||||||
self.dx = 0.
|
self.dx = 0.
|
||||||
self.dy = 0.
|
self.dy = 0.
|
||||||
|
self.lastEv = _time_ns() / 1e6
|
||||||
|
self.skip = True
|
||||||
elif self.fsmState == 'pressed' or self.fsmState == 'moved':
|
elif self.fsmState == 'pressed' or self.fsmState == 'moved':
|
||||||
if self.key not in cids:
|
if self.key not in cids:
|
||||||
# emit mouse gesture notification
|
# emit mouse gesture notification
|
||||||
from .base import _HIDPP_Notification as _HIDPP_Notification
|
from .base import _HIDPP_Notification as _HIDPP_Notification
|
||||||
from .common import pack as _pack
|
from .common import pack as _pack
|
||||||
from .diversion import process_notification as _process_notification
|
from .diversion import process_notification as _process_notification
|
||||||
payload = _pack('!hh', int(self.dx), int(self.dy))
|
self.push_mouse_event()
|
||||||
|
payload = _pack('!' + (len(self.data) * 'h'), *self.data)
|
||||||
notification = _HIDPP_Notification(0, 0, 0, 0, payload)
|
notification = _HIDPP_Notification(0, 0, 0, 0, payload)
|
||||||
_process_notification(self.device, self.device.status, notification, _hidpp20.FEATURE.MOUSE_GESTURE)
|
_process_notification(self.device, self.device.status, notification, _hidpp20.FEATURE.MOUSE_GESTURE)
|
||||||
|
self.data.clear()
|
||||||
|
self.data.append(0)
|
||||||
self.fsmState = 'idle'
|
self.fsmState = 'idle'
|
||||||
self.dx = 0.
|
else:
|
||||||
self.dy = 0.
|
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_ns() / 1e6
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
MouseGestureKeys = [
|
MouseGestureKeys = [
|
||||||
|
@ -1120,7 +1153,9 @@ class DivertedMouseMovementRW(object):
|
||||||
state = device._divertedMMState
|
state = device._divertedMMState
|
||||||
if n.address == 0x00:
|
if n.address == 0x00:
|
||||||
cid1, cid2, cid3, cid4 = _unpack('!HHHH', n.data[:8])
|
cid1, cid2, cid3, cid4 = _unpack('!HHHH', n.data[:8])
|
||||||
state.handle_keys_event({cid1, cid2, cid3, cid4})
|
x = state.handle_keys_event({cid1, cid2, cid3, cid4})
|
||||||
|
if x:
|
||||||
|
return True
|
||||||
elif n.address == 0x10:
|
elif n.address == 0x10:
|
||||||
dx, dy = _unpack('!hh', n.data[:4])
|
dx, dy = _unpack('!hh', n.data[:4])
|
||||||
state.handle_move_event(dx, dy)
|
state.handle_move_event(dx, dy)
|
||||||
|
|
|
@ -511,6 +511,7 @@ class DiversionDialog:
|
||||||
(_('Modifiers'), _DIV.Modifiers, []),
|
(_('Modifiers'), _DIV.Modifiers, []),
|
||||||
(_('Key'), _DIV.Key, ''),
|
(_('Key'), _DIV.Key, ''),
|
||||||
(_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
|
(_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
|
||||||
|
(_('Mouse Gesture'), _DIV.MouseGesture, ''),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -1073,6 +1074,94 @@ class TestUI(ConditionUI):
|
||||||
return str(component.test)
|
return str(component.test)
|
||||||
|
|
||||||
|
|
||||||
|
class MouseGestureUI(ConditionUI):
|
||||||
|
|
||||||
|
CLASS = _DIV.MouseGesture
|
||||||
|
MOUSE_GESTURE_NAMES = [
|
||||||
|
'Mouse Up', 'Mouse Down', 'Mouse Left', 'Mouse Right', 'Mouse Up-left', 'Mouse Up-right', 'Mouse Down-left',
|
||||||
|
'Mouse Down-right'
|
||||||
|
]
|
||||||
|
MOVE_NAMES = list(map(str, _CONTROL)) + MOUSE_GESTURE_NAMES
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
self.widgets = {}
|
||||||
|
self.fields = []
|
||||||
|
self.del_btns = []
|
||||||
|
self.add_btn = Gtk.Button(_('Add action'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
|
||||||
|
self.add_btn.connect('clicked', self._clicked_add)
|
||||||
|
self.widgets[self.add_btn] = (1, 0, 1, 1)
|
||||||
|
|
||||||
|
def _create_field(self):
|
||||||
|
field = Gtk.ComboBoxText.new_with_entry()
|
||||||
|
for g in self.MOUSE_GESTURE_NAMES:
|
||||||
|
field.append(g, g)
|
||||||
|
CompletionEntry.add_completion_to_entry(field.get_child(), self.MOVE_NAMES)
|
||||||
|
field.connect('changed', self._on_update)
|
||||||
|
self.fields.append(field)
|
||||||
|
self.widgets[field] = (len(self.fields) - 1, 0, 1, 1)
|
||||||
|
return field
|
||||||
|
|
||||||
|
def _create_del_btn(self):
|
||||||
|
btn = Gtk.Button(_('Delete'), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True, vexpand=True)
|
||||||
|
self.del_btns.append(btn)
|
||||||
|
self.widgets[btn] = (len(self.del_btns) - 1, 1, 1, 1)
|
||||||
|
btn.connect('clicked', self._clicked_del, len(self.del_btns) - 1)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def _clicked_add(self, _btn):
|
||||||
|
self.component.__init__(self.collect_value() + [''])
|
||||||
|
self.show(self.component)
|
||||||
|
self.fields[len(self.component.movements) - 1].grab_focus()
|
||||||
|
|
||||||
|
def _clicked_del(self, _btn, pos):
|
||||||
|
v = self.collect_value()
|
||||||
|
v.pop(pos)
|
||||||
|
self.component.__init__(v)
|
||||||
|
self.show(self.component)
|
||||||
|
self._on_update_callback()
|
||||||
|
|
||||||
|
def _on_update(self, *args):
|
||||||
|
super()._on_update(*args)
|
||||||
|
for i, f in enumerate(self.fields):
|
||||||
|
if f.get_visible():
|
||||||
|
icon = 'dialog-warning' if i < len(self.component.movements
|
||||||
|
) and self.component.movements[i] not in self.MOVE_NAMES else ''
|
||||||
|
f.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
|
|
||||||
|
def show(self, component):
|
||||||
|
n = len(component.movements)
|
||||||
|
while len(self.fields) < n:
|
||||||
|
self._create_field()
|
||||||
|
self._create_del_btn()
|
||||||
|
self.widgets[self.add_btn] = (n + 1, 0, 1, 1)
|
||||||
|
super().show(component)
|
||||||
|
for i in range(n):
|
||||||
|
field = self.fields[i]
|
||||||
|
with self.ignore_changes():
|
||||||
|
field.get_child().set_text(component.movements[i])
|
||||||
|
field.set_size_request(int(0.3 * self.panel.get_toplevel().get_size()[0]), 0)
|
||||||
|
field.show_all()
|
||||||
|
self.del_btns[i].show()
|
||||||
|
for i in range(n, len(self.fields)):
|
||||||
|
self.fields[i].hide()
|
||||||
|
self.del_btns[i].hide()
|
||||||
|
self.add_btn.set_valign(Gtk.Align.END if n >= 1 else Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
def collect_value(self):
|
||||||
|
return [f.get_active_text().strip() for f in self.fields if f.get_visible()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def left_label(cls, component):
|
||||||
|
return _('Mouse Gesture')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def right_label(cls, component):
|
||||||
|
if len(component.movements) == 0:
|
||||||
|
return 'No-op'
|
||||||
|
else:
|
||||||
|
return ' -> '.join(component.movements)
|
||||||
|
|
||||||
|
|
||||||
class ActionUI(RuleComponentUI):
|
class ActionUI(RuleComponentUI):
|
||||||
|
|
||||||
CLASS = _DIV.Action
|
CLASS = _DIV.Action
|
||||||
|
@ -1337,6 +1426,7 @@ COMPONENT_UI = {
|
||||||
_DIV.Modifiers: ModifiersUI,
|
_DIV.Modifiers: ModifiersUI,
|
||||||
_DIV.Key: KeyUI,
|
_DIV.Key: KeyUI,
|
||||||
_DIV.Test: TestUI,
|
_DIV.Test: TestUI,
|
||||||
|
_DIV.MouseGesture: MouseGestureUI,
|
||||||
_DIV.KeyPress: KeyPressUI,
|
_DIV.KeyPress: KeyPressUI,
|
||||||
_DIV.MouseScroll: MouseScrollUI,
|
_DIV.MouseScroll: MouseScrollUI,
|
||||||
_DIV.MouseClick: MouseClickUI,
|
_DIV.MouseClick: MouseClickUI,
|
||||||
|
|
Loading…
Reference in New Issue