~veebers/autopilot/fix_1178014

« back to all changes in this revision

Viewing changes to autopilot/emulators/X11.py

  • Committer: Thomi Richards
  • Date: 2012-05-06 22:45:27 UTC
  • Revision ID: thomi.richards@canonical.com-20120506224527-xh6wixqiw0rarkmh
Imported code from unity.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
 
2
# Copyright 2010 Canonical
 
3
# Author: Alex Launi
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify it
 
6
# under the terms of the GNU General Public License version 3, as published
 
7
# by the Free Software Foundation.
 
8
#
 
9
# This script is designed to run unity in a test drive manner. It will drive
 
10
# X and test the GL calls that Unity makes, so that we can easily find out if
 
11
# we are triggering graphics driver/X bugs.
 
12
 
 
13
"""A collection of emulators for X11 - namely keyboards and mice.
 
14
 
 
15
In the future we may also need other devices.
 
16
 
 
17
"""
 
18
 
 
19
import gtk.gdk
 
20
import logging
 
21
import os
 
22
import subprocess
 
23
from time import sleep
 
24
 
 
25
from autopilot.emulators.bamf import BamfWindow
 
26
from Xlib import X, XK
 
27
from Xlib.display import Display
 
28
from Xlib.ext.xtest import fake_input
 
29
 
 
30
 
 
31
_PRESSED_KEYS = []
 
32
_PRESSED_MOUSE_BUTTONS = []
 
33
_DISPLAY = Display()
 
34
logger = logging.getLogger(__name__)
 
35
 
 
36
def reset_display():
 
37
    global _DISPLAY
 
38
    _DISPLAY = Display()
 
39
    
 
40
class Keyboard(object):
 
41
    """Wrapper around xlib to make faking keyboard input possible."""
 
42
 
 
43
    _special_X_keysyms = {
 
44
        ' ' : "space",
 
45
        '\t' : "Tab",
 
46
        '\n' : "Return",  # for some reason this needs to be cr, not lf
 
47
        '\r' : "Return",
 
48
        '\e' : "Escape",
 
49
        '!' : "exclam",
 
50
        '#' : "numbersign",
 
51
        '%' : "percent",
 
52
        '$' : "dollar",
 
53
        '&' : "ampersand",
 
54
        '"' : "quotedbl",
 
55
        '\'' : "apostrophe",
 
56
        '(' : "parenleft",
 
57
        ')' : "parenright",
 
58
        '*' : "asterisk",
 
59
        '=' : "equal",
 
60
        '+' : "plus",
 
61
        ',' : "comma",
 
62
        '-' : "minus",
 
63
        '.' : "period",
 
64
        '/' : "slash",
 
65
        ':' : "colon",
 
66
        ';' : "semicolon",
 
67
        '<' : "less",
 
68
        '>' : "greater",
 
69
        '?' : "question",
 
70
        '@' : "at",
 
71
        '[' : "bracketleft",
 
72
        ']' : "bracketright",
 
73
        '\\' : "backslash",
 
74
        '^' : "asciicircum",
 
75
        '_' : "underscore",
 
76
        '`' : "grave",
 
77
        '{' : "braceleft",
 
78
        '|' : "bar",
 
79
        '}' : "braceright",
 
80
        '~' : "asciitilde"
 
81
        }
 
82
 
 
83
    _keysym_translations = {
 
84
        'Control' : 'Control_L',
 
85
        'Ctrl' : 'Control_L',
 
86
        'Alt' : 'Alt_L',
 
87
        'AltR': 'Alt_R',
 
88
        'Super' : 'Super_L',
 
89
        'Shift' : 'Shift_L',
 
90
        'Enter' : 'Return',
 
91
        'Space' : ' ',
 
92
    }
 
93
 
 
94
    def __init__(self):
 
95
        self.shifted_keys = [k[1] for k in _DISPLAY._keymap_codes if k]
 
96
 
 
97
    def press(self, keys, delay=0.2):
 
98
        """Send key press events only.
 
99
 
 
100
        The 'keys' argument must be a string of keys you want
 
101
        pressed. For example:
 
102
 
 
103
        press('Alt+F2')
 
104
 
 
105
        presses the 'Alt' and 'F2' keys.
 
106
 
 
107
        """
 
108
        if not isinstance(keys, basestring):
 
109
            raise TypeError("'keys' argument must be a string.")
 
110
        logger.debug("Pressing keys %r with delay %f", keys, delay)
 
111
        for key in self.__translate_keys(keys):
 
112
            self.__perform_on_key(key, X.KeyPress)
 
113
            sleep(delay)
 
114
 
 
115
    def release(self, keys, delay=0.2):
 
116
        """Send key release events only.
 
117
 
 
118
        The 'keys' argument must be a string of keys you want
 
119
        released. For example:
 
120
 
 
121
        release('Alt+F2')
 
122
 
 
123
        releases the 'Alt' and 'F2' keys.
 
124
 
 
125
        """
 
126
        if not isinstance(keys, basestring):
 
127
            raise TypeError("'keys' argument must be a string.")
 
128
        logger.debug("Releasing keys %r with delay %f", keys, delay)
 
129
        # release keys in the reverse order they were pressed in.
 
130
        keys = self.__translate_keys(keys)
 
131
        keys.reverse()
 
132
        for key in keys:
 
133
            self.__perform_on_key(key, X.KeyRelease)
 
134
            sleep(delay)
 
135
 
 
136
    def press_and_release(self, keys, delay=0.2):
 
137
        """Press and release all items in 'keys'.
 
138
 
 
139
        This is the same as calling 'press(keys);release(keys)'.
 
140
 
 
141
        The 'keys' argument must be a string of keys you want
 
142
        pressed and released.. For example:
 
143
 
 
144
        press_and_release('Alt+F2'])
 
145
 
 
146
        presses both the 'Alt' and 'F2' keys, and then releases both keys.
 
147
 
 
148
        """
 
149
 
 
150
        self.press(keys, delay)
 
151
        self.release(keys, delay)
 
152
 
 
153
    def type(self, string, delay=0.1):
 
154
        """Simulate a user typing a string of text.
 
155
 
 
156
        Only 'normal' keys can be typed with this method. Control characters
 
157
        (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
 
158
 
 
159
        """
 
160
        if not isinstance(string, basestring):
 
161
            raise TypeError("'keys' argument must be a string.")
 
162
        logger.debug("Typing text %r", string)
 
163
        for key in string:
 
164
            self.press(key, delay)
 
165
            self.release(key, delay)
 
166
 
 
167
    @staticmethod
 
168
    def cleanup():
 
169
        """Generate KeyRelease events for any un-released keys.
 
170
 
 
171
        Make sure you call this at the end of any test to release
 
172
        any keys that were pressed and not released.
 
173
 
 
174
        """
 
175
        global _PRESSED_KEYS
 
176
        for keycode in _PRESSED_KEYS:
 
177
            logger.warning("Releasing key %r as part of cleanup call.", keycode)
 
178
            fake_input(_DISPLAY, X.KeyRelease, keycode)
 
179
        _PRESSED_KEYS = []
 
180
 
 
181
    def __perform_on_key(self, key, event):
 
182
        if not isinstance(key, basestring):
 
183
            raise TypeError("Key parameter must be a string")
 
184
 
 
185
        keycode = 0
 
186
        shift_mask = 0
 
187
 
 
188
        keycode, shift_mask = self.__char_to_keycode(key)
 
189
 
 
190
        if shift_mask != 0:
 
191
            fake_input(_DISPLAY, event, 50)
 
192
 
 
193
        if event == X.KeyPress:
 
194
            logger.debug("Sending press event for key: %s", key)
 
195
            _PRESSED_KEYS.append(keycode)
 
196
        elif event == X.KeyRelease:
 
197
            logger.debug("Sending release event for key: %s", key)
 
198
            if keycode in _PRESSED_KEYS:
 
199
                _PRESSED_KEYS.remove(keycode)
 
200
            else:
 
201
                logger.warning("Generating release event for keycode %d that was not pressed.", keycode)
 
202
 
 
203
        fake_input(_DISPLAY, event, keycode)
 
204
        _DISPLAY.sync()
 
205
 
 
206
    def __get_keysym(self, key) :
 
207
        keysym = XK.string_to_keysym(key)
 
208
        if keysym == 0 :
 
209
            # Unfortunately, although this works to get the correct keysym
 
210
            # i.e. keysym for '#' is returned as "numbersign"
 
211
            # the subsequent display.keysym_to_keycode("numbersign") is 0.
 
212
            keysym = XK.string_to_keysym(self._special_X_keysyms[key])
 
213
        return keysym
 
214
 
 
215
    def __is_shifted(self, key) :
 
216
        return len(key) == 1 and ord(key) in self.shifted_keys
 
217
 
 
218
    def __char_to_keycode(self, key) :
 
219
        keysym = self.__get_keysym(key)
 
220
        keycode = _DISPLAY.keysym_to_keycode(keysym)
 
221
        if keycode == 0 :
 
222
            print "Sorry, can't map", key
 
223
 
 
224
        if (self.__is_shifted(key)) :
 
225
            shift_mask = X.ShiftMask
 
226
        else :
 
227
            shift_mask = 0
 
228
        return keycode, shift_mask
 
229
 
 
230
    def __translate_keys(self, key_string):
 
231
        return [self._keysym_translations.get(k, k) for k in key_string.split('+')]
 
232
 
 
233
 
 
234
class Mouse(object):
 
235
    """Wrapper around xlib to make moving the mouse easier."""
 
236
 
 
237
    @property
 
238
    def x(self):
 
239
        """Mouse position X coordinate."""
 
240
        return self.position()[0]
 
241
 
 
242
    @property
 
243
    def y(self):
 
244
        """Mouse position Y coordinate."""
 
245
        return self.position()[1]
 
246
 
 
247
    def press(self, button=1):
 
248
        """Press mouse button at current mouse location."""
 
249
        logger.debug("Pressing mouse button %d", button)
 
250
        _PRESSED_MOUSE_BUTTONS.append(button)
 
251
        fake_input(_DISPLAY, X.ButtonPress, button)
 
252
        _DISPLAY.sync()
 
253
 
 
254
    def release(self, button=1):
 
255
        """Releases mouse button at current mouse location."""
 
256
        logger.debug("Releasing mouse button %d", button)
 
257
        if button in _PRESSED_MOUSE_BUTTONS:
 
258
            _PRESSED_MOUSE_BUTTONS.remove(button)
 
259
        else:
 
260
            logger.warning("Generating button release event or button %d that was not pressed.", button)
 
261
        fake_input(_DISPLAY, X.ButtonRelease, button)
 
262
        _DISPLAY.sync()
 
263
 
 
264
    def click(self, button=1, press_duration=0.25):
 
265
        """Click mouse at current location."""
 
266
        self.press(button)
 
267
        sleep(press_duration)
 
268
        self.release(button)
 
269
 
 
270
    def move(self, x, y, animate=True, rate=100, time_between_events=0.001):
 
271
        '''Moves mouse to location (x, y, pixels_per_event, time_between_event)'''
 
272
        logger.debug("Moving mouse to position %d,%d %s animation.", x, y,
 
273
            "with" if animate else "without")
 
274
 
 
275
        def perform_move(x, y, sync):
 
276
            fake_input(_DISPLAY, X.MotionNotify, sync, X.CurrentTime, X.NONE, x=x, y=y)
 
277
            _DISPLAY.sync()
 
278
            sleep(time_between_events)
 
279
 
 
280
        if not animate:
 
281
            perform_move(x, y, False)
 
282
 
 
283
        dest_x, dest_y = x, y
 
284
        curr_x, curr_y = self.position()
 
285
 
 
286
        # calculate a path from our current position to our destination
 
287
        dy = float(curr_y - dest_y)
 
288
        dx = float(curr_x - dest_x)
 
289
        slope = dy / dx if dx > 0 else 0
 
290
        yint = curr_y - (slope * curr_x)
 
291
        xscale = rate if dest_x > curr_x else -rate
 
292
 
 
293
        while (int(curr_x) != dest_x):
 
294
            target_x = min(curr_x + xscale, dest_x) if dest_x > curr_x else max(curr_x + xscale, dest_x)
 
295
            perform_move(target_x - curr_x, 0, True)
 
296
            curr_x = target_x
 
297
 
 
298
        if (curr_y != dest_y):
 
299
            yscale = rate if dest_y > curr_y else -rate
 
300
            while (curr_y != dest_y):
 
301
                target_y = min(curr_y + yscale, dest_y) if dest_y > curr_y else max(curr_y + yscale, dest_y)
 
302
                perform_move(0, target_y - curr_y, True)
 
303
                curr_y = target_y
 
304
 
 
305
    def position(self):
 
306
        """Returns the current position of the mouse pointer."""
 
307
        coord = _DISPLAY.screen().root.query_pointer()._data
 
308
        x, y = coord["root_x"], coord["root_y"]
 
309
        return x, y
 
310
 
 
311
    @staticmethod
 
312
    def cleanup():
 
313
        """Put mouse in a known safe state."""
 
314
        global _PRESSED_MOUSE_BUTTONS
 
315
        for btn in _PRESSED_MOUSE_BUTTONS:
 
316
            logger.debug("Releasing mouse button %d as part of cleanup", btn)
 
317
            fake_input(_DISPLAY, X.ButtonRelease, btn)
 
318
        _PRESSED_MOUSE_BUTTONS = []
 
319
        sg = ScreenGeometry()
 
320
        sg.move_mouse_to_monitor(0)
 
321
 
 
322
 
 
323
class ScreenGeometry:
 
324
    """Get details about screen geometry."""
 
325
 
 
326
    class BlacklistedDriverError(RuntimeError):
 
327
        """Cannot set primary monitor when running drivers listed in the driver blacklist."""
 
328
 
 
329
    def __init__(self):
 
330
        self._default_screen = gtk.gdk.screen_get_default()
 
331
        self._blacklisted_drivers = ["NVIDIA"]
 
332
 
 
333
    def get_num_monitors(self):
 
334
        """Get the number of monitors attached to the PC."""
 
335
        return self._default_screen.get_n_monitors()
 
336
 
 
337
    def get_primary_monitor(self):
 
338
        return self._default_screen.get_primary_monitor()
 
339
 
 
340
    def set_primary_monitor(self, monitor):
 
341
        """Set `monitor` to be the primary monitor.
 
342
 
 
343
        `monitor` must be an integer between 0 and the number of configured monitors.
 
344
        ValueError is raised if an invalid monitor is specified.
 
345
 
 
346
        BlacklistedDriverError is raised if your video driver does not support this.
 
347
 
 
348
        """
 
349
        glxinfo_out = subprocess.check_output("glxinfo")
 
350
        for dri in self._blacklisted_drivers:
 
351
            if dri in glxinfo_out:
 
352
                raise ScreenGeometry.BlacklistedDriverError('Impossible change the primary monitor for the given driver')
 
353
 
 
354
        if monitor < 0 or monitor >= self.get_num_monitors():
 
355
            raise ValueError('Monitor %d is not in valid range of 0 <= monitor < %d.' % (self.get_num_monitors()))
 
356
 
 
357
        monitor_name = self._default_screen.get_monitor_plug_name(monitor)
 
358
 
 
359
        if not monitor_name:
 
360
            raise ValueError('Could not get monitor name from monitor number %d.' % (monitor))
 
361
 
 
362
        ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary")
 
363
 
 
364
        if ret != 0:
 
365
            raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret))
 
366
 
 
367
    def get_screen_width(self):
 
368
        return self._default_screen.get_width()
 
369
 
 
370
    def get_screen_height(self):
 
371
        return self._default_screen.get_height()
 
372
 
 
373
    def get_monitor_geometry(self, monitor_number):
 
374
        """Get the geometry for a particular monitor.
 
375
 
 
376
        Returns a tuple containing (x,y,width,height).
 
377
 
 
378
        """
 
379
        if monitor_number < 0 or monitor_number >= self.get_num_monitors():
 
380
            raise ValueError('Specified monitor number is out of range.')
 
381
        return tuple(self._default_screen.get_monitor_geometry(monitor_number))
 
382
 
 
383
    def is_rect_on_monitor(self, monitor_number, rect):
 
384
        """Returns True if `rect` is _entirely_ on the specified monitor, with no overlap."""
 
385
 
 
386
        if type(rect) is not tuple or len(rect) != 4:
 
387
            raise TypeError("rect must be a tuple of 4 int elements.")
 
388
 
 
389
        (x, y, w, h) = rect
 
390
        (m_x, m_y, m_w, m_h) = self.get_monitor_geometry(monitor_number)
 
391
        return (x >= m_x and x + w <= m_x + m_w and y >= m_y and y + h <= m_y + m_h)
 
392
 
 
393
    def move_mouse_to_monitor(self, monitor_number):
 
394
        """Move the mouse to the center of the specified monitor."""
 
395
        geo = self.get_monitor_geometry(monitor_number)
 
396
        x = geo[0] + (geo[2] / 2)
 
397
        y = geo[1] + (geo[3] / 2)
 
398
        #dont animate this or it might not get there due to barriers
 
399
        Mouse().move(x, y, False)
 
400
 
 
401
    def drag_window_to_monitor(self, window, monitor):
 
402
        if not isinstance(window, BamfWindow):
 
403
            raise TypeError("Window must be a BamfWindow")
 
404
 
 
405
        if window.monitor == monitor:
 
406
            logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor))
 
407
            return
 
408
 
 
409
        assert(not window.is_maximized)
 
410
        (win_x, win_y, win_w, win_h) = window.geometry
 
411
        (m_x, m_y, m_w, m_h) = self.get_monitor_geometry(monitor)
 
412
 
 
413
        logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor))
 
414
 
 
415
        mouse = Mouse()
 
416
        keyboard = Keyboard()
 
417
        mouse.move(win_x + win_w/2, win_y + win_h/2)
 
418
        keyboard.press("Alt")
 
419
        mouse.press()
 
420
        keyboard.release("Alt")
 
421
 
 
422
        # We do the movements in two steps, to reduce the risk of being
 
423
        # blocked by the pointer barrier
 
424
        target_x = m_x + m_w/2
 
425
        target_y = m_y + m_h/2
 
426
        mouse.move(win_x, target_y, rate=20, time_between_events=0.005)
 
427
        mouse.move(target_x, target_y, rate=20, time_between_events=0.005)
 
428
        mouse.release()