443 lines
15 KiB
Python
443 lines
15 KiB
Python
import time
|
|
import sys
|
|
|
|
try:
|
|
import Quartz
|
|
except:
|
|
assert False, "You must first install pyobjc-core and pyobjc: https://pyautogui.readthedocs.io/en/latest/install.html"
|
|
import AppKit
|
|
|
|
import pyautogui
|
|
from pyautogui import LEFT, MIDDLE, RIGHT
|
|
|
|
if sys.platform != 'darwin':
|
|
raise Exception('The pyautogui_osx module should only be loaded on an OS X system.')
|
|
|
|
|
|
|
|
""" Taken from events.h
|
|
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
|
|
|
The *KB dictionaries in pyautogui map a string that can be passed to keyDown(),
|
|
keyUp(), or press() into the code used for the OS-specific keyboard function.
|
|
|
|
They should always be lowercase, and the same keys should be used across all OSes."""
|
|
keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES])
|
|
keyboardMapping.update({
|
|
'a': 0x00, # kVK_ANSI_A
|
|
's': 0x01, # kVK_ANSI_S
|
|
'd': 0x02, # kVK_ANSI_D
|
|
'f': 0x03, # kVK_ANSI_F
|
|
'h': 0x04, # kVK_ANSI_H
|
|
'g': 0x05, # kVK_ANSI_G
|
|
'z': 0x06, # kVK_ANSI_Z
|
|
'x': 0x07, # kVK_ANSI_X
|
|
'c': 0x08, # kVK_ANSI_C
|
|
'v': 0x09, # kVK_ANSI_V
|
|
'b': 0x0b, # kVK_ANSI_B
|
|
'q': 0x0c, # kVK_ANSI_Q
|
|
'w': 0x0d, # kVK_ANSI_W
|
|
'e': 0x0e, # kVK_ANSI_E
|
|
'r': 0x0f, # kVK_ANSI_R
|
|
'y': 0x10, # kVK_ANSI_Y
|
|
't': 0x11, # kVK_ANSI_T
|
|
'1': 0x12, # kVK_ANSI_1
|
|
'!': 0x12, # kVK_ANSI_1
|
|
'2': 0x13, # kVK_ANSI_2
|
|
'@': 0x13, # kVK_ANSI_2
|
|
'3': 0x14, # kVK_ANSI_3
|
|
'#': 0x14, # kVK_ANSI_3
|
|
'4': 0x15, # kVK_ANSI_4
|
|
'$': 0x15, # kVK_ANSI_4
|
|
'6': 0x16, # kVK_ANSI_6
|
|
'^': 0x16, # kVK_ANSI_6
|
|
'5': 0x17, # kVK_ANSI_5
|
|
'%': 0x17, # kVK_ANSI_5
|
|
'=': 0x18, # kVK_ANSI_Equal
|
|
'+': 0x18, # kVK_ANSI_Equal
|
|
'9': 0x19, # kVK_ANSI_9
|
|
'(': 0x19, # kVK_ANSI_9
|
|
'7': 0x1a, # kVK_ANSI_7
|
|
'&': 0x1a, # kVK_ANSI_7
|
|
'-': 0x1b, # kVK_ANSI_Minus
|
|
'_': 0x1b, # kVK_ANSI_Minus
|
|
'8': 0x1c, # kVK_ANSI_8
|
|
'*': 0x1c, # kVK_ANSI_8
|
|
'0': 0x1d, # kVK_ANSI_0
|
|
')': 0x1d, # kVK_ANSI_0
|
|
']': 0x1e, # kVK_ANSI_RightBracket
|
|
'}': 0x1e, # kVK_ANSI_RightBracket
|
|
'o': 0x1f, # kVK_ANSI_O
|
|
'u': 0x20, # kVK_ANSI_U
|
|
'[': 0x21, # kVK_ANSI_LeftBracket
|
|
'{': 0x21, # kVK_ANSI_LeftBracket
|
|
'i': 0x22, # kVK_ANSI_I
|
|
'p': 0x23, # kVK_ANSI_P
|
|
'l': 0x25, # kVK_ANSI_L
|
|
'j': 0x26, # kVK_ANSI_J
|
|
"'": 0x27, # kVK_ANSI_Quote
|
|
'"': 0x27, # kVK_ANSI_Quote
|
|
'k': 0x28, # kVK_ANSI_K
|
|
';': 0x29, # kVK_ANSI_Semicolon
|
|
':': 0x29, # kVK_ANSI_Semicolon
|
|
'\\': 0x2a, # kVK_ANSI_Backslash
|
|
'|': 0x2a, # kVK_ANSI_Backslash
|
|
',': 0x2b, # kVK_ANSI_Comma
|
|
'<': 0x2b, # kVK_ANSI_Comma
|
|
'/': 0x2c, # kVK_ANSI_Slash
|
|
'?': 0x2c, # kVK_ANSI_Slash
|
|
'n': 0x2d, # kVK_ANSI_N
|
|
'm': 0x2e, # kVK_ANSI_M
|
|
'.': 0x2f, # kVK_ANSI_Period
|
|
'>': 0x2f, # kVK_ANSI_Period
|
|
'`': 0x32, # kVK_ANSI_Grave
|
|
'~': 0x32, # kVK_ANSI_Grave
|
|
' ': 0x31, # kVK_Space
|
|
'space': 0x31,
|
|
'\r': 0x24, # kVK_Return
|
|
'\n': 0x24, # kVK_Return
|
|
'enter': 0x24, # kVK_Return
|
|
'return': 0x24, # kVK_Return
|
|
'\t': 0x30, # kVK_Tab
|
|
'tab': 0x30, # kVK_Tab
|
|
'backspace': 0x33, # kVK_Delete, which is "Backspace" on OS X.
|
|
'\b': 0x33, # kVK_Delete, which is "Backspace" on OS X.
|
|
'esc': 0x35, # kVK_Escape
|
|
'escape': 0x35, # kVK_Escape
|
|
'command': 0x37, # kVK_Command
|
|
'shift': 0x38, # kVK_Shift
|
|
'shiftleft': 0x38, # kVK_Shift
|
|
'capslock': 0x39, # kVK_CapsLock
|
|
'option': 0x3a, # kVK_Option
|
|
'optionleft': 0x3a, # kVK_Option
|
|
'alt': 0x3a, # kVK_Option
|
|
'altleft': 0x3a, # kVK_Option
|
|
'ctrl': 0x3b, # kVK_Control
|
|
'ctrlleft': 0x3b, # kVK_Control
|
|
'shiftright': 0x3c, # kVK_RightShift
|
|
'optionright': 0x3d, # kVK_RightOption
|
|
'ctrlright': 0x3e, # kVK_RightControl
|
|
'fn': 0x3f, # kVK_Function
|
|
'f17': 0x40, # kVK_F17
|
|
'volumeup': 0x48, # kVK_VolumeUp
|
|
'volumedown': 0x49, # kVK_VolumeDown
|
|
'volumemute': 0x4a, # kVK_Mute
|
|
'f18': 0x4f, # kVK_F18
|
|
'f19': 0x50, # kVK_F19
|
|
'f20': 0x5a, # kVK_F20
|
|
'f5': 0x60, # kVK_F5
|
|
'f6': 0x61, # kVK_F6
|
|
'f7': 0x62, # kVK_F7
|
|
'f3': 0x63, # kVK_F3
|
|
'f8': 0x64, # kVK_F8
|
|
'f9': 0x65, # kVK_F9
|
|
'f11': 0x67, # kVK_F11
|
|
'f13': 0x69, # kVK_F13
|
|
'f16': 0x6a, # kVK_F16
|
|
'f14': 0x6b, # kVK_F14
|
|
'f10': 0x6d, # kVK_F10
|
|
'f12': 0x6f, # kVK_F12
|
|
'f15': 0x71, # kVK_F15
|
|
'help': 0x72, # kVK_Help
|
|
'home': 0x73, # kVK_Home
|
|
'pageup': 0x74, # kVK_PageUp
|
|
'pgup': 0x74, # kVK_PageUp
|
|
'del': 0x75, # kVK_ForwardDelete
|
|
'delete': 0x75, # kVK_ForwardDelete
|
|
'f4': 0x76, # kVK_F4
|
|
'end': 0x77, # kVK_End
|
|
'f2': 0x78, # kVK_F2
|
|
'pagedown': 0x79, # kVK_PageDown
|
|
'pgdn': 0x79, # kVK_PageDown
|
|
'f1': 0x7a, # kVK_F1
|
|
'left': 0x7b, # kVK_LeftArrow
|
|
'right': 0x7c, # kVK_RightArrow
|
|
'down': 0x7d, # kVK_DownArrow
|
|
'up': 0x7e, # kVK_UpArrow
|
|
'yen': 0x5d, # kVK_JIS_Yen
|
|
#'underscore' : 0x5e, # kVK_JIS_Underscore (only applies to Japanese keyboards)
|
|
#'comma': 0x5f, # kVK_JIS_KeypadComma (only applies to Japanese keyboards)
|
|
'eisu': 0x66, # kVK_JIS_Eisu
|
|
'kana': 0x68, # kVK_JIS_Kana
|
|
})
|
|
|
|
"""
|
|
# TODO - additional key codes to add
|
|
kVK_ANSI_KeypadDecimal = 0x41,
|
|
kVK_ANSI_KeypadMultiply = 0x43,
|
|
kVK_ANSI_KeypadPlus = 0x45,
|
|
kVK_ANSI_KeypadClear = 0x47,
|
|
kVK_ANSI_KeypadDivide = 0x4B,
|
|
kVK_ANSI_KeypadEnter = 0x4C,
|
|
kVK_ANSI_KeypadMinus = 0x4E,
|
|
kVK_ANSI_KeypadEquals = 0x51,
|
|
kVK_ANSI_Keypad0 = 0x52,
|
|
kVK_ANSI_Keypad1 = 0x53,
|
|
kVK_ANSI_Keypad2 = 0x54,
|
|
kVK_ANSI_Keypad3 = 0x55,
|
|
kVK_ANSI_Keypad4 = 0x56,
|
|
kVK_ANSI_Keypad5 = 0x57,
|
|
kVK_ANSI_Keypad6 = 0x58,
|
|
kVK_ANSI_Keypad7 = 0x59,
|
|
kVK_ANSI_Keypad8 = 0x5B,
|
|
kVK_ANSI_Keypad9 = 0x5C,
|
|
"""
|
|
|
|
# add mappings for uppercase letters
|
|
for c in 'abcdefghijklmnopqrstuvwxyz':
|
|
keyboardMapping[c.upper()] = keyboardMapping[c]
|
|
|
|
# Taken from ev_keymap.h
|
|
# http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86.1/IOHIDSystem/IOKit/hidsystem/ev_keymap.h
|
|
special_key_translate_table = {
|
|
'KEYTYPE_SOUND_UP': 0,
|
|
'KEYTYPE_SOUND_DOWN': 1,
|
|
'KEYTYPE_BRIGHTNESS_UP': 2,
|
|
'KEYTYPE_BRIGHTNESS_DOWN': 3,
|
|
'KEYTYPE_CAPS_LOCK': 4,
|
|
'KEYTYPE_HELP': 5,
|
|
'POWER_KEY': 6,
|
|
'KEYTYPE_MUTE': 7,
|
|
'UP_ARROW_KEY': 8,
|
|
'DOWN_ARROW_KEY': 9,
|
|
'KEYTYPE_NUM_LOCK': 10,
|
|
'KEYTYPE_CONTRAST_UP': 11,
|
|
'KEYTYPE_CONTRAST_DOWN': 12,
|
|
'KEYTYPE_LAUNCH_PANEL': 13,
|
|
'KEYTYPE_EJECT': 14,
|
|
'KEYTYPE_VIDMIRROR': 15,
|
|
'KEYTYPE_PLAY': 16,
|
|
'KEYTYPE_NEXT': 17,
|
|
'KEYTYPE_PREVIOUS': 18,
|
|
'KEYTYPE_FAST': 19,
|
|
'KEYTYPE_REWIND': 20,
|
|
'KEYTYPE_ILLUMINATION_UP': 21,
|
|
'KEYTYPE_ILLUMINATION_DOWN': 22,
|
|
'KEYTYPE_ILLUMINATION_TOGGLE': 23
|
|
}
|
|
|
|
def _keyDown(key):
|
|
if key not in keyboardMapping or keyboardMapping[key] is None:
|
|
return
|
|
|
|
if key in special_key_translate_table:
|
|
_specialKeyEvent(key, 'down')
|
|
else:
|
|
_normalKeyEvent(key, 'down')
|
|
|
|
def _keyUp(key):
|
|
if key not in keyboardMapping or keyboardMapping[key] is None:
|
|
return
|
|
|
|
if key in special_key_translate_table:
|
|
_specialKeyEvent(key, 'up')
|
|
else:
|
|
_normalKeyEvent(key, 'up')
|
|
|
|
|
|
def _normalKeyEvent(key, upDown):
|
|
assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"
|
|
|
|
try:
|
|
if pyautogui.isShiftCharacter(key):
|
|
key_code = keyboardMapping[key.lower()]
|
|
|
|
event = Quartz.CGEventCreateKeyboardEvent(None,
|
|
keyboardMapping['shift'], upDown == 'down')
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
|
# Tiny sleep to let OS X catch up on us pressing shift
|
|
time.sleep(0.01)
|
|
|
|
else:
|
|
key_code = keyboardMapping[key]
|
|
|
|
event = Quartz.CGEventCreateKeyboardEvent(None, key_code, upDown == 'down')
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
|
time.sleep(0.01)
|
|
|
|
# TODO - wait, is the shift key's keyup not done?
|
|
# TODO - get rid of this try-except.
|
|
|
|
except KeyError:
|
|
raise RuntimeError("Key %s not implemented." % (key))
|
|
|
|
def _specialKeyEvent(key, upDown):
|
|
""" Helper method for special keys.
|
|
|
|
Source: http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac
|
|
"""
|
|
assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"
|
|
|
|
key_code = special_key_translate_table[key]
|
|
|
|
ev = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
|
|
Quartz.NSSystemDefined, # type
|
|
(0,0), # location
|
|
0xa00 if upDown == 'down' else 0xb00, # flags
|
|
0, # timestamp
|
|
0, # window
|
|
0, # ctx
|
|
8, # subtype
|
|
(key_code << 16) | ((0xa if upDown == 'down' else 0xb) << 8), # data1
|
|
-1 # data2
|
|
)
|
|
|
|
Quartz.CGEventPost(0, ev.CGEvent())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _position():
|
|
loc = AppKit.NSEvent.mouseLocation()
|
|
return int(loc.x), int(Quartz.CGDisplayPixelsHigh(0) - loc.y)
|
|
|
|
|
|
def _size():
|
|
return Quartz.CGDisplayPixelsWide(Quartz.CGMainDisplayID()), Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID())
|
|
|
|
|
|
|
|
def _scroll(clicks, x=None, y=None):
|
|
_vscroll(clicks, x, y)
|
|
|
|
|
|
"""
|
|
According to https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html#//apple_ref/c/func/Quartz.CGEventCreateScrollWheelEvent
|
|
"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."
|
|
The scrolling functions will create multiple events that scroll 10 each, and then scroll the remainder.
|
|
"""
|
|
|
|
def _vscroll(clicks, x=None, y=None):
|
|
_moveTo(x, y)
|
|
clicks = int(clicks)
|
|
for _ in range(abs(clicks) // 10):
|
|
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
|
|
None, # no source
|
|
Quartz.kCGScrollEventUnitLine, # units
|
|
1, # wheelCount (number of dimensions)
|
|
10 if clicks >= 0 else -10) # vertical movement
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
|
|
|
|
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
|
|
None, # no source
|
|
Quartz.kCGScrollEventUnitLine, # units
|
|
1, # wheelCount (number of dimensions)
|
|
clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
|
|
|
|
|
|
def _hscroll(clicks, x=None, y=None):
|
|
_moveTo(x, y)
|
|
clicks = int(clicks)
|
|
for _ in range(abs(clicks) // 10):
|
|
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
|
|
None, # no source
|
|
Quartz.kCGScrollEventUnitLine, # units
|
|
2, # wheelCount (number of dimensions)
|
|
0, # vertical movement
|
|
10 if clicks >= 0 else -10) # horizontal movement
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
|
|
|
|
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
|
|
None, # no source
|
|
Quartz.kCGScrollEventUnitLine, # units
|
|
2, # wheelCount (number of dimensions)
|
|
0, # vertical movement
|
|
(clicks % 10) if clicks >= 0 else (-1 * clicks % 10)) # horizontal movement
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
|
|
|
|
|
|
def _mouseDown(x, y, button):
|
|
if button == LEFT:
|
|
_sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
|
|
elif button == MIDDLE:
|
|
_sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
|
|
elif button == RIGHT:
|
|
_sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
|
|
else:
|
|
assert False, "button argument not in ('left', 'middle', 'right')"
|
|
|
|
|
|
def _mouseUp(x, y, button):
|
|
if button == LEFT:
|
|
_sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
|
|
elif button == MIDDLE:
|
|
_sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
|
|
elif button == RIGHT:
|
|
_sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
|
|
else:
|
|
assert False, "button argument not in ('left', 'middle', 'right')"
|
|
|
|
|
|
def _click(x, y, button):
|
|
if button == LEFT:
|
|
_sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
|
|
_sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
|
|
elif button == MIDDLE:
|
|
_sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
|
|
_sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
|
|
elif button == RIGHT:
|
|
_sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
|
|
_sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
|
|
else:
|
|
assert False, "button argument not in ('left', 'middle', 'right')"
|
|
|
|
def _multiClick(x, y, button, num):
|
|
btn = None
|
|
down = None
|
|
up = None
|
|
|
|
if button == LEFT:
|
|
btn = Quartz.kCGMouseButtonLeft
|
|
down = Quartz.kCGEventLeftMouseDown
|
|
up = Quartz.kCGEventLeftMouseUp
|
|
elif button == MIDDLE:
|
|
btn = Quartz.kCGMouseButtonCenter
|
|
down = Quartz.kCGEventOtherMouseDown
|
|
up = Quartz.kCGEventOtherMouseUp
|
|
elif button == RIGHT:
|
|
btn = Quartz.kCGMouseButtonRight
|
|
down = Quartz.kCGEventRightMouseDown
|
|
up = Quartz.kCGEventRightMouseUp
|
|
else:
|
|
assert False, "button argument not in ('left', 'middle', 'right')"
|
|
return
|
|
|
|
mouseEvent = Quartz.CGEventCreateMouseEvent(None, down, (x, y), btn)
|
|
Quartz.CGEventSetIntegerValueField(mouseEvent, Quartz.kCGMouseEventClickState, num)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
|
|
Quartz.CGEventSetType(mouseEvent, up)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
|
|
for i in range(0, num-1):
|
|
Quartz.CGEventSetType(mouseEvent, down)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
|
|
Quartz.CGEventSetType(mouseEvent, up)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
|
|
|
|
|
|
def _sendMouseEvent(ev, x, y, button):
|
|
mouseEvent = Quartz.CGEventCreateMouseEvent(None, ev, (x, y), button)
|
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)
|
|
|
|
|
|
def _dragTo(x, y, button):
|
|
if button == LEFT:
|
|
_sendMouseEvent(Quartz.kCGEventLeftMouseDragged , x, y, Quartz.kCGMouseButtonLeft)
|
|
elif button == MIDDLE:
|
|
_sendMouseEvent(Quartz.kCGEventOtherMouseDragged , x, y, Quartz.kCGMouseButtonCenter)
|
|
elif button == RIGHT:
|
|
_sendMouseEvent(Quartz.kCGEventRightMouseDragged , x, y, Quartz.kCGMouseButtonRight)
|
|
else:
|
|
assert False, "button argument not in ('left', 'middle', 'right')"
|
|
time.sleep(0.01) # needed to allow OS time to catch up.
|
|
|
|
def _moveTo(x, y):
|
|
_sendMouseEvent(Quartz.kCGEventMouseMoved, x, y, 0)
|
|
time.sleep(0.01) # needed to allow OS time to catch up.
|
|
|