You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
17 KiB
532 lines
17 KiB
# -*- coding: utf-8 -*-
|
|
# GUI Application automation and testing library
|
|
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
|
|
# https://github.com/pywinauto/pywinauto/graphs/contributors
|
|
# http://pywinauto.readthedocs.io/en/latest/credits.html
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice, this
|
|
# list of conditions and the following disclaimer.
|
|
#
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# * Neither the name of pywinauto nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
"""Linux/Unix branch of the keyboard module
|
|
|
|
It allows to send keystrokes to the active display using python-xlib library.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
from Xlib.display import Display
|
|
from Xlib import X
|
|
from Xlib.ext.xtest import fake_input
|
|
import Xlib.XK
|
|
import time
|
|
import six
|
|
|
|
_display = Display()
|
|
|
|
DEBUG = 0
|
|
|
|
spec_keysyms = {
|
|
' ': "space",
|
|
'\t': "Tab",
|
|
'\n': "Return", # for some reason this needs to be cr, not lf
|
|
'\r': "Return",
|
|
'\e': "Escape",
|
|
'!': "exclam",
|
|
'#': "numbersign",
|
|
'%': "percent",
|
|
'$': "dollar",
|
|
'&': "ampersand",
|
|
'"': "quotedbl",
|
|
'\'': "apostrophe",
|
|
'(': "parenleft",
|
|
')': "parenright",
|
|
'*': "asterisk",
|
|
'=': "equal",
|
|
'+': "plus",
|
|
',': "comma",
|
|
'-': "minus",
|
|
'.': "period",
|
|
'/': "slash",
|
|
':': "colon",
|
|
';': "semicolon",
|
|
'<': "less",
|
|
'>': "greater",
|
|
'?': "question",
|
|
'@': "at",
|
|
'[': "bracketleft",
|
|
']': "bracketright",
|
|
'\\': "backslash",
|
|
'^': "asciicircum",
|
|
'_': "underscore",
|
|
'`': "grave",
|
|
'{': "braceleft",
|
|
'|': "bar",
|
|
'}': "braceright",
|
|
'~': "asciitilde"
|
|
}
|
|
|
|
|
|
def _to_keycode(key):
|
|
"""return python X11 keycode of symbol"""
|
|
return _display.keysym_to_keycode(Xlib.XK.string_to_keysym(key))
|
|
|
|
INPUT_KEYBOARD = 1
|
|
KEYEVENTF_EXTENDEDKEY = 1
|
|
KEYEVENTF_KEYUP = 2
|
|
KEYEVENTF_UNICODE = 4
|
|
KEYEVENTF_SCANCODE = 8
|
|
VK_SHIFT = _to_keycode('Shift_L')
|
|
VK_CONTROL = _to_keycode('Control_L')
|
|
VK_MENU = _to_keycode('Menu')
|
|
|
|
# 'codes' recognized as {CODE repeat)?}
|
|
|
|
|
|
CODES = {
|
|
'BACK': _to_keycode('BackSpace'),
|
|
'BACKSPACE': _to_keycode('BackSpace'),
|
|
'BKSP': _to_keycode('BackSpace'),
|
|
'BREAK': _to_keycode('Break'),
|
|
'BS': _to_keycode('BackSpace'),
|
|
'CAP': _to_keycode('Caps_Lock'),
|
|
'CAPSLOCK': _to_keycode('Caps_Lock'),
|
|
'DEL': _to_keycode('Delete'),
|
|
'DELETE': _to_keycode('Delete'),
|
|
'DOWN': _to_keycode('Down'),
|
|
'END': _to_keycode('End'),
|
|
'ENTER': _to_keycode('Return'),
|
|
'ESC': _to_keycode('Escape'),
|
|
'F1': _to_keycode('F1'),
|
|
'F2': _to_keycode('F2'),
|
|
'F3': _to_keycode('F3'),
|
|
'F4': _to_keycode('F4'),
|
|
'F5': _to_keycode('F5'),
|
|
'F6': _to_keycode('F6'),
|
|
'F7': _to_keycode('F7'),
|
|
'F8': _to_keycode('F8'),
|
|
'F9': _to_keycode('F9'),
|
|
'F10': _to_keycode('F10'),
|
|
'F11': _to_keycode('F11'),
|
|
'F12': _to_keycode('F12'),
|
|
'F13': _to_keycode('F13'),
|
|
'F14': _to_keycode('F14'),
|
|
'F15': _to_keycode('F15'),
|
|
'F16': _to_keycode('F16'),
|
|
'F17': _to_keycode('F17'),
|
|
'F18': _to_keycode('F18'),
|
|
'F19': _to_keycode('F19'),
|
|
'F20': _to_keycode('F20'),
|
|
'F21': _to_keycode('F21'),
|
|
'F22': _to_keycode('F22'),
|
|
'F23': _to_keycode('F23'),
|
|
'F24': _to_keycode('F24'),
|
|
'HELP': _to_keycode('Help'),
|
|
'HOME': _to_keycode('Home'),
|
|
'INS': _to_keycode('Insert'),
|
|
'INSERT': _to_keycode('Insert'),
|
|
'LEFT': _to_keycode('Left'),
|
|
'LWIN': _to_keycode('Super_L'),
|
|
'NUMLOCK': _to_keycode('Num_Lock'),
|
|
'PGDN': _to_keycode('Page_Down'),
|
|
'PGUP': _to_keycode('Page_Up'),
|
|
'PRTSC': _to_keycode('Print'),
|
|
'RIGHT': _to_keycode('Right'),
|
|
'RMENU': _to_keycode('Alt_R'),
|
|
'RWIN': _to_keycode('Super_R'),
|
|
'SCROLLLOCK': _to_keycode('Scroll_Lock'),
|
|
'SPACE': _to_keycode('space'),
|
|
'TAB': _to_keycode('Tab'),
|
|
'UP': _to_keycode('Up'),
|
|
|
|
'VK_ACCEPT': 30,
|
|
'VK_ADD': 107,
|
|
'VK_APPS': 93,
|
|
'VK_ATTN': 246,
|
|
'VK_BACK': _to_keycode('BackSpace'),
|
|
'VK_CANCEL': _to_keycode('Break'),
|
|
'VK_CAPITAL': _to_keycode('Caps_Lock'),
|
|
'VK_CLEAR': 12,
|
|
'VK_CONTROL': _to_keycode('Control_L'),
|
|
'VK_CONVERT': 28,
|
|
'VK_CRSEL': 247,
|
|
'VK_DECIMAL': 110,
|
|
'VK_DELETE': _to_keycode('Delete'),
|
|
'VK_DIVIDE': 111,
|
|
'VK_DOWN': _to_keycode('Down'),
|
|
'VK_END': _to_keycode('End'),
|
|
'VK_EREOF': 249,
|
|
'VK_ESCAPE': _to_keycode('Escape'),
|
|
'VK_EXECUTE': 43,
|
|
'VK_EXSEL': 248,
|
|
'VK_F1': _to_keycode('F1'),
|
|
'VK_F2': _to_keycode('F2'),
|
|
'VK_F3': _to_keycode('F3'),
|
|
'VK_F4': _to_keycode('F4'),
|
|
'VK_F5': _to_keycode('F5'),
|
|
'VK_F6': _to_keycode('F6'),
|
|
'VK_F7': _to_keycode('F7'),
|
|
'VK_F8': _to_keycode('F8'),
|
|
'VK_F9': _to_keycode('F9'),
|
|
'VK_F10': _to_keycode('F10'),
|
|
'VK_F11': _to_keycode('F11'),
|
|
'VK_F12': _to_keycode('F12'),
|
|
'VK_F13': _to_keycode('F13'),
|
|
'VK_F14': _to_keycode('F14'),
|
|
'VK_F15': _to_keycode('F15'),
|
|
'VK_F16': _to_keycode('F16'),
|
|
'VK_F17': _to_keycode('F17'),
|
|
'VK_F18': _to_keycode('F18'),
|
|
'VK_F19': _to_keycode('F19'),
|
|
'VK_F20': _to_keycode('F20'),
|
|
'VK_F21': _to_keycode('F21'),
|
|
'VK_F22': _to_keycode('F22'),
|
|
'VK_F23': _to_keycode('F23'),
|
|
'VK_F24': _to_keycode('F24'),
|
|
'VK_FINAL': 24,
|
|
'VK_HANGEUL': 21,
|
|
'VK_HANGUL': 21,
|
|
'VK_HANJA': 25,
|
|
'VK_HELP': _to_keycode('Help'),
|
|
'VK_HOME': _to_keycode('Home'),
|
|
'VK_INSERT': _to_keycode('Insert'),
|
|
'VK_JUNJA': 23,
|
|
'VK_KANA': 21,
|
|
'VK_KANJI': 25,
|
|
'VK_LBUTTON': 1,
|
|
'VK_LCONTROL': _to_keycode('Control_L'),
|
|
'VK_LEFT': _to_keycode('Left'),
|
|
'VK_LMENU': _to_keycode('Alt_L'),
|
|
'VK_LSHIFT': _to_keycode('Shift_L'),
|
|
'VK_LWIN': _to_keycode('Super_L'),
|
|
'VK_MBUTTON': 4,
|
|
'VK_MENU': _to_keycode('Alt_L'),
|
|
'VK_MODECHANGE': 31,
|
|
'VK_MULTIPLY': 106,
|
|
'VK_NEXT': _to_keycode('Page_Down'),
|
|
'VK_NONAME': 252,
|
|
'VK_NONCONVERT': 29,
|
|
'VK_NUMLOCK': _to_keycode('Num_Lock'),
|
|
'VK_NUMPAD0': _to_keycode('KP_0'),
|
|
'VK_NUMPAD1': _to_keycode('KP_1'),
|
|
'VK_NUMPAD2': _to_keycode('KP_2'),
|
|
'VK_NUMPAD3': _to_keycode('KP_3'),
|
|
'VK_NUMPAD4': _to_keycode('KP_4'),
|
|
'VK_NUMPAD5': _to_keycode('KP_5'),
|
|
'VK_NUMPAD6': _to_keycode('KP_6'),
|
|
'VK_NUMPAD7': _to_keycode('KP_7'),
|
|
'VK_NUMPAD8': _to_keycode('KP_8'),
|
|
'VK_NUMPAD9': _to_keycode('KP_9'),
|
|
'VK_OEM_CLEAR': 254,
|
|
'VK_PA1': 253,
|
|
'VK_PAUSE': 19,
|
|
'VK_PLAY': 250,
|
|
'VK_PRINT': _to_keycode('Print'),
|
|
'VK_PRIOR': _to_keycode('Page_Up'),
|
|
'VK_PROCESSKEY': 229,
|
|
'VK_RBUTTON': 2,
|
|
'VK_RCONTROL': _to_keycode('Control_R'),
|
|
'VK_RETURN': _to_keycode('Return'),
|
|
'VK_RIGHT': _to_keycode('Right'),
|
|
'VK_RMENU': _to_keycode('Alt_R'),
|
|
'VK_RSHIFT': _to_keycode('Shift_R'),
|
|
'VK_RWIN': _to_keycode('Super_R'),
|
|
'VK_SCROLL': _to_keycode('Scroll_Lock'),
|
|
'VK_SELECT': 41,
|
|
'VK_SEPARATOR': 108,
|
|
'VK_SHIFT': _to_keycode('Shift_L'),
|
|
'VK_SNAPSHOT': _to_keycode('Print'),
|
|
'VK_SPACE': _to_keycode('Space'),
|
|
'VK_SUBTRACT': 109,
|
|
'VK_TAB': _to_keycode('Tab'),
|
|
'VK_UP': _to_keycode('Up'),
|
|
'ZOOM': 251, #no item in xlib
|
|
}
|
|
|
|
# modifier keys
|
|
MODIFIERS = {
|
|
'+': VK_SHIFT,
|
|
'^': VK_CONTROL,
|
|
'%': VK_MENU,
|
|
}
|
|
|
|
|
|
class KeySequenceError(Exception):
|
|
|
|
"""Exception raised when a key sequence string has a syntax error"""
|
|
|
|
def __str__(self):
|
|
return ' '.join(self.args)
|
|
|
|
|
|
class KeyAction(object):
|
|
|
|
"""
|
|
Class that represents a single 'keyboard' action
|
|
|
|
It represents either a PAUSE action (not reallly keyboard) or a keyboard
|
|
action (press or release or both) of a particular key.
|
|
"""
|
|
|
|
def __init__(self, key, down = True, up = True):
|
|
"""Init a single key action params"""
|
|
self.key = key
|
|
self.down = down
|
|
self.up = up
|
|
self.ctrl = False
|
|
self.alt = False
|
|
self.shift = False
|
|
self.is_shifted = False
|
|
|
|
@staticmethod
|
|
def _key_modifiers(ctrl, shift, alt, action = X.KeyPress):
|
|
"""Apply key modifiers"""
|
|
if ctrl:
|
|
fake_input(_display, action, CODES['VK_CONTROL'])
|
|
if shift:
|
|
fake_input(_display, action, CODES['VK_SHIFT'])
|
|
if alt:
|
|
fake_input(_display, action, CODES['VK_MENU'])
|
|
|
|
def run(self):
|
|
"""Do a single keyboard action using xlib"""
|
|
if isinstance(self.key, six.string_types):
|
|
key = self.key
|
|
self.key = Xlib.XK.string_to_keysym(self.key)
|
|
if self.key == 0:
|
|
self.key = Xlib.XK.string_to_keysym(spec_keysyms[key])
|
|
self.key = _display.keysym_to_keycode(self.key)
|
|
if self.key == 0:
|
|
raise RuntimeError('Key {} not found!'.format(self.key))
|
|
self.is_shifted = key.isupper() or key in '~!@#$%^&*()_+{}|:"<>?'
|
|
elif not isinstance(self.key, six.integer_types):
|
|
raise TypeError('self.key = {} is not a string or integer'.format(self.key))
|
|
|
|
self._key_modifiers(self.ctrl, (self.shift or self.is_shifted),
|
|
self.alt, action = X.KeyPress)
|
|
if self.down:
|
|
fake_input(_display, X.KeyPress, self.key)
|
|
_display.sync()
|
|
if self.up:
|
|
fake_input(_display, X.KeyRelease, self.key)
|
|
_display.sync()
|
|
self._key_modifiers(self.ctrl, (self.shift or self.is_shifted),
|
|
self.alt, action = X.KeyRelease)
|
|
_display.sync()
|
|
|
|
def _get_down_up_string(self):
|
|
"""Return a string that will show whether the string is up or down
|
|
|
|
return 'down' if the key is a press only
|
|
return 'up' if the key is up only
|
|
return '' if the key is up & down (as default)
|
|
"""
|
|
if self.down and self.up:
|
|
return ""
|
|
if self.down:
|
|
return "down"
|
|
if self.up:
|
|
return "up"
|
|
return "" # TODO: raise RuntimeError('Nor "down" or "up" action specified!')
|
|
|
|
def key_description(self):
|
|
"""Return a description of the key"""
|
|
return "{}".format(self.key)
|
|
|
|
def __str__(self):
|
|
"""Return key with modifiers as a string"""
|
|
parts = []
|
|
parts.append(self.key_description())
|
|
up_down = self._get_down_up_string()
|
|
if up_down:
|
|
parts.append(up_down)
|
|
|
|
return "<{}>".format(" ".join(parts))
|
|
__repr__ = __str__
|
|
|
|
|
|
class PauseAction(KeyAction):
|
|
|
|
"""Represents a pause action"""
|
|
|
|
def __init__(self, how_long):
|
|
self.how_long = how_long
|
|
|
|
def run(self):
|
|
"""Pause for the lenght of time specified"""
|
|
time.sleep(self.how_long)
|
|
|
|
def __str__(self):
|
|
return "<PAUSE {}>".format(self.how_long)
|
|
__repr__ = __str__
|
|
|
|
|
|
def handle_code(code):
|
|
"""Handle a key or sequence of keys in braces"""
|
|
code_keys = []
|
|
# it is a known code (e.g. {DOWN}, {ENTER}, etc)
|
|
if code in CODES:
|
|
code_keys.append(KeyAction(CODES[code]))
|
|
|
|
# it is an escaped modifier e.g. {%}, {^}, {+}
|
|
elif len(code) == 1:
|
|
code_keys.append(KeyAction(code))
|
|
|
|
# it is a repetition or a pause {DOWN 5}, {PAUSE 1.3}
|
|
elif ' ' in code:
|
|
to_repeat, count = code.rsplit(None, 1)
|
|
if to_repeat == "PAUSE":
|
|
try:
|
|
pause_time = float(count)
|
|
except ValueError:
|
|
raise KeySequenceError('invalid pause time {}'.format(count))
|
|
code_keys.append(PauseAction(pause_time))
|
|
|
|
else:
|
|
try:
|
|
count = int(count)
|
|
except ValueError:
|
|
raise KeySequenceError(
|
|
'invalid repetition count {}'.format(count))
|
|
|
|
# If the value in to_repeat is a VK e.g. DOWN
|
|
# we need to add the code repeated
|
|
if to_repeat in CODES:
|
|
code_keys.extend(
|
|
[KeyAction(CODES[to_repeat])] * count)
|
|
# otherwise parse the keys and we get back a KeyAction
|
|
else:
|
|
to_repeat = parse_keys(to_repeat)
|
|
if isinstance(to_repeat, list):
|
|
keys = to_repeat * count
|
|
else:
|
|
keys = [to_repeat] * count
|
|
code_keys.extend(keys)
|
|
else:
|
|
raise RuntimeError("Unknown code: {}".format(code))
|
|
|
|
return code_keys
|
|
|
|
|
|
def parse_keys(string,
|
|
with_spaces = False,
|
|
with_tabs = False,
|
|
with_newlines = False,
|
|
modifiers = None):
|
|
"""Return the parsed keys"""
|
|
keys = []
|
|
if not modifiers:
|
|
modifiers = []
|
|
index = 0
|
|
while index < len(string):
|
|
|
|
c = string[index]
|
|
index += 1
|
|
# check if one of CTRL, SHIFT, ALT has been pressed
|
|
if c in MODIFIERS.keys():
|
|
# remember that we are currently modified
|
|
modifiers.append(c)
|
|
# hold down the modifier key
|
|
#keys.append(KeyAction(modifier, up = False))
|
|
if DEBUG:
|
|
print("MODS+", modifiers)
|
|
continue
|
|
|
|
# Apply modifiers over a bunch of characters (not just one!)
|
|
elif c == "(":
|
|
# find the end of the bracketed text
|
|
end_pos = string.find(")", index)
|
|
if end_pos == -1:
|
|
raise KeySequenceError('`)` not found')
|
|
keys.extend(
|
|
parse_keys(string[index:end_pos], modifiers = modifiers))
|
|
index = end_pos + 1
|
|
|
|
# Escape or named key
|
|
elif c == "{":
|
|
# We start searching from index + 1 to account for the case {}}
|
|
end_pos = string.find("}", index+1)
|
|
if end_pos == -1:
|
|
raise KeySequenceError('`}` not found')
|
|
|
|
code = string[index:end_pos]
|
|
index = end_pos + 1
|
|
keys.extend(handle_code(code))
|
|
|
|
# unmatched ")"
|
|
elif c == ')':
|
|
raise KeySequenceError('`)` should be preceeded by `(`')
|
|
|
|
# unmatched "}"
|
|
elif c == '}':
|
|
raise KeySequenceError('`}` should be preceeded by `{`')
|
|
|
|
# so it is a normal character
|
|
else:
|
|
# don't output white space unless flags to output have been set
|
|
if (c == ' ' and not with_spaces or
|
|
c == '\t' and not with_tabs or
|
|
c == '\n' and not with_newlines):
|
|
continue
|
|
|
|
# output newline
|
|
if c in ('~', '\n'):
|
|
keys.append(KeyAction(CODES["ENTER"]))
|
|
|
|
elif modifiers:
|
|
keys.append(KeyAction(c))
|
|
|
|
else:
|
|
keys.append(KeyAction(c))
|
|
|
|
# as we have handled the text - release the modifiers
|
|
while modifiers:
|
|
if DEBUG:
|
|
print("MODS-", modifiers)
|
|
mod = modifiers.pop()
|
|
if mod == '+':
|
|
keys[-1].shift = True
|
|
elif mod == '^':
|
|
keys[-1].ctrl = True
|
|
elif mod == '%':
|
|
keys[-1].alt = True
|
|
|
|
# just in case there were any modifiers left pressed - release them
|
|
while modifiers:
|
|
keys.append(KeyAction(modifiers.pop(), down = False))
|
|
return keys
|
|
|
|
|
|
def send_keys(keys,
|
|
pause=0.05,
|
|
with_spaces=False,
|
|
with_tabs=False,
|
|
with_newlines=False,
|
|
turn_off_numlock=True):
|
|
"""Parse the keys and type them"""
|
|
keys = parse_keys(keys, with_spaces, with_tabs, with_newlines)
|
|
for k in keys:
|
|
k.run()
|
|
time.sleep(pause)
|