~3v1n0/autopilot/badwindow-errors-protect

« back to all changes in this revision

Viewing changes to autopilot/emulators/bamf.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
# Copyright 2011 Canonical
 
2
# Author: Thomi Richards
 
3
#
 
4
# This program is free software: you can redistribute it and/or modify it
 
5
# under the terms of the GNU General Public License version 3, as published
 
6
# by the Free Software Foundation.
 
7
 
 
8
"Various classes for interacting with BAMF."
 
9
 
 
10
import dbus
 
11
import dbus.glib
 
12
import gio
 
13
import gobject
 
14
import os
 
15
from Xlib import display, X, protocol
 
16
from gtk import gdk
 
17
 
 
18
from autopilot.emulators.dbus_handler import session_bus
 
19
 
 
20
__all__ = [
 
21
    "Bamf",
 
22
    "BamfApplication",
 
23
    "BamfWindow",
 
24
    ]
 
25
 
 
26
_BAMF_BUS_NAME = 'org.ayatana.bamf'
 
27
_X_DISPLAY = display.Display()
 
28
 
 
29
 
 
30
def _filter_user_visible(win):
 
31
    """Filter out non-user-visible objects.
 
32
 
 
33
    In some cases the DBus method we need to call hasn't been registered yet,
 
34
    in which case we do the safe thing and return False.
 
35
 
 
36
    """
 
37
    try:
 
38
        return win.user_visible
 
39
    except dbus.DBusException:
 
40
        return False
 
41
 
 
42
 
 
43
class Bamf(object):
 
44
    """High-level class for interacting with Bamf from within a test.
 
45
 
 
46
    Use this class to inspect the state of running applications and open
 
47
    windows.
 
48
 
 
49
    """
 
50
 
 
51
    def __init__(self):
 
52
        matcher_path = '/org/ayatana/bamf/matcher'
 
53
        self.matcher_interface_name = 'org.ayatana.bamf.matcher'
 
54
        self.matcher_proxy = session_bus.get_object(_BAMF_BUS_NAME, matcher_path)
 
55
        self.matcher_interface = dbus.Interface(self.matcher_proxy, self.matcher_interface_name)
 
56
 
 
57
    def get_running_applications(self, user_visible_only=True):
 
58
        """Get a list of the currently running applications.
 
59
 
 
60
        If user_visible_only is True (the default), only applications
 
61
        visible to the user in the switcher will be returned.
 
62
 
 
63
        """
 
64
        apps = [BamfApplication(p) for p in self.matcher_interface.RunningApplications()]
 
65
        if user_visible_only:
 
66
            return filter(_filter_user_visible, apps)
 
67
        return apps
 
68
 
 
69
    def get_running_applications_by_desktop_file(self, desktop_file):
 
70
        """Return a list of applications that have the desktop file 'desktop_file'`.
 
71
 
 
72
        This method may return an empty list, if no applications
 
73
        are found with the specified desktop file.
 
74
 
 
75
        """
 
76
        return [a for a in self.get_running_applications() if a.desktop_file == desktop_file]
 
77
 
 
78
    def get_application_by_xid(self, xid):
 
79
        """Return the application that has a child with the requested xid or None."""
 
80
 
 
81
        app_path = self.matcher_interface.ApplicationForXid(xid)
 
82
        if len(app_path):
 
83
            return BamfApplication(app_path)
 
84
        return None
 
85
 
 
86
    def get_open_windows(self, user_visible_only=True):
 
87
        """Get a list of currently open windows.
 
88
 
 
89
        If user_visible_only is True (the default), only applications
 
90
        visible to the user in the switcher will be returned.
 
91
 
 
92
        The result is sorted to be in stacking order.
 
93
 
 
94
        """
 
95
 
 
96
        windows = [BamfWindow(w) for w in self.matcher_interface.WindowStackForMonitor(-1)]
 
97
        if user_visible_only:
 
98
            windows = filter(_filter_user_visible, windows)
 
99
        # Now sort on stacking order.
 
100
        return reversed(windows)
 
101
 
 
102
    def get_window_by_xid(self, xid):
 
103
        """Get the BamfWindow that matches the provided 'xid'."""
 
104
        windows = [BamfWindow(w) for w in self.matcher_interface.WindowPaths() if BamfWindow(w).x_id == xid]
 
105
        return windows[0] if windows else None
 
106
 
 
107
    def wait_until_application_is_running(self, desktop_file, timeout):
 
108
        """Wait until a given application is running.
 
109
 
 
110
        'desktop_file' is the name of the application desktop file.
 
111
        'timeout' is the maximum time to wait, in seconds. If set to
 
112
        something less than 0, this method will wait forever.
 
113
 
 
114
        This method returns true once the application is found, or false
 
115
        if the application was not found until the timeout was reached.
 
116
        """
 
117
        desktop_file = os.path.split(desktop_file)[1]
 
118
        # python workaround since you can't assign to variables in the enclosing scope:
 
119
        # see on_timeout_reached below...
 
120
        found_app = [True]
 
121
 
 
122
        # maybe the app is running already?
 
123
        if len(self.get_running_applications_by_desktop_file(desktop_file)) == 0:
 
124
            wait_forever = timeout < 0
 
125
            gobject_loop = gobject.MainLoop()
 
126
 
 
127
            # No, so define a callback to watch the ViewOpened signal:
 
128
            def on_view_added(bamf_path, name):
 
129
                if bamf_path.split('/')[-1].startswith('application'):
 
130
                    app = BamfApplication(bamf_path)
 
131
                    if desktop_file == os.path.split(app.desktop_file)[1]:
 
132
                        gobject_loop.quit()
 
133
 
 
134
            # ...and one for when the user-defined timeout has been reached:
 
135
            def on_timeout_reached():
 
136
                gobject_loop.quit()
 
137
                found_app[0] = False
 
138
                return False
 
139
 
 
140
            # need a timeout? if so, connect it:
 
141
            if not wait_forever:
 
142
                gobject.timeout_add(timeout * 1000, on_timeout_reached)
 
143
            # connect signal handler:
 
144
            session_bus.add_signal_receiver(on_view_added, 'ViewOpened')
 
145
            # pump the gobject main loop until either the correct signal is emitted, or the
 
146
            # timeout happens.
 
147
            gobject_loop.run()
 
148
 
 
149
        return found_app[0]
 
150
 
 
151
    def launch_application(self, desktop_file, files=[], wait=True):
 
152
        """Launch an application by specifying a desktop file.
 
153
 
 
154
        `files` is a list of files to pass to the application. Not all apps support this.
 
155
 
 
156
        If `wait` is True, this method will block until the application has launched.
 
157
 
 
158
        Returns the Gobject process object. if wait is True (the default),
 
159
        this method will not return until an instance of this application
 
160
        appears in the BAMF application list.
 
161
        """
 
162
        if type(files) is not list:
 
163
            raise TypeError("files must be a list.")
 
164
        proc = gio.unix.DesktopAppInfo(desktop_file)
 
165
        proc.launch_uris(files)
 
166
        if wait:
 
167
            self.wait_until_application_is_running(desktop_file, -1)
 
168
        return proc
 
169
 
 
170
 
 
171
class BamfApplication(object):
 
172
    """Represents an application, with information as returned by Bamf.
 
173
 
 
174
    Don't instantiate this class yourself. instead, use the methods as
 
175
    provided by the Bamf class.
 
176
 
 
177
    """
 
178
    def __init__(self, bamf_app_path):
 
179
        self.bamf_app_path = bamf_app_path
 
180
        try:
 
181
            self._app_proxy = session_bus.get_object(_BAMF_BUS_NAME, bamf_app_path)
 
182
            self._view_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.view')
 
183
            self._app_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.application')
 
184
        except dbus.DBusException, e:
 
185
            e.message += 'bamf_app_path=%r' % (bamf_app_path)
 
186
            raise
 
187
 
 
188
    @property
 
189
    def desktop_file(self):
 
190
        """Get the application desktop file"""
 
191
        return os.path.split(self._app_iface.DesktopFile())[1]
 
192
 
 
193
    @property
 
194
    def name(self):
 
195
        """Get the application name.
 
196
 
 
197
        Note: This may change according to the current locale. If you want a unique
 
198
        string to match applications against, use the desktop_file instead.
 
199
 
 
200
        """
 
201
        return self._view_iface.Name()
 
202
 
 
203
    @property
 
204
    def icon(self):
 
205
        """Get the application icon."""
 
206
        return self._view_iface.Icon()
 
207
 
 
208
    @property
 
209
    def is_active(self):
 
210
        """Is the application active (i.e.- has keyboard focus)?"""
 
211
        return self._view_iface.IsActive()
 
212
 
 
213
    @property
 
214
    def is_urgent(self):
 
215
        """Is the application currently signalling urgency?"""
 
216
        return self._view_iface.IsUrgent()
 
217
 
 
218
    @property
 
219
    def user_visible(self):
 
220
        """Is this application visible to the user?
 
221
 
 
222
        Some applications (such as the panel) are hidden to the user but will
 
223
        still be returned by bamf.
 
224
 
 
225
        """
 
226
        return self._view_iface.UserVisible()
 
227
 
 
228
    def get_windows(self):
 
229
        """Get a list of the application windows."""
 
230
        return [BamfWindow(w) for w in self._view_iface.Children()]
 
231
 
 
232
    def __repr__(self):
 
233
        return "<BamfApplication '%s'>" % (self.name)
 
234
 
 
235
 
 
236
class BamfWindow(object):
 
237
    """Represents an application window, as returned by Bamf.
 
238
 
 
239
    Don't instantiate this class yourself. Instead, use the appropriate methods
 
240
    in BamfApplication.
 
241
 
 
242
    """
 
243
    def __init__(self, window_path):
 
244
        self._bamf_win_path = window_path
 
245
        self._app_proxy = session_bus.get_object(_BAMF_BUS_NAME, window_path)
 
246
        self._window_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.window')
 
247
        self._view_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.view')
 
248
 
 
249
        self._xid = int(self._window_iface.GetXid())
 
250
        self._x_root_win = _X_DISPLAY.screen().root
 
251
        self._x_win = _X_DISPLAY.create_resource_object('window', self._xid)
 
252
 
 
253
    @property
 
254
    def x_id(self):
 
255
        """Get the X11 Window Id."""
 
256
        return self._xid
 
257
 
 
258
    @property
 
259
    def x_win(self):
 
260
        """Get the X11 window object of the underlying window."""
 
261
        return self._x_win
 
262
 
 
263
    @property
 
264
    def name(self):
 
265
        """Get the window name.
 
266
 
 
267
        Note: This may change according to the current locale. If you want a unique
 
268
        string to match windows against, use the x_id instead.
 
269
 
 
270
        """
 
271
        return self._view_iface.Name()
 
272
 
 
273
    @property
 
274
    def title(self):
 
275
        """Get the window title.
 
276
 
 
277
        This may be different from the application name.
 
278
 
 
279
        Note that this may change depending on the current locale.
 
280
 
 
281
        """
 
282
        return self._getProperty('_NET_WM_NAME')
 
283
 
 
284
    @property
 
285
    def geometry(self):
 
286
        """Get the geometry for this window.
 
287
 
 
288
        Returns a tuple containing (x, y, width, height).
 
289
 
 
290
        """
 
291
        # FIXME: We need to use the gdk window here to get the real coordinates
 
292
        geometry = self._x_win.get_geometry()
 
293
        origin = gdk.window_foreign_new(self._xid).get_origin()
 
294
        return (origin[0], origin[1], geometry.width, geometry.height)
 
295
 
 
296
    @property
 
297
    def is_maximized(self):
 
298
        """Is the window maximized?
 
299
 
 
300
        Maximized in this case means both maximized
 
301
        vertically and horizontally. If a window is only maximized in one
 
302
        direction it is not considered maximized.
 
303
 
 
304
        """
 
305
        win_state = self._get_window_states()
 
306
        return '_NET_WM_STATE_MAXIMIZED_VERT' in win_state and \
 
307
            '_NET_WM_STATE_MAXIMIZED_HORZ' in win_state
 
308
 
 
309
    @property
 
310
    def application(self):
 
311
        """Get the application that owns this window.
 
312
 
 
313
        This method may return None if the window does not have an associated
 
314
        application. The 'desktop' window is one such example.
 
315
 
 
316
        """
 
317
        # BAMF returns a list of parents since some windows don't have an
 
318
        # associated application. For these windows we return none.
 
319
        parents = self._view_iface.Parents()
 
320
        if parents:
 
321
            return BamfApplication(parents[0])
 
322
        else:
 
323
            return None
 
324
 
 
325
    @property
 
326
    def user_visible(self):
 
327
        """Is this window visible to the user in the switcher?"""
 
328
        return self._view_iface.UserVisible()
 
329
 
 
330
    @property
 
331
    def is_hidden(self):
 
332
        """Is this window hidden?
 
333
 
 
334
        Windows are hidden when the 'Show Desktop' mode is activated.
 
335
 
 
336
        """
 
337
        win_state = self._get_window_states()
 
338
        return '_NET_WM_STATE_HIDDEN' in win_state
 
339
 
 
340
    @property
 
341
    def is_focused(self):
 
342
        """Is this window focused?"""
 
343
        win_state = self._get_window_states()
 
344
        return '_NET_WM_STATE_FOCUSED' in win_state
 
345
 
 
346
    @property
 
347
    def is_valid(self):
 
348
        """Is this window object valid?
 
349
 
 
350
        Invalid windows are caused by windows closing during the construction of
 
351
        this object instance.
 
352
 
 
353
        """
 
354
        return not self._x_win is None
 
355
 
 
356
    @property
 
357
    def monitor(self):
 
358
        """Returns the monitor to which the windows belongs to"""
 
359
        return self._window_iface.Monitor()
 
360
 
 
361
    @property
 
362
    def closed(self):
 
363
        """Returns True if the window has been closed"""
 
364
        # This will return False when the window is closed and then removed from BUS
 
365
        try:
 
366
            return (self._window_iface.GetXid() != self.x_id)
 
367
        except:
 
368
            return True
 
369
 
 
370
    def close(self):
 
371
        """Close the window."""
 
372
 
 
373
        self._setProperty('_NET_CLOSE_WINDOW', [0, 0])
 
374
 
 
375
    def set_focus(self):
 
376
        self._x_win.set_input_focus(X.RevertToParent, X.CurrentTime)
 
377
        self._x_win.configure(stack_mode=X.Above)
 
378
 
 
379
    def __repr__(self):
 
380
        return "<BamfWindow '%s'>" % (self.title if self._x_win else str(self._xid))
 
381
 
 
382
    def _getProperty(self, _type):
 
383
        """Get an X11 property.
 
384
 
 
385
        _type is a string naming the property type. win is the X11 window object.
 
386
 
 
387
        """
 
388
        atom = self._x_win.get_full_property(_X_DISPLAY.get_atom(_type), X.AnyPropertyType)
 
389
        if atom:
 
390
            return atom.value
 
391
 
 
392
    def _setProperty(self, _type, data, mask=None):
 
393
        if type(data) is str:
 
394
            dataSize = 8
 
395
        else:
 
396
            # data length must be 5 - pad with 0's if it's short, truncate otherwise.
 
397
            data = (data + [0] * (5 - len(data)))[:5]
 
398
            dataSize = 32
 
399
 
 
400
        ev = protocol.event.ClientMessage(window=self._x_win, client_type=_X_DISPLAY.get_atom(_type), data=(dataSize, data))
 
401
 
 
402
        if not mask:
 
403
            mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask)
 
404
        self._x_root_win.send_event(ev, event_mask=mask)
 
405
        _X_DISPLAY.sync()
 
406
 
 
407
    def _get_window_states(self):
 
408
        """Return a list of strings representing the current window state."""
 
409
 
 
410
        _X_DISPLAY.sync()
 
411
        return map(_X_DISPLAY.get_atom_name, self._getProperty('_NET_WM_STATE'))