173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
import os
|
|
import datetime
|
|
import threading
|
|
import Quartz
|
|
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN
|
|
|
|
_button_mapping = {
|
|
LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged),
|
|
RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged),
|
|
MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged)
|
|
}
|
|
_button_state = {
|
|
LEFT: False,
|
|
RIGHT: False,
|
|
MIDDLE: False
|
|
}
|
|
_last_click = {
|
|
"time": None,
|
|
"button": None,
|
|
"position": None,
|
|
"click_count": 0
|
|
}
|
|
|
|
class MouseEventListener(object):
|
|
def __init__(self, callback, blocking=False):
|
|
self.blocking = blocking
|
|
self.callback = callback
|
|
self.listening = True
|
|
|
|
def run(self):
|
|
""" Creates a listener and loops while waiting for an event. Intended to run as
|
|
a background thread. """
|
|
self.tap = Quartz.CGEventTapCreate(
|
|
Quartz.kCGSessionEventTap,
|
|
Quartz.kCGHeadInsertEventTap,
|
|
Quartz.kCGEventTapOptionDefault,
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) |
|
|
Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel),
|
|
self.handler,
|
|
None)
|
|
loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
|
|
loop = Quartz.CFRunLoopGetCurrent()
|
|
Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
|
|
Quartz.CGEventTapEnable(self.tap, True)
|
|
|
|
while self.listening:
|
|
Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
|
|
|
|
def handler(self, proxy, e_type, event, refcon):
|
|
# TODO Separate event types by button/wheel/move
|
|
scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
|
|
key_name = name_from_scancode(scan_code)
|
|
flags = Quartz.CGEventGetFlags(event)
|
|
event_type = ""
|
|
is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
|
|
if e_type == Quartz.kCGEventKeyDown:
|
|
event_type = "down"
|
|
elif e_type == Quartz.kCGEventKeyUp:
|
|
event_type = "up"
|
|
|
|
if self.blocking:
|
|
return None
|
|
|
|
self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
|
|
return event
|
|
|
|
# Exports
|
|
|
|
def init():
|
|
""" Initializes mouse state """
|
|
pass
|
|
|
|
def listen(queue):
|
|
""" Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """
|
|
if not os.geteuid() == 0:
|
|
raise OSError("Error 13 - Must be run as administrator")
|
|
listener = MouseEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP))
|
|
t = threading.Thread(target=listener.run, args=())
|
|
t.daemon = True
|
|
t.start()
|
|
|
|
def press(button=LEFT):
|
|
""" Sends a down event for the specified button, using the provided constants """
|
|
location = get_position()
|
|
button_code, button_down, _, _ = _button_mapping[button]
|
|
e = Quartz.CGEventCreateMouseEvent(
|
|
None,
|
|
button_down,
|
|
location,
|
|
button_code)
|
|
|
|
# Check if this is a double-click (same location within the last 300ms)
|
|
if _last_click["time"] is not None and datetime.datetime.now() - _last_click["time"] < datetime.timedelta(seconds=0.3) and _last_click["button"] == button and _last_click["position"] == location:
|
|
# Repeated Click
|
|
_last_click["click_count"] = min(3, _last_click["click_count"]+1)
|
|
else:
|
|
# Not a double-click - Reset last click
|
|
_last_click["click_count"] = 1
|
|
Quartz.CGEventSetIntegerValueField(
|
|
e,
|
|
Quartz.kCGMouseEventClickState,
|
|
_last_click["click_count"])
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
|
_button_state[button] = True
|
|
_last_click["time"] = datetime.datetime.now()
|
|
_last_click["button"] = button
|
|
_last_click["position"] = location
|
|
|
|
def release(button=LEFT):
|
|
""" Sends an up event for the specified button, using the provided constants """
|
|
location = get_position()
|
|
button_code, _, button_up, _ = _button_mapping[button]
|
|
e = Quartz.CGEventCreateMouseEvent(
|
|
None,
|
|
button_up,
|
|
location,
|
|
button_code)
|
|
|
|
if _last_click["time"] is not None and _last_click["time"] > datetime.datetime.now() - datetime.timedelta(microseconds=300000) and _last_click["button"] == button and _last_click["position"] == location:
|
|
# Repeated Click
|
|
Quartz.CGEventSetIntegerValueField(
|
|
e,
|
|
Quartz.kCGMouseEventClickState,
|
|
_last_click["click_count"])
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
|
_button_state[button] = False
|
|
|
|
def wheel(delta=1):
|
|
""" Sends a wheel event for the provided number of clicks. May be negative to reverse
|
|
direction. """
|
|
location = get_position()
|
|
e = Quartz.CGEventCreateMouseEvent(
|
|
None,
|
|
Quartz.kCGEventScrollWheel,
|
|
location,
|
|
Quartz.kCGMouseButtonLeft)
|
|
e2 = Quartz.CGEventCreateScrollWheelEvent(
|
|
None,
|
|
Quartz.kCGScrollEventUnitLine,
|
|
1,
|
|
delta)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2)
|
|
|
|
def move_to(x, y):
|
|
""" Sets the mouse's location to the specified coordinates. """
|
|
for b in _button_state:
|
|
if _button_state[b]:
|
|
e = Quartz.CGEventCreateMouseEvent(
|
|
None,
|
|
_button_mapping[b][3], # Drag Event
|
|
(x, y),
|
|
_button_mapping[b][0])
|
|
break
|
|
else:
|
|
e = Quartz.CGEventCreateMouseEvent(
|
|
None,
|
|
Quartz.kCGEventMouseMoved,
|
|
(x, y),
|
|
Quartz.kCGMouseButtonLeft)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
|
|
|
def get_position():
|
|
""" Returns the mouse's location as a tuple of (x, y). """
|
|
e = Quartz.CGEventCreate(None)
|
|
point = Quartz.CGEventGetLocation(e)
|
|
return (point.x, point.y) |