_pyautogui_osx.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import time
  2. import sys
  3. try:
  4. import Quartz
  5. except:
  6. assert False, "You must first install pyobjc-core and pyobjc: https://pyautogui.readthedocs.io/en/latest/install.html"
  7. import AppKit
  8. import pyautogui
  9. from pyautogui import LEFT, MIDDLE, RIGHT
  10. if sys.platform != 'darwin':
  11. raise Exception('The pyautogui_osx module should only be loaded on an OS X system.')
  12. """ Taken from events.h
  13. /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
  14. The *KB dictionaries in pyautogui map a string that can be passed to keyDown(),
  15. keyUp(), or press() into the code used for the OS-specific keyboard function.
  16. They should always be lowercase, and the same keys should be used across all OSes."""
  17. keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES])
  18. keyboardMapping.update({
  19. 'a': 0x00, # kVK_ANSI_A
  20. 's': 0x01, # kVK_ANSI_S
  21. 'd': 0x02, # kVK_ANSI_D
  22. 'f': 0x03, # kVK_ANSI_F
  23. 'h': 0x04, # kVK_ANSI_H
  24. 'g': 0x05, # kVK_ANSI_G
  25. 'z': 0x06, # kVK_ANSI_Z
  26. 'x': 0x07, # kVK_ANSI_X
  27. 'c': 0x08, # kVK_ANSI_C
  28. 'v': 0x09, # kVK_ANSI_V
  29. 'b': 0x0b, # kVK_ANSI_B
  30. 'q': 0x0c, # kVK_ANSI_Q
  31. 'w': 0x0d, # kVK_ANSI_W
  32. 'e': 0x0e, # kVK_ANSI_E
  33. 'r': 0x0f, # kVK_ANSI_R
  34. 'y': 0x10, # kVK_ANSI_Y
  35. 't': 0x11, # kVK_ANSI_T
  36. '1': 0x12, # kVK_ANSI_1
  37. '!': 0x12, # kVK_ANSI_1
  38. '2': 0x13, # kVK_ANSI_2
  39. '@': 0x13, # kVK_ANSI_2
  40. '3': 0x14, # kVK_ANSI_3
  41. '#': 0x14, # kVK_ANSI_3
  42. '4': 0x15, # kVK_ANSI_4
  43. '$': 0x15, # kVK_ANSI_4
  44. '6': 0x16, # kVK_ANSI_6
  45. '^': 0x16, # kVK_ANSI_6
  46. '5': 0x17, # kVK_ANSI_5
  47. '%': 0x17, # kVK_ANSI_5
  48. '=': 0x18, # kVK_ANSI_Equal
  49. '+': 0x18, # kVK_ANSI_Equal
  50. '9': 0x19, # kVK_ANSI_9
  51. '(': 0x19, # kVK_ANSI_9
  52. '7': 0x1a, # kVK_ANSI_7
  53. '&': 0x1a, # kVK_ANSI_7
  54. '-': 0x1b, # kVK_ANSI_Minus
  55. '_': 0x1b, # kVK_ANSI_Minus
  56. '8': 0x1c, # kVK_ANSI_8
  57. '*': 0x1c, # kVK_ANSI_8
  58. '0': 0x1d, # kVK_ANSI_0
  59. ')': 0x1d, # kVK_ANSI_0
  60. ']': 0x1e, # kVK_ANSI_RightBracket
  61. '}': 0x1e, # kVK_ANSI_RightBracket
  62. 'o': 0x1f, # kVK_ANSI_O
  63. 'u': 0x20, # kVK_ANSI_U
  64. '[': 0x21, # kVK_ANSI_LeftBracket
  65. '{': 0x21, # kVK_ANSI_LeftBracket
  66. 'i': 0x22, # kVK_ANSI_I
  67. 'p': 0x23, # kVK_ANSI_P
  68. 'l': 0x25, # kVK_ANSI_L
  69. 'j': 0x26, # kVK_ANSI_J
  70. "'": 0x27, # kVK_ANSI_Quote
  71. '"': 0x27, # kVK_ANSI_Quote
  72. 'k': 0x28, # kVK_ANSI_K
  73. ';': 0x29, # kVK_ANSI_Semicolon
  74. ':': 0x29, # kVK_ANSI_Semicolon
  75. '\\': 0x2a, # kVK_ANSI_Backslash
  76. '|': 0x2a, # kVK_ANSI_Backslash
  77. ',': 0x2b, # kVK_ANSI_Comma
  78. '<': 0x2b, # kVK_ANSI_Comma
  79. '/': 0x2c, # kVK_ANSI_Slash
  80. '?': 0x2c, # kVK_ANSI_Slash
  81. 'n': 0x2d, # kVK_ANSI_N
  82. 'm': 0x2e, # kVK_ANSI_M
  83. '.': 0x2f, # kVK_ANSI_Period
  84. '>': 0x2f, # kVK_ANSI_Period
  85. '`': 0x32, # kVK_ANSI_Grave
  86. '~': 0x32, # kVK_ANSI_Grave
  87. ' ': 0x31, # kVK_Space
  88. 'space': 0x31,
  89. '\r': 0x24, # kVK_Return
  90. '\n': 0x24, # kVK_Return
  91. 'enter': 0x24, # kVK_Return
  92. 'return': 0x24, # kVK_Return
  93. '\t': 0x30, # kVK_Tab
  94. 'tab': 0x30, # kVK_Tab
  95. 'backspace': 0x33, # kVK_Delete, which is "Backspace" on OS X.
  96. '\b': 0x33, # kVK_Delete, which is "Backspace" on OS X.
  97. 'esc': 0x35, # kVK_Escape
  98. 'escape': 0x35, # kVK_Escape
  99. 'command': 0x37, # kVK_Command
  100. 'shift': 0x38, # kVK_Shift
  101. 'shiftleft': 0x38, # kVK_Shift
  102. 'capslock': 0x39, # kVK_CapsLock
  103. 'option': 0x3a, # kVK_Option
  104. 'optionleft': 0x3a, # kVK_Option
  105. 'alt': 0x3a, # kVK_Option
  106. 'altleft': 0x3a, # kVK_Option
  107. 'ctrl': 0x3b, # kVK_Control
  108. 'ctrlleft': 0x3b, # kVK_Control
  109. 'shiftright': 0x3c, # kVK_RightShift
  110. 'optionright': 0x3d, # kVK_RightOption
  111. 'ctrlright': 0x3e, # kVK_RightControl
  112. 'fn': 0x3f, # kVK_Function
  113. 'f17': 0x40, # kVK_F17
  114. 'volumeup': 0x48, # kVK_VolumeUp
  115. 'volumedown': 0x49, # kVK_VolumeDown
  116. 'volumemute': 0x4a, # kVK_Mute
  117. 'f18': 0x4f, # kVK_F18
  118. 'f19': 0x50, # kVK_F19
  119. 'f20': 0x5a, # kVK_F20
  120. 'f5': 0x60, # kVK_F5
  121. 'f6': 0x61, # kVK_F6
  122. 'f7': 0x62, # kVK_F7
  123. 'f3': 0x63, # kVK_F3
  124. 'f8': 0x64, # kVK_F8
  125. 'f9': 0x65, # kVK_F9
  126. 'f11': 0x67, # kVK_F11
  127. 'f13': 0x69, # kVK_F13
  128. 'f16': 0x6a, # kVK_F16
  129. 'f14': 0x6b, # kVK_F14
  130. 'f10': 0x6d, # kVK_F10
  131. 'f12': 0x6f, # kVK_F12
  132. 'f15': 0x71, # kVK_F15
  133. 'help': 0x72, # kVK_Help
  134. 'home': 0x73, # kVK_Home
  135. 'pageup': 0x74, # kVK_PageUp
  136. 'pgup': 0x74, # kVK_PageUp
  137. 'del': 0x75, # kVK_ForwardDelete
  138. 'delete': 0x75, # kVK_ForwardDelete
  139. 'f4': 0x76, # kVK_F4
  140. 'end': 0x77, # kVK_End
  141. 'f2': 0x78, # kVK_F2
  142. 'pagedown': 0x79, # kVK_PageDown
  143. 'pgdn': 0x79, # kVK_PageDown
  144. 'f1': 0x7a, # kVK_F1
  145. 'left': 0x7b, # kVK_LeftArrow
  146. 'right': 0x7c, # kVK_RightArrow
  147. 'down': 0x7d, # kVK_DownArrow
  148. 'up': 0x7e, # kVK_UpArrow
  149. 'yen': 0x5d, # kVK_JIS_Yen
  150. #'underscore' : 0x5e, # kVK_JIS_Underscore (only applies to Japanese keyboards)
  151. #'comma': 0x5f, # kVK_JIS_KeypadComma (only applies to Japanese keyboards)
  152. 'eisu': 0x66, # kVK_JIS_Eisu
  153. 'kana': 0x68, # kVK_JIS_Kana
  154. })
  155. """
  156. # TODO - additional key codes to add
  157. kVK_ANSI_KeypadDecimal = 0x41,
  158. kVK_ANSI_KeypadMultiply = 0x43,
  159. kVK_ANSI_KeypadPlus = 0x45,
  160. kVK_ANSI_KeypadClear = 0x47,
  161. kVK_ANSI_KeypadDivide = 0x4B,
  162. kVK_ANSI_KeypadEnter = 0x4C,
  163. kVK_ANSI_KeypadMinus = 0x4E,
  164. kVK_ANSI_KeypadEquals = 0x51,
  165. kVK_ANSI_Keypad0 = 0x52,
  166. kVK_ANSI_Keypad1 = 0x53,
  167. kVK_ANSI_Keypad2 = 0x54,
  168. kVK_ANSI_Keypad3 = 0x55,
  169. kVK_ANSI_Keypad4 = 0x56,
  170. kVK_ANSI_Keypad5 = 0x57,
  171. kVK_ANSI_Keypad6 = 0x58,
  172. kVK_ANSI_Keypad7 = 0x59,
  173. kVK_ANSI_Keypad8 = 0x5B,
  174. kVK_ANSI_Keypad9 = 0x5C,
  175. """
  176. # add mappings for uppercase letters
  177. for c in 'abcdefghijklmnopqrstuvwxyz':
  178. keyboardMapping[c.upper()] = keyboardMapping[c]
  179. # Taken from ev_keymap.h
  180. # http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86.1/IOHIDSystem/IOKit/hidsystem/ev_keymap.h
  181. special_key_translate_table = {
  182. 'KEYTYPE_SOUND_UP': 0,
  183. 'KEYTYPE_SOUND_DOWN': 1,
  184. 'KEYTYPE_BRIGHTNESS_UP': 2,
  185. 'KEYTYPE_BRIGHTNESS_DOWN': 3,
  186. 'KEYTYPE_CAPS_LOCK': 4,
  187. 'KEYTYPE_HELP': 5,
  188. 'POWER_KEY': 6,
  189. 'KEYTYPE_MUTE': 7,
  190. 'UP_ARROW_KEY': 8,
  191. 'DOWN_ARROW_KEY': 9,
  192. 'KEYTYPE_NUM_LOCK': 10,
  193. 'KEYTYPE_CONTRAST_UP': 11,
  194. 'KEYTYPE_CONTRAST_DOWN': 12,
  195. 'KEYTYPE_LAUNCH_PANEL': 13,
  196. 'KEYTYPE_EJECT': 14,
  197. 'KEYTYPE_VIDMIRROR': 15,
  198. 'KEYTYPE_PLAY': 16,
  199. 'KEYTYPE_NEXT': 17,
  200. 'KEYTYPE_PREVIOUS': 18,
  201. 'KEYTYPE_FAST': 19,
  202. 'KEYTYPE_REWIND': 20,
  203. 'KEYTYPE_ILLUMINATION_UP': 21,
  204. 'KEYTYPE_ILLUMINATION_DOWN': 22,
  205. 'KEYTYPE_ILLUMINATION_TOGGLE': 23
  206. }
  207. def _keyDown(key):
  208. if key not in keyboardMapping or keyboardMapping[key] is None:
  209. return
  210. if key in special_key_translate_table:
  211. _specialKeyEvent(key, 'down')
  212. else:
  213. _normalKeyEvent(key, 'down')
  214. def _keyUp(key):
  215. if key not in keyboardMapping or keyboardMapping[key] is None:
  216. return
  217. if key in special_key_translate_table:
  218. _specialKeyEvent(key, 'up')
  219. else:
  220. _normalKeyEvent(key, 'up')
  221. def _normalKeyEvent(key, upDown):
  222. assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"
  223. try:
  224. if pyautogui.isShiftCharacter(key):
  225. key_code = keyboardMapping[key.lower()]
  226. event = Quartz.CGEventCreateKeyboardEvent(None,
  227. keyboardMapping['shift'], upDown == 'down')
  228. Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
  229. # Tiny sleep to let OS X catch up on us pressing shift
  230. time.sleep(pyautogui.DARWIN_CATCH_UP_TIME)
  231. else:
  232. key_code = keyboardMapping[key]
  233. event = Quartz.CGEventCreateKeyboardEvent(None, key_code, upDown == 'down')
  234. Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
  235. time.sleep(pyautogui.DARWIN_CATCH_UP_TIME)
  236. # TODO - wait, is the shift key's keyup not done?
  237. # TODO - get rid of this try-except.
  238. except KeyError:
  239. raise RuntimeError("Key %s not implemented." % (key))
  240. def _specialKeyEvent(key, upDown):
  241. """ Helper method for special keys.
  242. Source: http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac
  243. """
  244. assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"
  245. key_code = special_key_translate_table[key]
  246. ev = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
  247. Quartz.NSSystemDefined, # type
  248. (0,0), # location
  249. 0xa00 if upDown == 'down' else 0xb00, # flags
  250. 0, # timestamp
  251. 0, # window
  252. 0, # ctx
  253. 8, # subtype
  254. (key_code << 16) | ((0xa if upDown == 'down' else 0xb) << 8), # data1
  255. -1 # data2
  256. )
  257. Quartz.CGEventPost(0, ev.CGEvent())
  258. def _position():
  259. loc = AppKit.NSEvent.mouseLocation()
  260. return int(loc.x), int(Quartz.CGDisplayPixelsHigh(0) - loc.y)
  261. def _size():
  262. return Quartz.CGDisplayPixelsWide(Quartz.CGMainDisplayID()), Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID())
  263. def _scroll(clicks, x=None, y=None):
  264. _vscroll(clicks, x, y)
  265. """
  266. According to https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html#//apple_ref/c/func/Quartz.CGEventCreateScrollWheelEvent
  267. "Scrolling movement is generally represented by small signed integer values, typically in a range from -10 to +10. Large values may have unexpected results, depending on the application that processes the event."
  268. The scrolling functions will create multiple events that scroll 10 each, and then scroll the remainder.
  269. """
  270. def _vscroll(clicks, x=None, y=None):
  271. _moveTo(x, y)
  272. clicks = int(clicks)
  273. for _ in range(abs(clicks) // 10):
  274. scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
  275. None, # no source
  276. Quartz.kCGScrollEventUnitLine, # units
  277. 1, # wheelCount (number of dimensions)
  278. 10 if clicks >= 0 else -10) # vertical movement
  279. Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
  280. scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
  281. None, # no source
  282. Quartz.kCGScrollEventUnitLine, # units
  283. 1, # wheelCount (number of dimensions)
  284. clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
  285. Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
  286. def _hscroll(clicks, x=None, y=None):
  287. _moveTo(x, y)
  288. clicks = int(clicks)
  289. for _ in range(abs(clicks) // 10):
  290. scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
  291. None, # no source
  292. Quartz.kCGScrollEventUnitLine, # units
  293. 2, # wheelCount (number of dimensions)
  294. 0, # vertical movement
  295. 10 if clicks >= 0 else -10) # horizontal movement
  296. Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
  297. scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
  298. None, # no source
  299. Quartz.kCGScrollEventUnitLine, # units
  300. 2, # wheelCount (number of dimensions)
  301. 0, # vertical movement
  302. (clicks % 10) if clicks >= 0 else (-1 * clicks % 10)) # horizontal movement
  303. Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
  304. def _mouseDown(x, y, button):
  305. if button == LEFT:
  306. _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
  307. elif button == MIDDLE:
  308. _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
  309. elif button == RIGHT:
  310. _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
  311. else:
  312. assert False, "button argument not in ('left', 'middle', 'right')"
  313. def _mouseUp(x, y, button):
  314. if button == LEFT:
  315. _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
  316. elif button == MIDDLE:
  317. _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
  318. elif button == RIGHT:
  319. _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
  320. else:
  321. assert False, "button argument not in ('left', 'middle', 'right')"
  322. def _click(x, y, button):
  323. if button == LEFT:
  324. _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
  325. _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
  326. elif button == MIDDLE:
  327. _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
  328. _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
  329. elif button == RIGHT:
  330. _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
  331. _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
  332. else:
  333. assert False, "button argument not in ('left', 'middle', 'right')"
  334. _mouse_is_swapped_setting = None
  335. def _mouse_is_swapped():
  336. # TODO - for performance reasons, we only check the swapped mouse button
  337. # setting from the OS once at start up, rather than every mouse click.
  338. # This may change in the future.
  339. global _mouse_is_swapped_setting
  340. if _mouse_is_swapped_setting is None:
  341. _mouse_is_swapped_setting = False # TODO - for now, we can't detect this setting
  342. return _mouse_is_swapped_setting
  343. def _multiClick(x, y, button, num, interval=0.0):
  344. btn = None
  345. down = None
  346. up = None
  347. if button == LEFT:
  348. btn = Quartz.kCGMouseButtonLeft
  349. down = Quartz.kCGEventLeftMouseDown
  350. up = Quartz.kCGEventLeftMouseUp
  351. elif button == MIDDLE:
  352. btn = Quartz.kCGMouseButtonCenter
  353. down = Quartz.kCGEventOtherMouseDown
  354. up = Quartz.kCGEventOtherMouseUp
  355. elif button == RIGHT:
  356. btn = Quartz.kCGMouseButtonRight
  357. down = Quartz.kCGEventRightMouseDown
  358. up = Quartz.kCGEventRightMouseUp
  359. else:
  360. assert False, "button argument not in ('left', 'middle', 'right')"
  361. return
  362. for i in range(num):
  363. _click(x, y, button)
  364. time.sleep(interval)
  365. def _sendMouseEvent(ev, x, y, button):
  366. mouseEvent = Quartz.CGEventCreateMouseEvent(None, ev, (x, y), button)
  367. Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
  368. def _dragTo(x, y, button):
  369. if button == LEFT:
  370. _sendMouseEvent(Quartz.kCGEventLeftMouseDragged , x, y, Quartz.kCGMouseButtonLeft)
  371. elif button == MIDDLE:
  372. _sendMouseEvent(Quartz.kCGEventOtherMouseDragged , x, y, Quartz.kCGMouseButtonCenter)
  373. elif button == RIGHT:
  374. _sendMouseEvent(Quartz.kCGEventRightMouseDragged , x, y, Quartz.kCGMouseButtonRight)
  375. else:
  376. assert False, "button argument not in ('left', 'middle', 'right')"
  377. time.sleep(pyautogui.DARWIN_CATCH_UP_TIME) # needed to allow OS time to catch up.
  378. def _moveTo(x, y):
  379. _sendMouseEvent(Quartz.kCGEventMouseMoved, x, y, 0)
  380. time.sleep(pyautogui.DARWIN_CATCH_UP_TIME) # needed to allow OS time to catch up.