Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(289)

Side by Side Diff: src/platform/autox/autox.py

Issue 1148006: autox: port to Python. (Closed)
Patch Set: fix license (Chromium -> Chromium OS) Created 10 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
6
7 import re
8 import time
9
10 import Xlib.display
11 import Xlib.protocol.request
12 from Xlib import X
13 from Xlib import XK
14 from Xlib.ext import xtest
15
16 class AutoX(object):
17 """AutoX provides an interface for interacting with X applications.
18
19 This is done by using the XTEST extension to inject events into the X
20 server.
21
22 Example usage:
23
24 autox = AutoX()
25 autox.move_pointer(200, 100)
26 autox.press_button(1)
27 autox.move_pointer(250, 150)
28 autox.release_button(1)
29
30 autox.send_hotkey("Ctrl+Alt+L")
31 autox.send_text("this is my password\n")
32 """
33
34 # Map of characters that can be passed to send_text() that differ
35 # from their X keysym names.
36 __chars_to_keysyms = {
37 ' ': 'space',
38 '\n': 'Return',
39 '\t': 'Tab',
40 '~': 'asciitilde',
41 '!': 'exclam',
42 '@': 'at',
43 '#': 'numbersign',
44 '$': 'dollar',
45 '%': 'percent',
46 '^': 'asciicircum',
47 '&': 'ampersand',
48 '*': 'asterisk',
49 '(': 'parenleft',
50 ')': 'parenright',
51 '-': 'minus',
52 '_': 'underscore',
53 '+': 'plus',
54 '=': 'equal',
55 '{': 'braceleft',
56 '[': 'bracketleft',
57 '}': 'braceright',
58 ']': 'bracketright',
59 '|': 'bar',
60 ':': 'colon',
61 ';': 'semicolon',
62 '"': 'quotedbl',
63 '\'': 'apostrophe',
64 ',': 'comma',
65 '<': 'less',
66 '.': 'period',
67 '>': 'greater',
68 '/': 'slash',
69 '?': 'question',
70 }
71
72 class Error(Exception):
73 """Base exception class for AutoX."""
74 pass
75
76 class RuntimeError(Error):
77 """Error caused by a (possibly temporary) condition at runtime."""
78 pass
79
80 class InputError(Error):
81 """Error caused by invalid input from the caller."""
82 pass
83
84 class InvalidKeySymError(InputError):
85 """Error caused by the caller referencing an invalid keysym."""
86 def __init__(self, keysym):
87 self.__keysym = keysym
88
89 def __str__(self):
90 return "Invalid keysym \"%s\"" % self.__keysym
91
92 def __init__(self, display_name=None):
93 self.__display = Xlib.display.Display(display_name)
94 self.__root = self.__display.screen().root
95
96 def __get_keycode_for_keysym(self, keysym):
97 """Get the keycode corresponding to a keysym.
98
99 Args:
100 keysym: keysym name as str
101
102 Returns:
103 integer keycode
104
105 Raises:
106 InvalidKeySymError: keysym name isn't an actual keycode
107 RuntimeError: unable to map the keysym to a keycode (maybe it
108 isn't present in the current keymap)
109 """
110 keysym_num = XK.string_to_keysym(keysym)
111 if keysym_num == XK.NoSymbol:
112 raise self.InvalidKeySymError(keysym)
113 keycode = self.__display.keysym_to_keycode(keysym_num)
114 if not keycode:
115 raise self.RuntimeError(
116 'Unable to map keysym "%s" to a keycode' % keysym)
117 return keycode
118
119 def __keysym_requires_shift(self, keysym):
120 """Does a keysym require that a shift key be held down?
121
122 Args:
123 keysym: keysym name as str
124
125 Returns:
126 True or False
127
128 Raises:
129 InvalidKeySymError: keysym name isn't an actual keycode
130 RuntimeError: unable to map the keysym to a keycode (maybe it
131 isn't present in the current keymap)
132 """
133 keysym_num = XK.string_to_keysym(keysym)
134 if keysym_num == XK.NoSymbol:
135 raise self.InvalidKeySymError(keysym)
136 # This gives us a list of (keycode, index) tuples, sorted by index and
137 # then by keycode. Index 0 is without any modifiers, 1 is with Shift,
138 # 2 is with Mode_switch, and 3 is Mode_switch and Shift.
139 keycodes = self.__display.keysym_to_keycodes(keysym_num)
140 if not keycodes:
141 raise self.RuntimeError(
142 'Unable to map keysym "%s" to a keycode' % keysym)
143 # We don't use Mode_switch for anything, at least currently, so just
144 # check if the first index is unshifted.
145 return keycodes[0][1] != 0
146
147 def __handle_key_command(keysym, key_press):
148 """Looks up the keycode for a keysym and presses or releases it.
149
150 Helper method for press_key() and release_key().
151
152 Args:
153 keysym: keysym name as str
154 key_press: True to send key press; False to send release
155
156 Raises:
157 InputError: input was invalid; details in exception
158 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym()
159 """
160 keycode = self.__get_keycode_for_keysym(keysym)
161 if self.__keysym_requires_shift(keysym):
162 raise self.InputError(
163 'Keysym "%s" requires the Shift key to be held. Either use '
164 'send_text() or make separate calls to press/release_key(), '
165 'one for Shift_L and then one for the keycode\'s non-shifted '
166 'keysym' % keysym)
167
168 type = X.KeyPress if key_press else X.KeyRelease
169 xtest.fake_input(self.__display, type, detail=keycode)
170 self.__display.sync()
171
172 def __convert_escaped_string_to_keysym(self, escaped_string):
173 """Read an escaped keysym name from the beginning of a string.
174
175 Helper method called by send_text().
176
177 Args:
178 escaped_string: str prefixed with a backslash followed by a
179 keysym name in parens, e.g. "\\(Return)more text"
180
181 Returns:
182 tuple consisting of the keysym name and the number of
183 characters that should be skipped to get to the next character
184 in the string (including the leading backslash). For example,
185 "\\(Space)blah" yields ("Space", 8).
186
187 Raises:
188 InputError: unable to find an escaped keysym-looking thing at
189 the beginning of the string
190 """
191 if escaped_string[0] != '\\':
192 raise self.InputError('Escaped string is missing backslash')
193 if len(escaped_string) < 2:
194 raise self.InputError('Escaped string is too short')
195 if escaped_string[1] == '\\':
196 return ('backslash', 2)
197 if escaped_string[1] != '(':
198 raise self.InputError('Escaped string is missing opening paren')
199
200 end_index = escaped_string.find(')')
201 if end_index == -1 or end_index == 2:
202 raise self.InputError('Escaped string is missing closing paren')
203 return (escaped_string[2:end_index], end_index + 1)
204
205 def __convert_char_to_keysym(self, char):
206 """Convert a character into its keysym name.
207
208 Args:
209 char: str of length 1 containing the character to be looked up
210
211 Returns:
212 keysym name as str
213
214 Raises:
215 InputError: received non-length-1 string
216 InvalidKeySymError: character wasn't a keysym that we know about
217 (this may just mean that it needs to be added to
218 '__chars_to_keysyms')
219 """
220 if len(char) != 1:
221 raise self.InputError('Got non-length-1 string "%s"' % char)
222 if char.isalnum():
223 # Letters and digits are easy.
224 return char
225 if char in AutoX.__chars_to_keysyms:
226 return AutoX.__chars_to_keysyms[char]
227 raise self.InvalidKeySymError(char)
228
229 def get_pointer_position(self):
230 """Get the pointer's absolute position.
231
232 Returns:
233 (x, y) integer tuple
234 """
235 reply = Xlib.protocol.request.QueryPointer(
236 display=self.__display.display, window=self.__root)
237 return (reply.root_x, reply.root_y)
238
239 def press_button(self, button):
240 """Press a mouse button.
241
242 Args:
243 button: 1-indexed mouse button to press
244 """
245 xtest.fake_input(self.__display, X.ButtonPress, detail=button)
246 self.__display.sync()
247
248 def release_button(self, button):
249 """Release a mouse button.
250
251 Args:
252 button: 1-indexed mouse button to release
253 """
254 xtest.fake_input(self.__display, X.ButtonRelease, detail=button)
255 self.__display.sync()
256
257 def move_pointer(self, x, y):
258 """Move the mouse pointer to an absolute position.
259
260 Args:
261 x, y: integer position relative to the root window's origin
262 """
263 xtest.fake_input(self.__display, X.MotionNotify, x=x, y=y)
264 self.__display.sync()
265
266 def send_hotkey(self, hotkey):
267 """Send a combination of keystrokes.
268
269 Args:
270 hotkey: str describing a '+' or '-'-separated sequence of
271 keysyms, e.g. "Control_L+Alt_L+R" or "Ctrl-J". Several
272 aliases are accepted:
273
274 Ctrl -> Control_L
275 Alt -> Alt_L
276 Shift -> Shift_L
277
278 Whitespace is permitted around individual keysyms.
279
280 Raises:
281 InputError: hotkey sequence contained an error
282 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym()
283 """
284 # Did the shift key occur in the combination?
285 saw_shift = False
286 keycodes = []
287
288 regexp = re.compile('[-+]')
289 for keysym in regexp.split(hotkey):
290 keysym = keysym.strip()
291
292 if keysym == 'Ctrl':
293 keysym = 'Control_L'
294 elif keysym == 'Alt':
295 keysym = 'Alt_L'
296 elif keysym == 'Shift':
297 keysym = 'Shift_L'
298
299 if keysym == 'Shift_L' or keysym == 'Shift_R':
300 saw_shift = True
301
302 keycode = self.__get_keycode_for_keysym(keysym)
303
304 # Bail if we're being asked to press a key that requires Shift and
305 # the Shift key wasn't pressed already (but let it slide if they're
306 # just asking for an uppercase letter).
307 if self.__keysym_requires_shift(keysym) and not saw_shift and \
308 (len(keysym) != 1 or keysym < 'A' or keysym > 'Z'):
309 raise self.InputError(
310 'Keysym "%s" requires the Shift key to be held, '
311 'but it wasn\'t seen earlier in the key combo. '
312 'Either press Shift first or using the keycode\'s '
313 'non-shifted keysym instead' % keysym)
314
315 keycodes.append(keycode)
316
317 # Press the keys in the correct order and then reverse them in the
318 # opposite order.
319 for keycode in keycodes:
320 xtest.fake_input(self.__display, X.KeyPress, detail=keycode)
321 for keycode in reversed(keycodes):
322 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode)
323 self.__display.sync()
324
325 def press_key(self, keysym):
326 """Press the key corresponding to a keysym.
327
328 Args:
329 keysym: keysym name as str
330 """
331 self.__handle_key_command(keysym, True) # key_press=True
332
333 def release_key(self, keysym):
334 """Release the key corresponding to a keysym.
335
336 Args:
337 keysym: keysym name as str
338 """
339 self.__handle_key_command(keysym, False) # key_press=False
340
341 def send_text(self, text):
342 """Type a sequence of characters.
343
344 Args:
345 text: sequence of characters to type. Along with individual
346 single-byte characters, keysyms can be embedded by
347 preceding them with "\\(" and suffixing them with ")", e.g.
348 "first line\\(Return)second line"
349
350 Raises:
351 InputError: text string contained invalid input
352 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym()
353 """
354 shift_keycode = self.__get_keycode_for_keysym('Shift_L')
355 shift_pressed = False
356
357 i = 0
358 while i < len(text):
359 ch = text[i:i+1]
360 keysym = None
361 if ch == '\\':
362 (keysym, num_chars_to_skip) = \
363 self.__convert_escaped_string_to_keysym(text[i:])
364 i += num_chars_to_skip
365 else:
366 keysym = self.__convert_char_to_keysym(ch)
367 i += 1
368
369 keycode = self.__get_keycode_for_keysym(keysym)
370
371 # Press or release the shift key as needed for this keysym.
372 shift_required = self.__keysym_requires_shift(keysym)
373 if shift_required and not shift_pressed:
374 xtest.fake_input(
375 self.__display, X.KeyPress, detail=shift_keycode)
376 shift_pressed = True
377 elif not shift_required and shift_pressed:
378 xtest.fake_input(
379 self.__display, X.KeyRelease, detail=shift_keycode)
380 shift_pressed = False
381
382 xtest.fake_input(self.__display, X.KeyPress, detail=keycode)
383 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode)
384
385 if shift_pressed:
386 xtest.fake_input(
387 self.__display, X.KeyRelease, detail=shift_keycode)
388 self.__display.sync()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698