| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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] |
| OLD | NEW |