1
# Copyright 2011 Canonical
2
# Author: Thomi Richards
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.
8
"Various classes for interacting with BAMF."
15
from Xlib import display, X, protocol
18
from autopilot.emulators.dbus_handler import session_bus
26
_BAMF_BUS_NAME = 'org.ayatana.bamf'
27
_X_DISPLAY = display.Display()
30
def _filter_user_visible(win):
31
"""Filter out non-user-visible objects.
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.
38
return win.user_visible
39
except dbus.DBusException:
44
"""High-level class for interacting with Bamf from within a test.
46
Use this class to inspect the state of running applications and open
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)
57
def get_running_applications(self, user_visible_only=True):
58
"""Get a list of the currently running applications.
60
If user_visible_only is True (the default), only applications
61
visible to the user in the switcher will be returned.
64
apps = [BamfApplication(p) for p in self.matcher_interface.RunningApplications()]
66
return filter(_filter_user_visible, apps)
69
def get_running_applications_by_desktop_file(self, desktop_file):
70
"""Return a list of applications that have the desktop file 'desktop_file'`.
72
This method may return an empty list, if no applications
73
are found with the specified desktop file.
76
return [a for a in self.get_running_applications() if a.desktop_file == desktop_file]
78
def get_application_by_xid(self, xid):
79
"""Return the application that has a child with the requested xid or None."""
81
app_path = self.matcher_interface.ApplicationForXid(xid)
83
return BamfApplication(app_path)
86
def get_open_windows(self, user_visible_only=True):
87
"""Get a list of currently open windows.
89
If user_visible_only is True (the default), only applications
90
visible to the user in the switcher will be returned.
92
The result is sorted to be in stacking order.
96
windows = [BamfWindow(w) for w in self.matcher_interface.WindowStackForMonitor(-1)]
98
windows = filter(_filter_user_visible, windows)
99
# Now sort on stacking order.
100
return reversed(windows)
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
107
def wait_until_application_is_running(self, desktop_file, timeout):
108
"""Wait until a given application is running.
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.
114
This method returns true once the application is found, or false
115
if the application was not found until the timeout was reached.
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...
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()
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]:
134
# ...and one for when the user-defined timeout has been reached:
135
def on_timeout_reached():
140
# need a timeout? if so, connect it:
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
151
def launch_application(self, desktop_file, files=[], wait=True):
152
"""Launch an application by specifying a desktop file.
154
`files` is a list of files to pass to the application. Not all apps support this.
156
If `wait` is True, this method will block until the application has launched.
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.
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)
167
self.wait_until_application_is_running(desktop_file, -1)
171
class BamfApplication(object):
172
"""Represents an application, with information as returned by Bamf.
174
Don't instantiate this class yourself. instead, use the methods as
175
provided by the Bamf class.
178
def __init__(self, bamf_app_path):
179
self.bamf_app_path = bamf_app_path
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)
189
def desktop_file(self):
190
"""Get the application desktop file"""
191
return os.path.split(self._app_iface.DesktopFile())[1]
195
"""Get the application name.
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.
201
return self._view_iface.Name()
205
"""Get the application icon."""
206
return self._view_iface.Icon()
210
"""Is the application active (i.e.- has keyboard focus)?"""
211
return self._view_iface.IsActive()
215
"""Is the application currently signalling urgency?"""
216
return self._view_iface.IsUrgent()
219
def user_visible(self):
220
"""Is this application visible to the user?
222
Some applications (such as the panel) are hidden to the user but will
223
still be returned by bamf.
226
return self._view_iface.UserVisible()
228
def get_windows(self):
229
"""Get a list of the application windows."""
230
return [BamfWindow(w) for w in self._view_iface.Children()]
233
return "<BamfApplication '%s'>" % (self.name)
236
class BamfWindow(object):
237
"""Represents an application window, as returned by Bamf.
239
Don't instantiate this class yourself. Instead, use the appropriate methods
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')
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)
255
"""Get the X11 Window Id."""
260
"""Get the X11 window object of the underlying window."""
265
"""Get the window name.
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.
271
return self._view_iface.Name()
275
"""Get the window title.
277
This may be different from the application name.
279
Note that this may change depending on the current locale.
282
return self._getProperty('_NET_WM_NAME')
286
"""Get the geometry for this window.
288
Returns a tuple containing (x, y, width, height).
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)
297
def is_maximized(self):
298
"""Is the window maximized?
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.
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
310
def application(self):
311
"""Get the application that owns this window.
313
This method may return None if the window does not have an associated
314
application. The 'desktop' window is one such example.
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()
321
return BamfApplication(parents[0])
326
def user_visible(self):
327
"""Is this window visible to the user in the switcher?"""
328
return self._view_iface.UserVisible()
332
"""Is this window hidden?
334
Windows are hidden when the 'Show Desktop' mode is activated.
337
win_state = self._get_window_states()
338
return '_NET_WM_STATE_HIDDEN' in win_state
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
348
"""Is this window object valid?
350
Invalid windows are caused by windows closing during the construction of
351
this object instance.
354
return not self._x_win is None
358
"""Returns the monitor to which the windows belongs to"""
359
return self._window_iface.Monitor()
363
"""Returns True if the window has been closed"""
364
# This will return False when the window is closed and then removed from BUS
366
return (self._window_iface.GetXid() != self.x_id)
371
"""Close the window."""
373
self._setProperty('_NET_CLOSE_WINDOW', [0, 0])
376
self._x_win.set_input_focus(X.RevertToParent, X.CurrentTime)
377
self._x_win.configure(stack_mode=X.Above)
380
return "<BamfWindow '%s'>" % (self.title if self._x_win else str(self._xid))
382
def _getProperty(self, _type):
383
"""Get an X11 property.
385
_type is a string naming the property type. win is the X11 window object.
388
atom = self._x_win.get_full_property(_X_DISPLAY.get_atom(_type), X.AnyPropertyType)
392
def _setProperty(self, _type, data, mask=None):
393
if type(data) is str:
396
# data length must be 5 - pad with 0's if it's short, truncate otherwise.
397
data = (data + [0] * (5 - len(data)))[:5]
400
ev = protocol.event.ClientMessage(window=self._x_win, client_type=_X_DISPLAY.get_atom(_type), data=(dataSize, data))
403
mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask)
404
self._x_root_win.send_event(ev, event_mask=mask)
407
def _get_window_states(self):
408
"""Return a list of strings representing the current window state."""
411
return map(_X_DISPLAY.get_atom_name, self._getProperty('_NET_WM_STATE'))