diff --git a/configure.ac b/configure.ac index a6e254bb..951d6b96 100644 --- a/configure.ac +++ b/configure.ac @@ -38,6 +38,8 @@ AC_SUBST(GNOMEPYTHONEXTRAS_DEFSDIR) PYGTK_DEFSDIR=`$PKG_CONFIG --variable=defsdir pygtk-2.0` AC_SUBST(PYGTK_DEFSDIR) +PKG_CHECK_MODULES(PYCAIRO, pycairo) + # # Setup GETTEXT # diff --git a/lib/python/Makefile.am b/lib/python/Makefile.am index be712d70..6f28d335 100644 --- a/lib/python/Makefile.am +++ b/lib/python/Makefile.am @@ -1,6 +1,7 @@ INCLUDES = \ $(PYTHON_INCLUDES) \ $(PYGTK_CFLAGS) \ + $(PYCAIRO_CFLAGS) \ $(LIB_CFLAGS) \ -I $(top_srcdir)/lib/src @@ -11,6 +12,7 @@ pkgpyexec_LTLIBRARIES = _sugar.la _sugar_la_LDFLAGS = -module -avoid-version -R$(MOZILLA_HOME) _sugar_la_LIBADD = \ $(LIB_LIBS) \ + $(PYCAIRO_LIBS) \ $(top_builddir)/lib/src/libsugarprivate.la _sugar_la_SOURCES = \ diff --git a/lib/python/_sugar.defs b/lib/python/_sugar.defs index bead7b48..4ea9b8de 100644 --- a/lib/python/_sugar.defs +++ b/lib/python/_sugar.defs @@ -247,6 +247,15 @@ '("GdkPixbuf*" "pixbuf") ) ) + +(define-function cairo_surface_from_gdk_pixbuf + (c-name "sugar_cairo_surface_from_gdk_pixbuf") + (return-type "cairo_surface_t*") + (parameters + '("GdkPixbuf*" "pixbuf") + ) +) + ;; Enumerations and flags ... diff --git a/lib/python/_sugar.override b/lib/python/_sugar.override index 6e0fe720..8dd90f96 100644 --- a/lib/python/_sugar.override +++ b/lib/python/_sugar.override @@ -13,9 +13,11 @@ headers #include "sugar-download.h" #include "sugar-audio-manager.h" +#include "pycairo.h" #include #include +extern Pycairo_CAPI_t *Pycairo_CAPI; %% modulename gecko @@ -159,3 +161,23 @@ _wrap_sugar_hippo_canvas_image_set_image_from_gdk_pixbuf(PyGObject *self, PyObje return Py_None; } %% +override sugar_cairo_surface_from_gdk_pixbuf kwargs +static PyObject* +_wrap_sugar_cairo_surface_from_gdk_pixbuf(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "pixbuf", NULL }; + PyGObject *child; + cairo_surface_t *surface; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"O!:sugar.cairo_surface_from_gdk_pixbuf", kwlist, &PyGdkPixbuf_Type, &child)) + return NULL; + + surface = _cairo_surface_from_pixbuf(GDK_PIXBUF (child->obj)); + if (surface == NULL) { + PyErr_SetString(PyExc_RuntimeError, "pixbuf could not be converted"); + return NULL; + } + + return PycairoSurface_FromSurface(surface, NULL); +} +%% diff --git a/lib/python/_sugarmodule.c b/lib/python/_sugarmodule.c index 4d8511b9..c576efb0 100644 --- a/lib/python/_sugarmodule.c +++ b/lib/python/_sugarmodule.c @@ -5,6 +5,9 @@ /* include this first, before NO_IMPORT_PYGOBJECT is defined */ #include +#include +Pycairo_CAPI_t *Pycairo_CAPI; + void py_sugar_register_classes (PyObject *d); extern PyMethodDef py_sugar_functions[]; @@ -16,6 +19,8 @@ init_sugar(void) init_pygobject (); + Pycairo_IMPORT; + m = Py_InitModule ("_sugar", py_sugar_functions); d = PyModule_GetDict (m); diff --git a/shell/model/homeactivity.py b/shell/model/homeactivity.py index 2143ef2e..efaa2bb1 100644 --- a/shell/model/homeactivity.py +++ b/shell/model/homeactivity.py @@ -14,23 +14,73 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import time +import gobject +import logging + from sugar.presence import PresenceService from sugar.activity import Activity from sugar import profile -class HomeActivity: - def __init__(self, registry, window): +class HomeActivity(gobject.GObject): + __gsignals__ = { + 'launch-timeout': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + def __init__(self, bundle, activity_id): + gobject.GObject.__init__(self) + self._window = None + self._xid = None + self._service = None + self._id = activity_id + self._type = bundle.get_service_name() + self._icon_name = bundle.get_icon() + + self._launch_time = time.time() + self._launched = False + self._launch_timeout_id = gobject.timeout_add(10000, self._launch_timeout_cb) + + logging.debug("Activity %s (%s) launching..." % (self._id, self._type)) + + def __del__(self): + gobject.source_remove(self._launch_timeout_id) + self._launch_timeout_id = 0 + + def _launch_timeout_cb(self, user_data=None): + logging.debug("Activity %s (%s) launch timed out" % (self._id, self._type)) + self._launch_timeout_id = 0 + self.emit('launch-timeout') + return False + + def set_window(self, window): + """An activity is 'launched' once we get its window.""" + logging.debug("Activity %s (%s) finished launching" % (self._id, self._type)) + self._launched = True + gobject.source_remove(self._launch_timeout_id) + self._launch_timeout_id = 0 + + if self._window or self._xid: + raise RuntimeError("Activity is already launched!") + if not window: + raise ValueError("window must be valid") + self._window = window self._xid = window.get_xid() - self._service = Activity.get_service(window.get_xid()) - self._id = self._service.get_id() - self._type = self._service.get_type() - info = registry.get_bundle(self._type) - self._icon_name = info.get_icon() + # verify id and type details + act_id = self._service.get_id() + if act_id != self._id: + raise RuntimeError("Activity's real ID (%s) didn't match expected (%s)." % (act_id, self._id)) + act_type = self._service.get_type() + if act_type != self._type: + raise RuntimeError("Activity's real type (%s) didn't match expected (%s)." % (act_type, self._type)) def get_title(self): + if not self._launched: + raise RuntimeError("Activity is still launching.") return self._window.get_name() def get_icon_name(self): @@ -47,13 +97,25 @@ class HomeActivity: return self._id def get_xid(self): + if not self._launched: + raise RuntimeError("Activity is still launching.") return self._xid def get_window(self): + if not self._launched: + raise RuntimeError("Activity is still launching.") return self._window def get_type(self): return self._type def get_shared(self): + if not self._launched: + raise RuntimeError("Activity is still launching.") return self._service.get_shared() + + def get_launch_time(self): + return self._launch_time + + def get_launched(self): + return self._launched diff --git a/shell/model/homemodel.py b/shell/model/homemodel.py index f4fd3eec..b1b4c8f7 100644 --- a/shell/model/homemodel.py +++ b/shell/model/homemodel.py @@ -20,10 +20,14 @@ import gobject import wnck from model.homeactivity import HomeActivity +from sugar.activity import Activity class HomeModel(gobject.GObject): __gsignals__ = { + 'activity-launched': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), @@ -68,6 +72,12 @@ class HomeModel(gobject.GObject): if window.get_window_type() == wnck.WINDOW_NORMAL: self._remove_activity(window.get_xid()) + def _get_activity_by_xid(self, xid): + for act in self._activities.values(): + if act.get_xid() == xid: + return act + return None + def _active_window_changed_cb(self, screen): window = screen.get_active_window() if window == None: @@ -77,8 +87,13 @@ class HomeModel(gobject.GObject): return xid = window.get_xid() - if self._activities.has_key(xid): - self._current_activity = self._activities[xid] + act = self._get_activity_by_xid(window.get_xid()) + if act: + if act.get_launched() == True: + self._current_activity = act + else: + self._current_activity = None + logging.error('Actiivty for window %d was not yet launched.' % xid) else: self._current_activity = None logging.error('Model for window %d does not exist.' % xid) @@ -86,13 +101,57 @@ class HomeModel(gobject.GObject): self.emit('active-activity-changed', self._current_activity) def _add_activity(self, window): - activity = HomeActivity(self._bundle_registry, window) - self._activities[window.get_xid()] = activity + act_service = Activity.get_service(window.get_xid()) + act_id = act_service.get_id() + + activity = None + if self._activities.has_key(act_id): + activity = self._activities[act_id] + else: + # activity got lost, took longer to launch than we allow, + # or it was launched by something other than the shell + act_type = act_service.get_type() + bundle = self._bundle_registry.get_bundle(act_type) + if not bundle: + raise RuntimeError("No bundle for activity type '%s'." % act_type) + return + activity = HomeActivity(bundle, act_id) + self._activities[act_id] = activity + + activity.set_window(window) self.emit('activity-added', activity) + def _internal_remove_activity(self, activity): + self.emit('activity-removed', activity) + act_id = activity.get_id() + del self._activities[act_id] + def _remove_activity(self, xid): - if self._activities.has_key(xid): - self.emit('activity-removed', self._activities[xid]) - del self._activities[xid] + activity = self._get_activity_by_xid(xid) + if activity: + self._internal_remove_activity(activity) else: logging.error('Model for window %d does not exist.' % xid) + + def _activity_launch_timeout_cb(self, activity): + act_id = activity.get_id() + if not act_id in self._activities.keys(): + return + self._internal_remove_activity(activity) + + def notify_activity_launch(self, activity_id, service_name): + bundle = self._bundle_registry.get_bundle(service_name) + if not bundle: + raise ValueError("Activity service name '%s' was not found in the bundle registry." % service_name) + activity = HomeActivity(bundle, activity_id) + activity.connect('launch-timeout', self._activity_launch_timeout_cb) + self._activities[activity_id] = activity + self.emit('activity-launched', activity) + + def notify_activity_launch_failed(self, activity_id): + if self._activities.has_key(activity_id): + activity = self._activities[activity_id] + logging.debug("Activity %s (%s) launch failed" % (activity_id, activity.get_type())) + self._internal_remove_activity(activity) + else: + logging.error('Model for activity id %s does not exist.' % activity_id) diff --git a/shell/sugar-activity b/shell/sugar-activity index bb6cf30f..43b56dc8 100755 --- a/shell/sugar-activity +++ b/shell/sugar-activity @@ -18,9 +18,18 @@ import sys import os +import gobject from sugar.activity import ActivityFactory from sugar import env +from sugar import util + +def _success_cb(handler, activity, loop): + activity.start(util.unique_id()) + loop.quit() + +def _error_cb(handler, err, loop): + loop.quit() ppath = env.get_profile_path() bus_file = os.path.join(ppath, "session_bus_address") @@ -29,5 +38,10 @@ bus_name = f.read() f.close() os.environ['DBUS_SESSION_BUS_ADDRESS'] = bus_name -activity = ActivityFactory.create(sys.argv[1]) -activity.start() +loop = gobject.MainLoop() + +handler = ActivityFactory.create(sys.argv[1]) +handler.connect('success', _success_cb, loop) +handler.connect('error', _error_cb, loop) + +loop.run() diff --git a/shell/view/Shell.py b/shell/view/Shell.py index f3f6ca0b..03a3badc 100644 --- a/shell/view/Shell.py +++ b/shell/view/Shell.py @@ -68,6 +68,8 @@ class Shell(gobject.GObject): self._frame = Frame(self) self._frame.show_and_hide(3) + self._pservice = PresenceService.get_instance() + #self.start_activity('org.laptop.JournalActivity') def _handle_camera_key(self): @@ -189,40 +191,94 @@ class Shell(gobject.GObject): def get_model(self): return self._model - def join_activity(self, bundle_id, activity_id): - pservice = PresenceService.get_instance() + def _join_success_cb(self, handler, activity, activity_ps, activity_id, activity_type): + logging.debug("Joining activity %s (%s)" % (activity_id, activity_type)) + activity.join(activity_ps.object_path()) + def _join_error_cb(self, handler, err, home_model, activity_id, activity_type): + logging.error("Couldn't launch activity %s (%s):\n%s" % (activity_id, activity_type, err)) + home_mode.notify_activity_launch_failed(activity_id) + + def join_activity(self, bundle_id, activity_id): activity = self.get_activity(activity_id) if activity: activity.present() - else: - activity_ps = pservice.get_activity(activity_id) + return - if activity_ps: - # Get the service name for this activity, if - # we have a bundle on the system capable of handling - # this activity type - breg = self._model.get_bundle_registry() - bundle = breg.find_by_default_type(bundle_id) - if bundle: - serv_name = bundle.get_service_name() - try: - activity = ActivityFactory.create(serv_name) - except DBusException, e: - logging.error("Couldn't launch activity %s:\n%s" % (serv_name, e)) - else: - logging.debug("Joining activity type %s id %s" % (serv_name, activity_id)) - activity.join(activity_ps.object_path()) - else: - logging.error("Couldn't find activity for type %s" % bundle_id) - else: - logging.error('Cannot start activity.') + activity_ps = self._pservice.get_activity(activity_id) + if not activity_ps: + logging.error("Couldn't find shared activity for %s" % activity_id) + return + + # Get the service name for this activity, if + # we have a bundle on the system capable of handling + # this activity type + breg = self._model.get_bundle_registry() + bundle = breg.find_by_default_type(bundle_id) + if not bundle: + logging.error("Couldn't find activity for type %s" % bundle_id) + return + + act_type = bundle.get_service_name() + home_model = self._model.get_home() + home_model.notify_activity_launch(activity_id, act_type) + + handler = ActivityFactory.create(act_type) + handler.connect('success', self._join_success_cb, activity_ps, activity_id, act_type) + handler.connect('error', self._join_error_cb, home_model, activity_id, act_type) + + def _find_unique_activity_id(self): + # create a new unique activity ID + i = 0 + act_id = None + while i < 10: + act_id = sugar.util.unique_id() + i += 1 + + # check through existing activities + found = False + for xid, act_host in self._hosts.items(): + if act_host.get_id() == act_id: + found = True + break + if found: + act_id = None + continue + + # check through network activities + activities = self._pservice.get_activities() + for act in activities: + if act_id == act.get_id(): + found = True + break + if found: + act_id = None + continue + + return act_id + + def _start_success_cb(self, handler, activity, activity_id, activity_type): + logging.debug("Started activity %s (%s)" % (activity_id, activity_type)) + activity.start(activity_id) + + def _start_error_cb(self, handler, err, home_model, activity_id, activity_type): + logging.error("Couldn't launch activity %s (%s):\n%s" % (activity_id, activity_type, err)) + home_mode.notify_activity_launch_failed(activity_id) def start_activity(self, activity_type): logging.debug('Shell.start_activity') - activity = ActivityFactory.create(activity_type) - activity.start() - return activity + act_id = self._find_unique_activity_id() + if not act_id: + logging.error("Couldn't find available activity ID.") + return None + + home_model = self._model.get_home() + home_model.notify_activity_launch(act_id, activity_type) + + logging.debug("Shell.start_activity will start %s (%s)" % (act_id, activity_type)) + handler = ActivityFactory.create(activity_type) + handler.connect('success', self._start_success_cb, act_id, activity_type) + handler.connect('error', self._start_error_cb, home_model, act_id, activity_type) def set_zoom_level(self, level): if level == sugar.ZOOM_ACTIVITY: diff --git a/shell/view/clipboardicon.py b/shell/view/clipboardicon.py index ade37bd4..42c54532 100644 --- a/shell/view/clipboardicon.py +++ b/shell/view/clipboardicon.py @@ -4,6 +4,7 @@ from sugar.graphics.menuicon import MenuIcon from view.clipboardmenu import ClipboardMenu from sugar.activity import ActivityFactory from sugar.clipboard import clipboardservice +from sugar import util class ClipboardIcon(MenuIcon): @@ -39,22 +40,32 @@ class ClipboardIcon(MenuIcon): else: return None - def _icon_activated_cb(self, icon): - if self._percent == 100: - cb_service = clipboardservice.get_instance() - - (name, percent, icon, preview, format_types) = \ - cb_service.get_object(self._object_id) + def _activity_create_success_cb(self, handler, activity): + activity.start(util.unique_id()) + activity.execute("open_document", [self._object_id]) - if format_types: - logging.debug("_icon_activated_cb: " + self._object_id) - - activity_id = self._get_activity_for_mime_type(format_types[0]) - - if activity_id: - activity = ActivityFactory.create(activity_id) - activity.start() - activity.execute("open_document", [self._object_id]) + def _activity_create_error_cb(self, handler, err): + pass + + def _icon_activated_cb(self, icon): + if self._percent < 100: + return + + cb_service = clipboardservice.get_instance() + (name, percent, icon, preview, format_types) = \ + cb_service.get_object(self._object_id) + if not format_types: + return + + logging.debug("_icon_activated_cb: " + self._object_id) + activity_type = self._get_activity_for_mime_type(format_types[0]) + if not activity_type: + return + + # Launch the activity to handle this item + handler = ActivityFactory.create(activity_type) + handler.connect('success', self._activity_create_success_cb) + handler.connect('error', self._activity_create_error_cb) def _popup_action_cb(self, popup, action): self.popdown() diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index b4fe22ac..5e4d4f27 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -16,10 +16,44 @@ import hippo import math +import gobject from sugar.graphics.canvasicon import CanvasIcon from sugar.graphics import style +class ActivityIcon(CanvasIcon): + def __init__(self, activity): + icon_name = activity.get_icon_name() + icon_color = activity.get_icon_color() + CanvasIcon.__init__(self, icon_name=icon_name, color=icon_color) + style.apply_stylesheet(self, 'ring.ActivityIcon') + + self._activity = activity + self._pulse_id = 0 + self._launched = False + + self._pulse_id = gobject.timeout_add(200, self._pulse_cb) + + def __del__(self): + if self._pulse_id > 0: + gobject.source_remove(self._pulse_id) + + def _pulse_cb(self): + pass + + def set_launched(self): + if self._launched: + return + self._launched = True + gobject.source_remove(self._pulse_id) + self._pulse_id = 0 + + def get_launched(self): + return self._launched + + def get_activity(self): + return self._activity + class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): __gtype_name__ = 'SugarActivitiesDonut' def __init__(self, shell, **kwargs): @@ -29,34 +63,46 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): self._shell = shell self._model = shell.get_model().get_home() + self._model.connect('activity-launched', self._activity_launched_cb) self._model.connect('activity-added', self._activity_added_cb) - self._model.connect('activity-removed', self._activity_removed_cb) + self._model.connect('activity-removed', self._activity_removed_cb) + + def _activity_launched_cb(self, model, activity): + self._add_activity(activity) def _activity_added_cb(self, model, activity): - self._add_activity(activity) + # Mark the activity as launched + act_id = activity.get_id() + if not self._activities.has_key(act_id): + return + icon = self._activities[act_id] + icon.set_launched() def _activity_removed_cb(self, model, activity): self._remove_activity(activity) def _remove_activity(self, activity): - icon = self._activities[activity.get_id()] + act_id = activity.get_id() + if not self._activities.has_key(act_id): + return + icon = self._activities[act_id] self.remove(icon) - del self._activities[activity.get_id()] + del self._activities[act_id] def _add_activity(self, activity): - icon_name = activity.get_icon_name() - icon_color = activity.get_icon_color() - - icon = CanvasIcon(icon_name=icon_name, color=icon_color) - style.apply_stylesheet(icon, 'ring.ActivityIcon') - icon.connect('activated', self._activity_icon_clicked_cb, activity) + icon = ActivityIcon(activity) + icon.connect('activated', self._activity_icon_clicked_cb) self.append(icon, hippo.PACK_FIXED) self._activities[activity.get_id()] = icon self.emit_paint_needed(0, 0, -1, -1) - def _activity_icon_clicked_cb(self, item, activity): + def _activity_icon_clicked_cb(self, icon): + activity = icon.get_activity() + if not icon.get_launched(): + return + activity_host = self._shell.get_activity(activity.get_id()) if activity_host: activity_host.present() diff --git a/sugar/activity/Activity.py b/sugar/activity/Activity.py index c0023ab3..7926daed 100644 --- a/sugar/activity/Activity.py +++ b/sugar/activity/Activity.py @@ -62,9 +62,9 @@ class ActivityDbusService(dbus.service.Object): self._pservice = PresenceService.get_instance() @dbus.service.method(ACTIVITY_INTERFACE) - def start(self): - """Start the activity.""" - self._activity.start() + def start(self, activity_id): + """Start the activity in unshared mode.""" + self._activity.start(activity_id) @dbus.service.method(ACTIVITY_INTERFACE) def join(self, activity_ps_path): @@ -120,13 +120,13 @@ class Activity(gtk.Window): self._bus = ActivityDbusService(self) - def start(self): + def start(self, activity_id): """Start the activity.""" if self._activity_id != None: logging.warning('The activity has been already started.') return - self._activity_id = sugar.util.unique_id() + self._activity_id = activity_id #ds = datastore.get_instance() #self._journal_object = ds.create('', {}, self._activity_id) @@ -162,9 +162,9 @@ class Activity(gtk.Window): if self._activity_id != None: logging.warning('The activity has been already started.') return + self._activity_id = activity_ps.get_id() self._shared = True - self._activity_id = activity_ps.get_id() # Publish the default service, it's a copy of # one of those we found on the network. diff --git a/sugar/activity/ActivityFactory.py b/sugar/activity/ActivityFactory.py index 481c5b92..94e765ec 100644 --- a/sugar/activity/ActivityFactory.py +++ b/sugar/activity/ActivityFactory.py @@ -76,24 +76,43 @@ class ActivityFactory(dbus.service.Object): if len(self._activities) == 0: gtk.main_quit() +class ActivityCreationHandler(gobject.GObject): + + __gsignals__ = { + 'error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'success': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, activity_name): + gobject.GObject.__init__(self) + + bus = dbus.SessionBus() + factory_name = activity_name + factory_path = get_path(factory_name) + + proxy_obj = bus.get_object(factory_name, factory_path) + factory = dbus.Interface(proxy_obj, "com.redhat.Sugar.ActivityFactory") + + factory.create(reply_handler=self._reply_handler, error_handler=self._error_handler) + + def _reply_handler(self, xid): + bus = dbus.SessionBus() + proxy_obj = bus.get_object(Activity.get_service_name(xid), + Activity.get_object_path(xid)) + activity = dbus.Interface(proxy_obj, Activity.ACTIVITY_INTERFACE) + self.emit('success', activity) + + def _error_handler(self, err): + logging.debug("Couldn't create activity: %s" % err) + self.emit('error', err) + def create(activity_name): - """Create a new activity from his name.""" - bus = dbus.SessionBus() - - factory_name = activity_name - factory_path = get_path(factory_name) - - proxy_obj = bus.get_object(factory_name, factory_path) - factory = dbus.Interface(proxy_obj, "com.redhat.Sugar.ActivityFactory") - - xid = factory.create() - - bus = dbus.SessionBus() - proxy_obj = bus.get_object(Activity.get_service_name(xid), - Activity.get_object_path(xid)) - activity = dbus.Interface(proxy_obj, Activity.ACTIVITY_INTERFACE) - - return activity + """Create a new activity from its name.""" + return ActivityCreationHandler(activity_name) def start_factory(activity_class, bundle_path): """Start the activity factory."""