_darwinmouse.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import os
  2. import datetime
  3. import threading
  4. import Quartz
  5. from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN
  6. _button_mapping = {
  7. LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged),
  8. RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged),
  9. MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged)
  10. }
  11. _button_state = {
  12. LEFT: False,
  13. RIGHT: False,
  14. MIDDLE: False
  15. }
  16. _last_click = {
  17. "time": None,
  18. "button": None,
  19. "position": None,
  20. "click_count": 0
  21. }
  22. class MouseEventListener(object):
  23. def __init__(self, callback, blocking=False):
  24. self.blocking = blocking
  25. self.callback = callback
  26. self.listening = True
  27. def run(self):
  28. """ Creates a listener and loops while waiting for an event. Intended to run as
  29. a background thread. """
  30. self.tap = Quartz.CGEventTapCreate(
  31. Quartz.kCGSessionEventTap,
  32. Quartz.kCGHeadInsertEventTap,
  33. Quartz.kCGEventTapOptionDefault,
  34. Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) |
  35. Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) |
  36. Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) |
  37. Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) |
  38. Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) |
  39. Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) |
  40. Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) |
  41. Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel),
  42. self.handler,
  43. None)
  44. loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
  45. loop = Quartz.CFRunLoopGetCurrent()
  46. Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
  47. Quartz.CGEventTapEnable(self.tap, True)
  48. while self.listening:
  49. Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
  50. def handler(self, proxy, e_type, event, refcon):
  51. # TODO Separate event types by button/wheel/move
  52. scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
  53. key_name = name_from_scancode(scan_code)
  54. flags = Quartz.CGEventGetFlags(event)
  55. event_type = ""
  56. is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
  57. if e_type == Quartz.kCGEventKeyDown:
  58. event_type = "down"
  59. elif e_type == Quartz.kCGEventKeyUp:
  60. event_type = "up"
  61. if self.blocking:
  62. return None
  63. self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
  64. return event
  65. # Exports
  66. def init():
  67. """ Initializes mouse state """
  68. pass
  69. def listen(queue):
  70. """ Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """
  71. if not os.geteuid() == 0:
  72. raise OSError("Error 13 - Must be run as administrator")
  73. listener = MouseEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP))
  74. t = threading.Thread(target=listener.run, args=())
  75. t.daemon = True
  76. t.start()
  77. def press(button=LEFT):
  78. """ Sends a down event for the specified button, using the provided constants """
  79. location = get_position()
  80. button_code, button_down, _, _ = _button_mapping[button]
  81. e = Quartz.CGEventCreateMouseEvent(
  82. None,
  83. button_down,
  84. location,
  85. button_code)
  86. # Check if this is a double-click (same location within the last 300ms)
  87. 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:
  88. # Repeated Click
  89. _last_click["click_count"] = min(3, _last_click["click_count"]+1)
  90. else:
  91. # Not a double-click - Reset last click
  92. _last_click["click_count"] = 1
  93. Quartz.CGEventSetIntegerValueField(
  94. e,
  95. Quartz.kCGMouseEventClickState,
  96. _last_click["click_count"])
  97. Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
  98. _button_state[button] = True
  99. _last_click["time"] = datetime.datetime.now()
  100. _last_click["button"] = button
  101. _last_click["position"] = location
  102. def release(button=LEFT):
  103. """ Sends an up event for the specified button, using the provided constants """
  104. location = get_position()
  105. button_code, _, button_up, _ = _button_mapping[button]
  106. e = Quartz.CGEventCreateMouseEvent(
  107. None,
  108. button_up,
  109. location,
  110. button_code)
  111. 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:
  112. # Repeated Click
  113. Quartz.CGEventSetIntegerValueField(
  114. e,
  115. Quartz.kCGMouseEventClickState,
  116. _last_click["click_count"])
  117. Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
  118. _button_state[button] = False
  119. def wheel(delta=1):
  120. """ Sends a wheel event for the provided number of clicks. May be negative to reverse
  121. direction. """
  122. location = get_position()
  123. e = Quartz.CGEventCreateMouseEvent(
  124. None,
  125. Quartz.kCGEventScrollWheel,
  126. location,
  127. Quartz.kCGMouseButtonLeft)
  128. e2 = Quartz.CGEventCreateScrollWheelEvent(
  129. None,
  130. Quartz.kCGScrollEventUnitLine,
  131. 1,
  132. delta)
  133. Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
  134. Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2)
  135. def move_to(x, y):
  136. """ Sets the mouse's location to the specified coordinates. """
  137. for b in _button_state:
  138. if _button_state[b]:
  139. e = Quartz.CGEventCreateMouseEvent(
  140. None,
  141. _button_mapping[b][3], # Drag Event
  142. (x, y),
  143. _button_mapping[b][0])
  144. break
  145. else:
  146. e = Quartz.CGEventCreateMouseEvent(
  147. None,
  148. Quartz.kCGEventMouseMoved,
  149. (x, y),
  150. Quartz.kCGMouseButtonLeft)
  151. Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
  152. def get_position():
  153. """ Returns the mouse's location as a tuple of (x, y). """
  154. e = Quartz.CGEventCreate(None)
  155. point = Quartz.CGEventGetLocation(e)
  156. return (point.x, point.y)