1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
# Copyright 2010 Canonical
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.
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.
13
"""A collection of emulators for X11 - namely keyboards and mice.
15
In the future we may also need other devices.
23
from time import sleep
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
32
_PRESSED_MOUSE_BUTTONS = []
34
logger = logging.getLogger(__name__)
40
class Keyboard(object):
41
"""Wrapper around xlib to make faking keyboard input possible."""
43
_special_X_keysyms = {
46
'\n' : "Return", # for some reason this needs to be cr, not lf
83
_keysym_translations = {
84
'Control' : 'Control_L',
95
self.shifted_keys = [k[1] for k in _DISPLAY._keymap_codes if k]
97
def press(self, keys, delay=0.2):
98
"""Send key press events only.
100
The 'keys' argument must be a string of keys you want
101
pressed. For example:
105
presses the 'Alt' and 'F2' keys.
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)
115
def release(self, keys, delay=0.2):
116
"""Send key release events only.
118
The 'keys' argument must be a string of keys you want
119
released. For example:
123
releases the 'Alt' and 'F2' keys.
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)
133
self.__perform_on_key(key, X.KeyRelease)
136
def press_and_release(self, keys, delay=0.2):
137
"""Press and release all items in 'keys'.
139
This is the same as calling 'press(keys);release(keys)'.
141
The 'keys' argument must be a string of keys you want
142
pressed and released.. For example:
144
press_and_release('Alt+F2'])
146
presses both the 'Alt' and 'F2' keys, and then releases both keys.
150
self.press(keys, delay)
151
self.release(keys, delay)
153
def type(self, string, delay=0.1):
154
"""Simulate a user typing a string of text.
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').
160
if not isinstance(string, basestring):
161
raise TypeError("'keys' argument must be a string.")
162
logger.debug("Typing text %r", string)
164
self.press(key, delay)
165
self.release(key, delay)
169
"""Generate KeyRelease events for any un-released keys.
171
Make sure you call this at the end of any test to release
172
any keys that were pressed and not released.
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)
181
def __perform_on_key(self, key, event):
182
if not isinstance(key, basestring):
183
raise TypeError("Key parameter must be a string")
188
keycode, shift_mask = self.__char_to_keycode(key)
191
fake_input(_DISPLAY, event, 50)
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)
201
logger.warning("Generating release event for keycode %d that was not pressed.", keycode)
203
fake_input(_DISPLAY, event, keycode)
206
def __get_keysym(self, key) :
207
keysym = XK.string_to_keysym(key)
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])
215
def __is_shifted(self, key) :
216
return len(key) == 1 and ord(key) in self.shifted_keys
218
def __char_to_keycode(self, key) :
219
keysym = self.__get_keysym(key)
220
keycode = _DISPLAY.keysym_to_keycode(keysym)
222
print "Sorry, can't map", key
224
if (self.__is_shifted(key)) :
225
shift_mask = X.ShiftMask
228
return keycode, shift_mask
230
def __translate_keys(self, key_string):
231
return [self._keysym_translations.get(k, k) for k in key_string.split('+')]
235
"""Wrapper around xlib to make moving the mouse easier."""
239
"""Mouse position X coordinate."""
240
return self.position()[0]
244
"""Mouse position Y coordinate."""
245
return self.position()[1]
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)
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)
260
logger.warning("Generating button release event or button %d that was not pressed.", button)
261
fake_input(_DISPLAY, X.ButtonRelease, button)
264
def click(self, button=1, press_duration=0.25):
265
"""Click mouse at current location."""
267
sleep(press_duration)
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")
275
def perform_move(x, y, sync):
276
fake_input(_DISPLAY, X.MotionNotify, sync, X.CurrentTime, X.NONE, x=x, y=y)
278
sleep(time_between_events)
281
perform_move(x, y, False)
283
dest_x, dest_y = x, y
284
curr_x, curr_y = self.position()
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
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)
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)
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"]
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)
323
class ScreenGeometry:
324
"""Get details about screen geometry."""
326
class BlacklistedDriverError(RuntimeError):
327
"""Cannot set primary monitor when running drivers listed in the driver blacklist."""
330
self._default_screen = gtk.gdk.screen_get_default()
331
self._blacklisted_drivers = ["NVIDIA"]
333
def get_num_monitors(self):
334
"""Get the number of monitors attached to the PC."""
335
return self._default_screen.get_n_monitors()
337
def get_primary_monitor(self):
338
return self._default_screen.get_primary_monitor()
340
def set_primary_monitor(self, monitor):
341
"""Set `monitor` to be the primary monitor.
343
`monitor` must be an integer between 0 and the number of configured monitors.
344
ValueError is raised if an invalid monitor is specified.
346
BlacklistedDriverError is raised if your video driver does not support this.
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')
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()))
357
monitor_name = self._default_screen.get_monitor_plug_name(monitor)
360
raise ValueError('Could not get monitor name from monitor number %d.' % (monitor))
362
ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary")
365
raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret))
367
def get_screen_width(self):
368
return self._default_screen.get_width()
370
def get_screen_height(self):
371
return self._default_screen.get_height()
373
def get_monitor_geometry(self, monitor_number):
374
"""Get the geometry for a particular monitor.
376
Returns a tuple containing (x,y,width,height).
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))
383
def is_rect_on_monitor(self, monitor_number, rect):
384
"""Returns True if `rect` is _entirely_ on the specified monitor, with no overlap."""
386
if type(rect) is not tuple or len(rect) != 4:
387
raise TypeError("rect must be a tuple of 4 int elements.")
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)
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)
401
def drag_window_to_monitor(self, window, monitor):
402
if not isinstance(window, BamfWindow):
403
raise TypeError("Window must be a BamfWindow")
405
if window.monitor == monitor:
406
logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor))
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)
413
logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor))
416
keyboard = Keyboard()
417
mouse.move(win_x + win_w/2, win_y + win_h/2)
418
keyboard.press("Alt")
420
keyboard.release("Alt")
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)