# Copyright (C) 2006-2007 Owen Williams. # Copyright (C) 2006-2008 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging import time import gconf import wnck import gobject import gtk import dbus from sugar import wm from sugar import dispatch from sugar.graphics.xocolor import XoColor from sugar.presence import presenceservice from jarabe.model.bundleregistry import get_registry _SERVICE_NAME = "org.laptop.Activity" _SERVICE_PATH = "/org/laptop/Activity" _SERVICE_INTERFACE = "org.laptop.Activity" class Activity(gobject.GObject): """Activity which appears in the "Home View" of the Sugar shell This class stores the Sugar Shell's metadata regarding a given activity/application in the system. It interacts with the sugar.activity.* modules extensively in order to accomplish its tasks. """ __gtype_name__ = 'SugarHomeActivity' __gproperties__ = { 'launching' : (bool, None, None, False, gobject.PARAM_READWRITE), } def __init__(self, activity_info, activity_id, window=None): """Initialise the HomeActivity activity_info -- sugar.activity.registry.ActivityInfo instance, provides the information required to actually create the new instance. This is, in effect, the "type" of activity being created. activity_id -- unique identifier for this instance of the activity type window -- Main WnckWindow of the activity """ gobject.GObject.__init__(self) self._window = None self._service = None self._activity_id = activity_id self._activity_info = activity_info self._launch_time = time.time() self._launching = True if window is not None: self.set_window(window) self._retrieve_service() self._name_owner_changed_handler = None if not self._service: bus = dbus.SessionBus() self._name_owner_changed_handler = bus.add_signal_receiver( self._name_owner_changed_cb, signal_name="NameOwnerChanged", dbus_interface="org.freedesktop.DBus") def set_window(self, window): """Set the window for the activity We allow resetting the window for an activity so that we can replace the launcher once we get its real window. """ if not window: raise ValueError("window must be valid") self._window = window def get_service(self): """Get the activity service Note that non-native Sugar applications will not have such a service, so the return value will be None in those cases. """ return self._service def get_title(self): """Retrieve the application's root window's suggested title""" if self._window: return self._window.get_name() else: return '' def get_icon_path(self): """Retrieve the activity's icon (file) name""" if self.is_journal(): icon_theme = gtk.icon_theme_get_default() info = icon_theme.lookup_icon('activity-journal', gtk.ICON_SIZE_SMALL_TOOLBAR, 0) if not info: return None fname = info.get_filename() del info return fname elif self._activity_info: return self._activity_info.get_icon() else: return None def get_icon_color(self): """Retrieve the appropriate icon colour for this activity Uses activity_id to index into the PresenceService's set of activity colours, if the PresenceService does not have an entry (implying that this is not a Sugar-shared application) uses the local user's profile colour for the icon. """ pservice = presenceservice.get_instance() # HACK to suppress warning in logs when activity isn't found # (if it's locally launched and not shared yet) activity = None for act in pservice.get_activities(): if self._activity_id == act.props.id: activity = act break if activity != None: return XoColor(activity.props.color) else: client = gconf.client_get_default() return XoColor(client.get_string("/desktop/sugar/user/color")) def get_activity_id(self): """Retrieve the "activity_id" passed in to our constructor This is a "globally likely unique" identifier generated by sugar.util.unique_id """ return self._activity_id def get_xid(self): """Retrieve the X-windows ID of our root window""" if self._window is not None: return self._window.get_xid() else: return None def get_window(self): """Retrieve the X-windows root window of this application This was stored by the set_window method, which was called by HomeModel._add_activity, which was called via a callback that looks for all 'window-opened' events. HomeModel currently uses a dbus service query on the activity to determine to which HomeActivity the newly launched window belongs. """ return self._window def get_type(self): """Retrieve the activity bundle id for future reference""" if self._window is None: return None else: return wm.get_bundle_id(self._window) def is_journal(self): """Returns boolean if the activity is of type JournalActivity""" return self.get_type() == 'org.laptop.JournalActivity' def get_launch_time(self): """Return the time at which the activity was first launched Format is floating-point time.time() value (seconds since the epoch) """ return self._launch_time def get_pid(self): """Returns the activity's PID""" return self._window.get_pid() def get_bundle_path(self): """Returns the activity's bundle directory""" if self._activity_info is None: return None else: return self._activity_info.get_path() def get_activity_name(self): """Returns the activity's bundle name""" if self._activity_info is None: return None else: return self._activity_info.get_name() def equals(self, activity): if self._activity_id and activity.get_activity_id(): return self._activity_id == activity.get_activity_id() if self._window.get_xid() and activity.get_xid(): return self._window.get_xid() == activity.get_xid() return False def do_set_property(self, pspec, value): if pspec.name == 'launching': self._launching = value def do_get_property(self, pspec): if pspec.name == 'launching': return self._launching def _get_service_name(self): if self._activity_id: return _SERVICE_NAME + self._activity_id else: return None def _retrieve_service(self): if not self._activity_id: return try: bus = dbus.SessionBus() proxy = bus.get_object(self._get_service_name(), _SERVICE_PATH + "/" + self._activity_id) self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) except dbus.DBusException: self._service = None def _name_owner_changed_cb(self, name, old, new): if name == self._get_service_name(): self._retrieve_service() self.set_active(True) self._name_owner_changed_handler.remove() self._name_owner_changed_handler = None def set_active(self, state): """Propagate the current state to the activity object""" if self._service is not None: self._service.SetActive(state, reply_handler=self._set_active_success, error_handler=self._set_active_error) def _set_active_success(self): pass def _set_active_error(self, err): logging.error("set_active() failed: %s", err) class ShellModel(gobject.GObject): """Model of the shell (activity management) The ShellModel is basically the point of registration for all running activities within Sugar. It traps events that tell the system there is a new activity being created (generated by the activity factories), or removed, as well as those which tell us that the currently focussed activity has changed. The HomeModel tracks a set of HomeActivity instances, which are tracking the window to activity mappings the activity factories have set up. """ __gsignals__ = { 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'launch-started': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'launch-completed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'launch-failed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])) } ZOOM_MESH = 0 ZOOM_GROUP = 1 ZOOM_HOME = 2 ZOOM_ACTIVITY = 3 def __init__(self): gobject.GObject.__init__(self) self._screen = wnck.screen_get_default() self._screen.connect('window-opened', self._window_opened_cb) self._screen.connect('window-closed', self._window_closed_cb) self._screen.connect('active-window-changed', self._active_window_changed_cb) self.zoom_level_changed = dispatch.Signal() self._desktop_level = self.ZOOM_HOME self._zoom_level = self.ZOOM_HOME self._current_activity = None self._activities = [] self._active_activity = None self._tabbing_activity = None self._pservice = presenceservice.get_instance() self._screen.toggle_showing_desktop(True) def _update_zoom_level(self, window): if window.get_window_type() == wnck.WINDOW_DIALOG: return elif window.get_window_type() == wnck.WINDOW_NORMAL: new_level = self.ZOOM_ACTIVITY else: new_level = self._desktop_level if self._zoom_level != new_level: old_level = self._zoom_level self._zoom_level = new_level self.zoom_level_changed.send(self, old_level=old_level, new_level=new_level) def set_zoom_level(self, new_level, x_event_time=0): old_level = self.zoom_level if old_level == new_level: return if old_level != self.ZOOM_ACTIVITY: screen = gtk.gdk.screen_get_default() active_window_type = screen.get_active_window().get_type_hint() if active_window_type != gtk.gdk.WINDOW_TYPE_HINT_DESKTOP: return self._zoom_level = new_level if new_level is not self.ZOOM_ACTIVITY: self._desktop_level = new_level self.zoom_level_changed.send(self, old_level=old_level, new_level=new_level) show_desktop = new_level is not self.ZOOM_ACTIVITY self._screen.toggle_showing_desktop(show_desktop) if new_level is self.ZOOM_ACTIVITY: # activate the window, in case it was iconified # (e.g. during sugar launch, the Journal starts in this state) window = self._active_activity.get_window() if window: window.activate(x_event_time or gtk.get_current_event_time()) def _get_zoom_level(self): return self._zoom_level zoom_level = property(_get_zoom_level) def _get_activities_with_window(self): ret = [] for i in self._activities: if i.get_window() is not None: ret.append(i) return ret def get_previous_activity(self, current=None): if not current: current = self._active_activity activities = self._get_activities_with_window() i = activities.index(current) if len(activities) == 0: return None elif i - 1 >= 0: return activities[i - 1] else: return activities[len(activities) - 1] def get_next_activity(self, current=None): if not current: current = self._active_activity activities = self._get_activities_with_window() i = activities.index(current) if len(activities) == 0: return None elif i + 1 < len(activities): return activities[i + 1] else: return activities[0] def get_active_activity(self): """Returns the activity that the user is currently working in""" return self._active_activity def get_tabbing_activity(self): """Returns the activity that is currently highlighted during tabbing""" return self._tabbing_activity def set_tabbing_activity(self, activity): """Sets the activity that is currently highlighted during tabbing""" self._tabbing_activity = activity self.emit("tabbing-activity-changed", self._tabbing_activity) def _set_active_activity(self, home_activity): if self._active_activity == home_activity: return if home_activity: home_activity.set_active(True) if self._active_activity: self._active_activity.set_active(False) self._active_activity = home_activity self.emit('active-activity-changed', self._active_activity) def __iter__(self): return iter(self._activities) def __len__(self): return len(self._activities) def __getitem__(self, i): return self._activities[i] def index(self, obj): return self._activities.index(obj) def _window_opened_cb(self, screen, window): if window.get_window_type() == wnck.WINDOW_NORMAL: home_activity = None activity_id = wm.get_activity_id(window) service_name = wm.get_bundle_id(window) if service_name: registry = get_registry() activity_info = registry.get_bundle(service_name) else: activity_info = None if activity_id: home_activity = self.get_activity_by_id(activity_id) xid = window.get_xid() gdk_window = gtk.gdk.window_foreign_new(xid) gdk_window.set_decorations(0) window.maximize() if not home_activity: home_activity = Activity(activity_info, activity_id, window) self._add_activity(home_activity) else: home_activity.set_window(window) if wm.get_sugar_window_type(window) != 'launcher': home_activity.props.launching = False if not home_activity.is_journal(): self.emit('launch-completed', home_activity) startup_time = time.time() - home_activity.get_launch_time() logging.debug('%s launched in %f seconds.', home_activity.get_type(), startup_time) if self._active_activity is None: self._set_active_activity(home_activity) def _window_closed_cb(self, screen, window): if window.get_window_type() == wnck.WINDOW_NORMAL: if self._get_activity_by_xid(window.get_xid()) is not None: self._remove_activity_by_xid(window.get_xid()) def _get_activity_by_xid(self, xid): for home_activity in self._activities: if home_activity.get_xid() == xid: return home_activity return None def get_activity_by_id(self, activity_id): for home_activity in self._activities: if home_activity.get_activity_id() == activity_id: return home_activity return None def _active_window_changed_cb(self, screen, previous_window=None): window = screen.get_active_window() if window is None: return if window.get_window_type() != wnck.WINDOW_DIALOG: while window.get_transient() is not None: window = window.get_transient() act = self._get_activity_by_xid(window.get_xid()) if act is not None: self._set_active_activity(act) self._update_zoom_level(window) def _add_activity(self, home_activity): self._activities.append(home_activity) self.emit('activity-added', home_activity) def _remove_activity(self, home_activity): if home_activity == self._active_activity: windows = wnck.screen_get_default().get_windows_stacked() windows.reverse() for window in windows: new_activity = self._get_activity_by_xid(window.get_xid()) if new_activity is not None: self._set_active_activity(new_activity) break else: logging.error('No activities are running') self._set_active_activity(None) self.emit('activity-removed', home_activity) self._activities.remove(home_activity) def _remove_activity_by_xid(self, xid): home_activity = self._get_activity_by_xid(xid) if home_activity: self._remove_activity(home_activity) else: logging.error('Model for window %d does not exist.', xid) def notify_launch(self, activity_id, service_name): registry = get_registry() activity_info = registry.get_bundle(service_name) if not activity_info: raise ValueError("Activity service name '%s'" \ " was not found in the bundle registry." % service_name) home_activity = Activity(activity_info, activity_id) home_activity.props.launching = True self._add_activity(home_activity) self._set_active_activity(home_activity) self.emit('launch-started', home_activity) # FIXME: better learn about finishing processes by receiving a signal. # Now just check whether an activity has a window after ~90sec gobject.timeout_add_seconds(90, self._check_activity_launched, activity_id) def notify_launch_failed(self, activity_id): home_activity = self.get_activity_by_id(activity_id) if home_activity: logging.debug("Activity %s (%s) launch failed", activity_id, home_activity.get_type()) home_activity.props.launching = False self._remove_activity(home_activity) else: logging.error('Model for activity id %s does not exist.', activity_id) self.emit('launch-failed', home_activity) def _check_activity_launched(self, activity_id): home_activity = self.get_activity_by_id(activity_id) if not home_activity: logging.debug('Activity %s has been closed already.', activity_id) return False if home_activity.props.launching: logging.debug('Activity %s still launching, assuming it failed.', activity_id) self.notify_launch_failed(activity_id) return False _model = None def get_model(): global _model if _model is None: _model = ShellModel() return _model