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

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

Issue 1292003: autox: Add support for creating and querying windows. (Closed)
Patch Set: fix typo in comment 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
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # 2 #
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. 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 4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file. 5 # found in the LICENSE file.
6 6
7 import re 7 import re
8 import select
8 import time 9 import time
9 10
10 import Xlib.display 11 import Xlib.display
11 import Xlib.protocol.request 12 import Xlib.protocol.request
12 from Xlib import X 13 from Xlib import X
14 from Xlib import Xatom
13 from Xlib import XK 15 from Xlib import XK
14 from Xlib.ext import xtest 16 from Xlib.ext import xtest
15 17
16 class AutoX(object): 18 class AutoX(object):
17 """AutoX provides an interface for interacting with X applications. 19 """AutoX provides an interface for interacting with X applications.
18 20
19 This is done by using the XTEST extension to inject events into the X 21 This is done by using the XTEST extension to inject events into the X
20 server. 22 server. Convenience methods are also provided to query information
23 about windows and to wait for testable conditions to be met.
21 24
22 Example usage: 25 Example usage:
23 26
24 import autox 27 import autox
25 28
26 ax = autox.AutoX() 29 ax = autox.AutoX()
27 ax.move_pointer(200, 100) 30 ax.move_pointer(200, 100)
28 ax.press_button(1) 31 ax.press_button(1)
29 ax.move_pointer(250, 150) 32 ax.move_pointer(250, 150)
30 ax.release_button(1) 33 ax.release_button(1)
31 34
32 ax.send_hotkey("Ctrl+L") 35 ax.send_hotkey("Ctrl+L")
33 ax.send_text("http://www.example.org/\n") 36 ax.send_text("http://www.example.org/\n")
37
38 # Create a window and wait for it to get the focus.
39 win = ax.create_and_map_window(width=200, height=200, title='test')
40 info = ax.get_window_info(win.id)
41 ax.await_condition(
42 lambda: info.is_focused,
43 desc='Waiting for window 0x%x to be focused' % win.id)
44 ax.destroy_window(win)
45
46 # Create an override-redirect window and check that it appears in
47 # the position that it requested.
48 popup_win = ax.create_and_map_window(
49 x=200, y=200, width=200, height=200,
50 title='popup', override_redirect=True)
51 popup_info = ax.get_window_info(popup_win.id)
52 ax.await_condition(
53 lambda: popup_info.get_geometry() == (200, 200, 200, 200),
54 desc='Checking window 0x%x\'s geometry' % popup_win.id)
55 ax.destroy_window(popup_win)
34 """ 56 """
35 57
36 # Map of characters that can be passed to send_text() that differ 58 # Map of characters that can be passed to send_text() that differ
37 # from their X keysym names. 59 # from their X keysym names.
38 __chars_to_keysyms = { 60 __chars_to_keysyms = {
39 ' ': 'space', 61 ' ': 'space',
40 '\n': 'Return', 62 '\n': 'Return',
41 '\t': 'Tab', 63 '\t': 'Tab',
42 '~': 'asciitilde', 64 '~': 'asciitilde',
43 '!': 'exclam', 65 '!': 'exclam',
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
84 pass 106 pass
85 107
86 class InvalidKeySymError(InputError): 108 class InvalidKeySymError(InputError):
87 """Error caused by the caller referencing an invalid keysym.""" 109 """Error caused by the caller referencing an invalid keysym."""
88 def __init__(self, keysym): 110 def __init__(self, keysym):
89 self.__keysym = keysym 111 self.__keysym = keysym
90 112
91 def __str__(self): 113 def __str__(self):
92 return "Invalid keysym \"%s\"" % self.__keysym 114 return "Invalid keysym \"%s\"" % self.__keysym
93 115
116 class ConditionTimeoutError(Error):
117 """Error caused by a test condition timing out."""
118 pass
119
120 class WindowInfo:
121 """Container for the latest information we've seen about a window."""
122 def __init__(self, x, y, width, height, expose_callback=None):
123 self.x = x
124 self.y = y
125 self.width = width
126 self.height = height
127 self.expose_callback = expose_callback
128 self.was_exposed = False
129 self.is_focused = False
130
131 def get_geometry(self):
132 """Get a tuple containing the window's position and dimensions.
133
134 Returns:
135 tuple of ints: (x, y, width, height)
136 """
137 return (self.x, self.y, self.width, self.height)
138
94 def __init__(self, display_name=None): 139 def __init__(self, display_name=None):
95 self.__display = Xlib.display.Display(display_name) 140 self.__display = Xlib.display.Display(display_name)
96 self.__root = self.__display.screen().root 141 self.__root = self.__display.screen().root
142 self.__windows = {}
143
144 # Make sure that we get notified about property changes on the root
145 # window, since the caller may wait on conditions that use them.
146 self.__root.change_attributes(event_mask=X.PropertyChangeMask)
97 147
98 def __get_keycode_for_keysym(self, keysym): 148 def __get_keycode_for_keysym(self, keysym):
99 """Get the keycode corresponding to a keysym. 149 """Get the keycode corresponding to a keysym.
100 150
101 Args: 151 Args:
102 keysym: keysym name as str 152 keysym: keysym name as str
103 153
104 Returns: 154 Returns:
105 integer keycode 155 integer keycode
106 156
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
221 """ 271 """
222 if len(char) != 1: 272 if len(char) != 1:
223 raise self.InputError('Got non-length-1 string "%s"' % char) 273 raise self.InputError('Got non-length-1 string "%s"' % char)
224 if char.isalnum(): 274 if char.isalnum():
225 # Letters and digits are easy. 275 # Letters and digits are easy.
226 return char 276 return char
227 if char in AutoX.__chars_to_keysyms: 277 if char in AutoX.__chars_to_keysyms:
228 return AutoX.__chars_to_keysyms[char] 278 return AutoX.__chars_to_keysyms[char]
229 raise self.InvalidKeySymError(char) 279 raise self.InvalidKeySymError(char)
230 280
281 def await_condition(self, condition, desc='', timeout_sec=10.0):
282 """Wait until a condition becomes true.
283
284 We call the condition whenever we receive events from the X server
285 and return when it becomes true.
286
287 Args:
288 condition: callable object taking no args and returning a bool
289 desc: str describing the condition; just used in exception
290 timeout_sec: maximum time to wait for the condition
291
292 Raises:
293 ConditionTimeoutError: condition didn't occur before timeout
294 """
295 end_time = time.time() + timeout_sec
296 fd = self.__display.fileno()
297
298 while True:
299 self.sync()
300 if condition():
301 return
302
303 remaining_time = end_time - time.time()
304 if remaining_time <= 0:
305 break
306
307 (rfds, wfds, xfds) = select.select([fd], [], [], remaining_time)
308 if not rfds:
309 break
310
311 raise AutoX.ConditionTimeoutError(desc)
312
313 def sync(self):
314 """Flush X request queue and process all pending events.
315 """
316 self.__display.sync()
317 while self.__display.pending_events():
318 event = self.__display.next_event()
319 if event.type == X.ConfigureNotify:
320 info = self.__windows[event.window.id]
321 info.x = event.x
322 info.y = event.y
323 info.width = event.width
324 info.height = event.height
325 elif event.type == X.Expose:
326 info = self.__windows[event.window.id]
327 info.was_exposed = True
328 if info.expose_callback:
329 info.expose_callback(event)
330 else:
331 event.window.clear_area(
332 event.x, event.y, event.width, event.height)
333 elif event.type == X.FocusIn or event.type == X.FocusOut:
334 self.__windows[event.window.id].is_focused = \
335 (event.type == X.FocusIn)
336
337
231 def get_pointer_position(self): 338 def get_pointer_position(self):
232 """Get the pointer's absolute position. 339 """Get the pointer's absolute position.
233 340
234 Returns: 341 Returns:
235 (x, y) integer tuple 342 (x, y) integer tuple
236 """ 343 """
237 reply = Xlib.protocol.request.QueryPointer( 344 reply = Xlib.protocol.request.QueryPointer(
238 display=self.__display.display, window=self.__root) 345 display=self.__display.display, window=self.__root)
239 return (reply.root_x, reply.root_y) 346 return (reply.root_x, reply.root_y)
240 347
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after
381 self.__display, X.KeyRelease, detail=shift_keycode) 488 self.__display, X.KeyRelease, detail=shift_keycode)
382 shift_pressed = False 489 shift_pressed = False
383 490
384 xtest.fake_input(self.__display, X.KeyPress, detail=keycode) 491 xtest.fake_input(self.__display, X.KeyPress, detail=keycode)
385 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode) 492 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode)
386 493
387 if shift_pressed: 494 if shift_pressed:
388 xtest.fake_input( 495 xtest.fake_input(
389 self.__display, X.KeyRelease, detail=shift_keycode) 496 self.__display, X.KeyRelease, detail=shift_keycode)
390 self.__display.sync() 497 self.__display.sync()
498
499 def create_and_map_window(self, x=0, y=0, width=200, height=200,
500 title=None, override_redirect=False,
501 expose_callback=None):
502 """Create and map a window.
503
504 Waits until the window has been exposed before returning.
505
506 Args:
507 x, y: int position of window
508 width, height: int dimensions of window
509 title: str containing the window's title
510 override_redirect: whether this is an override-redirect
511 ("popup", in GTK's parlance) window. override-redirect
512 windows are mapped, placed, and sized without any window
513 manager involvement.
514
515 Returns:
516 python-xlib Window object (but see destroy_window())
517 """
518 win = self.__root.create_window(
519 x, y, width, height, border_width=0,
520 depth=X.CopyFromParent,
521 override_redirect=override_redirect,
522 background_pixel=self.__display.screen().white_pixel,
523 event_mask = (X.ExposureMask |
524 X.FocusChangeMask |
525 X.StructureNotifyMask))
526
527 info = AutoX.WindowInfo(
528 x, y, width, height, expose_callback=expose_callback)
529 self.__windows[win.id] = info
530
531 if title:
532 win.set_wm_name(title)
533 utf8_atom = self.__display.get_atom('UTF8_STRING')
534 win.change_property(
535 self.__display.get_atom('_NET_WM_NAME'),
536 utf8_atom, 8, data=title)
537
538 win.map()
539 self.await_condition(
540 lambda: info.was_exposed,
541 desc='Waiting for window 0x%x to be exposed' % win.id)
542 return win
543
544 def destroy_window(self, window):
545 """Destroy a window returned by create_and_map_window().
546
547 In addition to calling the window's destroy() method, this method
548 cleans up internal state that was being used to track the window.
549
550 Args:
551 window: python-xlib Window object
552 """
553 del self.__windows[window.id]
554 window.destroy()
555
556 def get_window_info(self, window_id):
557 """Get an object containing information about a window.
558
559 Args:
560 window_id: int ID of the window
561
562 Returns:
563 WindowInfo object (should be treated as read-only)
564 """
565 return self.__windows[window_id]
566
567 def get_top_window_id_at_point(self, x, y):
568 """Get the ID of the topmost mapped toplevel window at a given point.
569
570 Note that under reparenting window managers, this may not be the
571 client window that you're expecting.
572
573 Args:
574 x, y: int position of point
575
576 Returns:
577 int containing the ID of the window at the point, or 0 if no
578 window is there
579 """
580 reply = self.__root.query_tree()
581 for win in reversed(reply.children):
582 attr = win.get_attributes()
583 if attr.map_state == X.IsViewable:
584 geom = win.get_geometry()
585 if (geom.x <= x and geom.y <= y and
586 geom.x + geom.width > x and geom.y + geom.height > y):
587 return win.id
588 return 0
589
590 def get_active_window_property(self):
591 """Get the root window's _NET_ACTIVE_WINDOW property.
592
593 Returns:
594 int window ID from the property, or None if unset
595 """
596 reply = self.__root.get_property(
597 self.__display.get_atom('_NET_ACTIVE_WINDOW'), Xatom.WINDOW, 0, 1)
598 if not reply:
599 return None
600 return reply.value[0]
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