From eaef567cabc5c0dda9706faa0e17e20909131ade Mon Sep 17 00:00:00 2001 From: simpoir Date: Fri, 30 Jan 2009 04:14:48 +0000 Subject: ajout initial du tree jhbuild [jhbuild base] --- (limited to 'src/sugar/activity') diff --git a/src/sugar/activity/Makefile.am b/src/sugar/activity/Makefile.am new file mode 100644 index 0000000..91f6ea8 --- /dev/null +++ b/src/sugar/activity/Makefile.am @@ -0,0 +1,10 @@ +sugardir = $(pythondir)/sugar/activity +sugar_PYTHON = \ + __init__.py \ + activity.py \ + activityfactory.py \ + activityhandle.py \ + activityservice.py \ + bundlebuilder.py \ + main.py \ + namingalert.py \ No newline at end of file diff --git a/src/sugar/activity/__init__.py b/src/sugar/activity/__init__.py new file mode 100644 index 0000000..8d3ef2b --- /dev/null +++ b/src/sugar/activity/__init__.py @@ -0,0 +1,55 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Activity implementation code for Sugar-based activities + +Each activity within the OLPC environment must provide two +dbus services. The first, patterned after the + + sugar.activity.activityfactory.ActivityFactory + +class is responsible for providing a "create" method which +takes a small dictionary with values corresponding to a + + sugar.activity.activityhandle.ActivityHandle + +describing an individual instance of the activity. + +Each activity so registered is described by a + + sugar.activity.bundle.Bundle + +instance, which parses a specially formatted activity.info +file (stored in the activity directory's ./activity +subdirectory). The + + sugar.activity.bundlebuilder + +module provides facilities for the standard setup.py module +which produces and registers bundles from activity source +directories. + +Once instantiated by the ActivityFactory's create method, +each activity must provide an introspection API patterned +after the + + sugar.activity.activityservice.ActivityService + +class. This class allows for querying the ID of the root +window, requesting sharing across the network, and basic +"what type of application are you" queries. +""" diff --git a/src/sugar/activity/__init__py b/src/sugar/activity/__init__py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/sugar/activity/__init__py diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py new file mode 100644 index 0000000..d2ba278 --- /dev/null +++ b/src/sugar/activity/activity.py @@ -0,0 +1,1048 @@ +"""Base class for activities written in Python + +This is currently the only definitive reference for what an +activity must do to participate in the Sugar desktop. + + A Basic Activity + +All activities must implement a class derived from 'Activity' in this class. +The convention is to call it ActivitynameActivity, but this is not required as +the activity.info file associated with your activity will tell the sugar-shell +which class to start. + +For example the most minimal Activity: + + + from sugar.activity import activity + + class ReadActivity(activity.Activity): + pass + +To get a real, working activity, you will at least have to implement: + __init__(), read_file() and write_file() + +Aditionally, you will probably need a at least a Toolbar so you can have some +interesting buttons for the user, like for example 'exit activity' + +See the methods of the Activity class below for more information on what you +will need for a real activity. + +STABLE. +""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2007-2009 One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gettext +import logging +import os +import time +from hashlib import sha1 +import traceback +import gconf + +import gtk +import gobject +import dbus +import dbus.service +import cjson + +from sugar import util +from sugar.presence import presenceservice +from sugar.activity.activityservice import ActivityService +from sugar.activity.namingalert import NamingAlert +from sugar.graphics import style +from sugar.graphics.window import Window +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.alert import Alert +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.datastore import datastore +from sugar.session import XSMPClient +from sugar import wm + +_ = lambda msg: gettext.dgettext('sugar-toolkit', msg) + +SCOPE_PRIVATE = "private" +SCOPE_INVITE_ONLY = "invite" # shouldn't be shown in UI, it's implicit +SCOPE_NEIGHBORHOOD = "public" + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' + +class ActivityToolbar(gtk.Toolbar): + """The Activity toolbar with the Journal entry title, sharing, + Keep and Stop buttons + + All activities should have this toolbar. It is easiest to add it to your + Activity by using the ActivityToolbox. + """ + def __init__(self, activity): + gtk.Toolbar.__init__(self) + + self._activity = activity + self._updating_share = False + + activity.connect('shared', self.__activity_shared_cb) + activity.connect('joined', self.__activity_shared_cb) + activity.connect('notify::max_participants', + self.__max_participants_changed_cb) + + if activity.metadata: + self.title = gtk.Entry() + self.title.set_size_request(int(gtk.gdk.screen_width() / 3), -1) + self.title.set_text(activity.metadata['title']) + self.title.connect('changed', self.__title_changed_cb) + self._add_widget(self.title) + + activity.metadata.connect('updated', self.__jobject_updated_cb) + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.share = ToolComboBox(label_text=_('Share with:')) + self.share.combo.connect('changed', self.__share_changed_cb) + self.share.combo.append_item(SCOPE_PRIVATE, _('Private'), 'zoom-home') + self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'), + 'zoom-neighborhood') + self.insert(self.share, -1) + self.share.show() + + self._update_share() + + self.keep = ToolButton(tooltip=_('Keep')) + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + keep_icon = Icon(icon_name='document-save', xo_color=color) + self.keep.set_icon_widget(keep_icon) + keep_icon.show() + self.keep.props.accelerator = 'S' + self.keep.connect('clicked', self.__keep_clicked_cb) + self.insert(self.keep, -1) + self.keep.show() + + self.stop = ToolButton('activity-stop', tooltip=_('Stop')) + self.stop.props.accelerator = 'Q' + self.stop.connect('clicked', self.__stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + self._update_title_sid = None + + def _update_share(self): + self._updating_share = True + + if self._activity.props.max_participants == 1: + self.share.hide() + + if self._activity.get_shared(): + self.share.set_sensitive(False) + self.share.combo.set_active(1) + else: + self.share.set_sensitive(True) + self.share.combo.set_active(0) + + self._updating_share = False + + def __share_changed_cb(self, combo): + if self._updating_share: + return + + model = self.share.combo.get_model() + it = self.share.combo.get_active_iter() + (scope, ) = model.get(it, 0) + if scope == SCOPE_NEIGHBORHOOD: + self._activity.share() + + def __keep_clicked_cb(self, button): + self._activity.copy() + + def __stop_clicked_cb(self, button): + self._activity.close() + + def __jobject_updated_cb(self, jobject): + self.title.set_text(jobject['title']) + + def __title_changed_cb(self, entry): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add_seconds( + 1, self.__update_title_cb) + + def __update_title_cb(self): + title = self.title.get_text() + + self._activity.metadata['title'] = title + self._activity.metadata['title_set_by_user'] = '1' + self._activity.save() + + shared_activity = self._activity.get_shared_activity() + if shared_activity: + shared_activity.props.name = title + + self._update_title_sid = None + return False + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __activity_shared_cb(self, activity): + self._update_share() + + def __max_participants_changed_cb(self, activity, pspec): + self._update_share() + +class EditToolbar(gtk.Toolbar): + """Provides the standard edit toolbar for Activities. + + Members: + undo -- the undo button + redo -- the redo button + copy -- the copy button + paste -- the paste button + separator -- A separator between undo/redo and copy/paste + + This class only provides the 'edit' buttons in a standard layout, + your activity will need to either hide buttons which make no sense for your + Activity, or you need to connect the button events to your own callbacks: + + ## Example from Read.activity: + # Create the edit toolbar: + self._edit_toolbar = EditToolbar(self._view) + # Hide undo and redo, they're not needed + self._edit_toolbar.undo.props.visible = False + self._edit_toolbar.redo.props.visible = False + # Hide the separator too: + self._edit_toolbar.separator.props.visible = False + + # As long as nothing is selected, copy needs to be insensitive: + self._edit_toolbar.copy.set_sensitive(False) + # When the user clicks the button, call _edit_toolbar_copy_cb() + self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb) + + # Add the edit toolbar: + toolbox.add_toolbar(_('Edit'), self._edit_toolbar) + # And make it visible: + self._edit_toolbar.show() + """ + def __init__(self): + gtk.Toolbar.__init__(self) + + self.undo = ToolButton('edit-undo') + self.undo.set_tooltip(_('Undo')) + self.insert(self.undo, -1) + self.undo.show() + + self.redo = ToolButton('edit-redo') + self.redo.set_tooltip(_('Redo')) + self.insert(self.redo, -1) + self.redo.show() + + self.separator = gtk.SeparatorToolItem() + self.separator.set_draw(True) + self.insert(self.separator, -1) + self.separator.show() + + self.copy = ToolButton('edit-copy') + self.copy.set_tooltip(_('Copy')) + self.insert(self.copy, -1) + self.copy.show() + + self.paste = ToolButton('edit-paste') + self.paste.set_tooltip(_('Paste')) + self.insert(self.paste, -1) + self.paste.show() + +class ActivityToolbox(Toolbox): + """Creates the Toolbox for the Activity + + By default, the toolbox contains only the ActivityToolbar. After creating + the toolbox, you can add your activity specific toolbars, for example the + EditToolbar. + + To add the ActivityToolbox to your Activity in MyActivity.__init__() do: + + # Create the Toolbar with the ActivityToolbar: + toolbox = activity.ActivityToolbox(self) + ... your code, inserting all other toolbars you need, like EditToolbar + + # Add the toolbox to the activity frame: + self.set_toolbox(toolbox) + # And make it visible: + toolbox.show() + """ + def __init__(self, activity): + Toolbox.__init__(self) + + self._activity_toolbar = ActivityToolbar(activity) + self.add_toolbar(_('Activity'), self._activity_toolbar) + self._activity_toolbar.show() + + def get_activity_toolbar(self): + return self._activity_toolbar + +class _ActivitySession(gobject.GObject): + __gsignals__ = { + 'quit-requested': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'quit': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._xsmp_client = XSMPClient() + self._xsmp_client.connect('quit-requested', self.__sm_quit_requested_cb) + self._xsmp_client.connect('quit', self.__sm_quit_cb) + self._xsmp_client.startup() + + self._activities = [] + self._will_quit = [] + + def register(self, activity): + self._activities.append(activity) + + def unregister(self, activity): + self._activities.remove(activity) + + if len(self._activities) == 0: + logging.debug('Quitting the activity process.') + gtk.main_quit() + + def will_quit(self, activity, will_quit): + if will_quit: + self._will_quit.append(activity) + + # We can quit only when all the instances agreed to + for activity in self._activities: + if activity not in self._will_quit: + return + + self._xsmp_client.will_quit(True) + else: + self._will_quit = [] + self._xsmp_client.will_quit(False) + + def __sm_quit_requested_cb(self, client): + self.emit('quit-requested') + + def __sm_quit_cb(self, client): + self.emit('quit') + +class Activity(Window, gtk.Container): + """This is the base Activity class that all other Activities derive from. + This is where your activity starts. + + To get a working Activity: + 0. Derive your Activity from this class: + class MyActivity(activity.Activity): + ... + + 1. implement an __init__() method for your Activity class. + + Use your init method to create your own ActivityToolbar which will + contain some standard buttons: + toolbox = activity.ActivityToolbox(self) + + Add extra Toolbars to your toolbox. + + You should setup Activity sharing here too. + + Finaly, your Activity may need some resources which you can claim + here too. + + The __init__() method is also used to make the distinction between + being resumed from the Journal, or starting with a blank document. + + 2. Implement read_file() and write_file() + Most activities revolve around creating and storing Journal entries. + For example, Write: You create a document, it is saved to the Journal + and then later you resume working on the document. + + read_file() and write_file() will be called by sugar to tell your + Activity that it should load or save the document the user is working + on. + + 3. Implement our Activity Toolbars. + The Toolbars are added to your Activity in step 1 (the toolbox), but + you need to implement them somewhere. Now is a good time. + + There are a number of standard Toolbars. The most basic one, the one + your almost absolutely MUST have is the ActivityToolbar. Without + this, you're not really making a proper Sugar Activity (which may be + okay, but you should really stop and think about why not!) You do + this with the ActivityToolbox(self) call in step 1. + + Usually, you will also need the standard EditToolbar. This is the one + which has the standard copy and paste buttons. You need to derive + your own EditToolbar class from sugar.EditToolbar: + class EditToolbar(activity.EditToolbar): + ... + + See EditToolbar for the methods you should implement in your class. + + Finaly, your Activity will very likely need some activity specific + buttons and options you can create your own toolbars by deriving a + class from gtk.Toolbar: + class MySpecialToolbar(gtk.Toolbar): + ... + + 4. Use your creativity. Make your Activity something special and share + it with your friends! + + Read through the methods of the Activity class below, to learn more about + how to make an Activity work. + + Hint: A good and simple Activity to learn from is the Read activity. To + create your own activity, you may want to copy it and use it as a template. + """ + __gtype_name__ = 'SugarActivity' + + __gsignals__ = { + 'shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self, handle, create_jobject=True): + """Initialise the Activity + + handle -- sugar.activity.activityhandle.ActivityHandle + instance providing the activity id and access to the + presence service which *may* provide sharing for this + application + + create_jobject -- boolean + define if it should create a journal object if we are + not resuming + + Side effects: + + Sets the gdk screen DPI setting (resolution) to the + Sugar screen resolution. + + Connects our "destroy" message to our _destroy_cb + method. + + Creates a base gtk.Window within this window. + + Creates an ActivityService (self._bus) servicing + this application. + + Usage: + If your Activity implements __init__(), it should call + the base class __init()__ before doing Activity specific things. + + """ + Window.__init__(self) + + # process titles will only show 15 characters + # but they get truncated anyway so if more characters + # are supported in the future we will get a better view + # of the processes + proc_title = "%s <%s>" % (get_bundle_name(), handle.activity_id) + util.set_proc_title(proc_title) + + self.connect('realize', self.__realize_cb) + self.connect('delete-event', self.__delete_event_cb) + + self._active = False + self._activity_id = handle.activity_id + self._pservice = presenceservice.get_instance() + self.shared_activity = None + self._share_id = None + self._join_id = None + self._updating_jobject = False + self._closing = False + self._quit_requested = False + self._deleting = False + self._max_participants = 0 + self._invites_queue = [] + self._jobject = None + + self._session = _get_session() + self._session.register(self) + self._session.connect('quit-requested', + self.__session_quit_requested_cb) + self._session.connect('quit', self.__session_quit_cb) + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self._bus = ActivityService(self) + self._owns_file = False + + share_scope = SCOPE_PRIVATE + + if handle.object_id: + self._jobject = datastore.get(handle.object_id) + self.set_title(self._jobject.metadata['title']) + + if self._jobject.metadata.has_key('share-scope'): + share_scope = self._jobject.metadata['share-scope'] + + # handle activity share/join + mesh_instance = self._pservice.get_activity(self._activity_id, + warn_if_none=False) + logging.debug("*** Act %s, mesh instance %r, scope %s", + self._activity_id, mesh_instance, share_scope) + if mesh_instance is not None: + # There's already an instance on the mesh, join it + logging.debug("*** Act %s joining existing mesh instance %r", + self._activity_id, mesh_instance) + self.shared_activity = mesh_instance + self.shared_activity.connect('notify::private', + self.__privacy_changed_cb) + self._join_id = self.shared_activity.connect("joined", + self.__joined_cb) + if not self.shared_activity.props.joined: + self.shared_activity.join() + else: + self.__joined_cb(self.shared_activity, True, None) + elif share_scope != SCOPE_PRIVATE: + logging.debug("*** Act %s no existing mesh instance, but used to " \ + "be shared, will share" % self._activity_id) + # no existing mesh instance, but activity used to be shared, so + # restart the share + if share_scope == SCOPE_INVITE_ONLY: + self.share(private=True) + elif share_scope == SCOPE_NEIGHBORHOOD: + self.share(private=False) + else: + logging.debug("Unknown share scope %r" % share_scope) + + if handle.object_id is None and create_jobject: + logging.debug('Creating a jobject.') + self._jobject = datastore.create() + title = _('%s Activity') % get_bundle_name() + self._jobject.metadata['title'] = title + self.set_title(self._jobject.metadata['title']) + self._jobject.metadata['title_set_by_user'] = '0' + self._jobject.metadata['activity'] = self.get_bundle_id() + self._jobject.metadata['activity_id'] = self.get_id() + self._jobject.metadata['keep'] = '0' + self._jobject.metadata['preview'] = '' + self._jobject.metadata['share-scope'] = SCOPE_PRIVATE + if self.shared_activity is not None: + icon_color = self.shared_activity.props.color + else: + client = gconf.client_get_default() + icon_color = client.get_string('/desktop/sugar/user/color') + self._jobject.metadata['icon-color'] = icon_color + + self._jobject.file_path = '' + # Cannot call datastore.write async for creates: + # https://dev.laptop.org/ticket/3071 + datastore.write(self._jobject) + + def get_active(self): + return self._active + + def set_active(self, active): + if self._active != active: + self._active = active + if not self._active and self._jobject: + self.save() + + active = gobject.property( + type=bool, default=False, getter=get_active, setter=set_active) + + def get_max_participants(self): + return self._max_participants + + def set_max_participants(self, participants): + self._max_participants = participants + + max_participants = gobject.property( + type=int, default=0, getter=get_max_participants, + setter=set_max_participants) + + def get_id(self): + """Returns the activity id of the current instance of your activity. + + The activity id is sort-of-like the unix process id (PID). However, + unlike PIDs it is only different for each new instance (with + create_jobject = True set) and stays the same everytime a user + resumes an activity. This is also the identity of your Activity to other + XOs for use when sharing. + """ + return self._activity_id + + def get_bundle_id(self): + """Returns the bundle_id from the activity.info file""" + return os.environ['SUGAR_BUNDLE_ID'] + + def set_canvas(self, canvas): + """Sets the 'work area' of your activity with the canvas of your choice. + + One commonly used canvas is gtk.ScrolledWindow + """ + Window.set_canvas(self, canvas) + canvas.connect('map', self.__canvas_map_cb) + + def __session_quit_requested_cb(self, session): + self._quit_requested = True + + if not self._prepare_close(): + session.will_quit(self, False) + elif not self._updating_jobject: + session.will_quit(self, True) + + def __session_quit_cb(self, client): + self._complete_close() + + def __canvas_map_cb(self, canvas): + if self._jobject and self._jobject.file_path: + self.read_file(self._jobject.file_path) + + def __jobject_create_cb(self): + pass + + def __jobject_error_cb(self, err): + logging.debug("Error creating activity datastore object: %s" % err) + + def get_activity_root(self): + """ FIXME: Deprecated. This part of the API has been moved + out of this class to the module itself + + Returns a path for saving Activity specific preferences, etc. + + Returns a path to the location in the filesystem where the activity can + store activity related data that doesn't pertain to the current + execution of the activity and thus cannot go into the DataStore. + + Currently, this will return something like + ~/.sugar/default/MyActivityName/ + + Activities should ONLY save settings, user preferences and other data + which isn't specific to a journal item here. If (meta-)data is in anyway + specific to a journal entry, it MUST be stored in the DataStore. + """ + if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \ + os.environ['SUGAR_ACTIVITY_ROOT']: + return os.environ['SUGAR_ACTIVITY_ROOT'] + else: + return '/' + + def read_file(self, file_path): + """ + Subclasses implement this method if they support resuming objects from + the journal. 'file_path' is the file to read from. + + You should immediately open the file from the file_path, because the + file_name will be deleted immediately after returning from read_file(). + Once the file has been opened, you do not have to read it immediately: + After you have opened it, the file will only be really gone when you + close it. + + Although not required, this is also a good time to read all meta-data: + the file itself cannot be changed externally, but the title, description + and other metadata['tags'] may change. So if it is important for you to + notice changes, this is the time to record the originals. + """ + raise NotImplementedError + + def write_file(self, file_path): + """ + Subclasses implement this method if they support saving data to objects + in the journal. 'file_path' is the file to write to. + + If the user did make changes, you should create the file_path and save + all document data to it. + + Additionally, you should also write any metadata needed to resume your + activity. For example, the Read activity saves the current page and zoom + level, so it can display the page. + + Note: Currently, the file_path *WILL* be different from the one you + received in file_read(). Even if you kept the file_path from file_read() + open until now, you must still write the entire file to this file_path. + """ + raise NotImplementedError + + def __save_cb(self): + logging.debug('Activity.__save_cb') + self._updating_jobject = False + if self._quit_requested: + self._session.will_quit(self, True) + elif self._closing: + self._complete_close() + + def __save_error_cb(self, err): + logging.debug('Activity.__save_error_cb') + self._updating_jobject = False + if self._quit_requested: + self._session.will_quit(self, False) + if self._closing: + self._show_keep_failed_dialog() + self._closing = False + logging.debug("Error saving activity object to datastore: %s" % err) + + def _cleanup_jobject(self): + if self._jobject: + if self._owns_file and os.path.isfile(self._jobject.file_path): + logging.debug('_cleanup_jobject: removing %r' % + self._jobject.file_path) + os.remove(self._jobject.file_path) + self._owns_file = False + self._jobject.destroy() + self._jobject = None + + def _get_preview(self): + + if self.canvas is None or not hasattr(self.canvas, 'get_snapshot'): + return None + pixmap = self.canvas.get_snapshot((-1, -1, 0, 0)) + + width, height = pixmap.get_size() + pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, width, height) + pixbuf = pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), + 0, 0, 0, 0, width, height) + pixbuf = pixbuf.scale_simple(style.zoom(300), style.zoom(225), + gtk.gdk.INTERP_BILINEAR) + + preview_data = [] + def save_func(buf, data): + data.append(buf) + + pixbuf.save_to_callback(save_func, 'png', user_data=preview_data) + preview_data = ''.join(preview_data) + + return preview_data + + def _get_buddies(self): + if self.shared_activity is not None: + buddies = {} + for buddy in self.shared_activity.get_joined_buddies(): + if not buddy.props.owner: + buddy_id = sha1(buddy.props.key).hexdigest() + buddies[buddy_id] = [buddy.props.nick, buddy.props.color] + return buddies + else: + return {} + + def save(self): + """Request that the activity is saved to the Journal. + + This method is called by the close() method below. In general, + activities should not override this method. This method is part of the + public API of an Acivity, and should behave in standard ways. Use your + own implementation of write_file() to save your Activity specific data. + """ + + if self._jobject is None: + logging.debug('Cannot save, no journal object.') + return + + logging.debug('Activity.save: %r' % self._jobject.object_id) + + if self._updating_jobject: + logging.info('Activity.save: still processing a previous request.') + return + + buddies_dict = self._get_buddies() + if buddies_dict: + self.metadata['buddies_id'] = cjson.encode(buddies_dict.keys()) + self.metadata['buddies'] = cjson.encode(self._get_buddies()) + + preview = self._get_preview() + if preview is not None: + self.metadata['preview'] = dbus.ByteArray(preview) + + try: + file_path = os.path.join(self.get_activity_root(), 'instance', + '%i' % time.time()) + self.write_file(file_path) + self._owns_file = True + self._jobject.file_path = file_path + except NotImplementedError: + logging.debug('Activity.write_file is not implemented.') + + # Cannot call datastore.write async for creates: + # https://dev.laptop.org/ticket/3071 + if self._jobject.object_id is None: + datastore.write(self._jobject, transfer_ownership=True) + else: + self._updating_jobject = True + datastore.write(self._jobject, + transfer_ownership=True, + reply_handler=self.__save_cb, + error_handler=self.__save_error_cb) + + def copy(self): + """Request that the activity 'Keep in Journal' the current state + of the activity. + + Activities should not override this method. Instead, like save() do any + copy work that needs to be done in write_file() + """ + logging.debug('Activity.copy: %r' % self._jobject.object_id) + self.save() + self._jobject.object_id = None + + def __privacy_changed_cb(self, shared_activity, param_spec): + if shared_activity.props.private: + self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY + else: + self._jobject.metadata['share-scope'] = SCOPE_NEIGHBORHOOD + + def __joined_cb(self, activity, success, err): + """Callback when join has finished""" + self.shared_activity.disconnect(self._join_id) + self._join_id = None + if not success: + logging.debug("Failed to join activity: %s" % err) + return + + self.present() + self.emit('joined') + self.__privacy_changed_cb(self.shared_activity, None) + + def get_shared_activity(self): + """Returns an instance of the shared Activity or None + + The shared activity is of type sugar.presence.activity.Activity + """ + return self._shared_activity + + def get_shared(self): + """Returns TRUE if the activity is shared on the mesh.""" + if not self.shared_activity: + return False + return self.shared_activity.props.joined + + def __share_cb(self, ps, success, activity, err): + self._pservice.disconnect(self._share_id) + self._share_id = None + if not success: + logging.debug('Share of activity %s failed: %s.' % + (self._activity_id, err)) + return + + logging.debug('Share of activity %s successful, PS activity is %r.', + self._activity_id, activity) + + activity.props.name = self._jobject.metadata['title'] + + self.shared_activity = activity + self.shared_activity.connect('notify::private', + self.__privacy_changed_cb) + self.emit('shared') + self.__privacy_changed_cb(self.shared_activity, None) + + self._send_invites() + + def _invite_response_cb(self, error): + if error: + logging.error('Invite failed: %s' % error) + + def _send_invites(self): + while self._invites_queue: + buddy_key = self._invites_queue.pop() + buddy = self._pservice.get_buddy(buddy_key) + if buddy: + self.shared_activity.invite( + buddy, '', self._invite_response_cb) + else: + logging.error('Cannot invite %s, no such buddy.' % buddy_key) + + def invite(self, buddy_key): + """Invite a buddy to join this Activity. + + Side Effects: + Calls self.share(True) to privately share the activity if it wasn't + shared before. + """ + self._invites_queue.append(buddy_key) + + if (self.shared_activity is None + or not self.shared_activity.props.joined): + self.share(True) + else: + self._send_invites() + + def share(self, private=False): + """Request that the activity be shared on the network. + + private -- bool: True to share by invitation only, + False to advertise as shared to everyone. + + Once the activity is shared, its privacy can be changed by setting + its 'private' property. + """ + if self.shared_activity and self.shared_activity.props.joined: + raise RuntimeError("Activity %s already shared." % + self._activity_id) + verb = private and 'private' or 'public' + logging.debug('Requesting %s share of activity %s.' % + (verb, self._activity_id)) + self._share_id = self._pservice.connect("activity-shared", + self.__share_cb) + self._pservice.share_activity(self, private=private) + + def _show_keep_failed_dialog(self): + alert = Alert() + alert.props.title = _('Keep error') + alert.props.msg = _('Keep error: all changes will be lost') + + cancel_icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Don\'t stop'), cancel_icon) + + stop_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Stop anyway'), stop_icon) + + self.add_alert(alert) + alert.connect('response', self._keep_failed_dialog_response_cb) + + self.present() + + def _keep_failed_dialog_response_cb(self, alert, response_id): + self.remove_alert(alert) + if response_id == gtk.RESPONSE_OK: + self.close(skip_save=True) + + def can_close(self): + """Activities should override this function if they want to perform + extra checks before actually closing.""" + + return True + + def _prepare_close(self, skip_save=False): + if not skip_save: + try: + self.save() + except Exception: + logging.info(traceback.format_exc()) + self._show_keep_failed_dialog() + return False + + if self.shared_activity: + self.shared_activity.leave() + + self._closing = True + + return True + + def _complete_close(self): + self._cleanup_jobject() + self.destroy() + + # Make the exported object inaccessible + dbus.service.Object.remove_from_connection(self._bus) + + self._session.unregister(self) + + def close(self, skip_save=False): + """Request that the activity be stopped and saved to the Journal + + Activities should not override this method, but should implement + write_file() to do any state saving instead. If the application wants + to control wether it can close, it should override can_close(). + """ + if not self.can_close(): + return + + if skip_save or self.metadata.get('title_set_by_user', '0') == '1': + if not self._closing: + if not self._prepare_close(skip_save): + return + + if not self._updating_jobject: + self._complete_close() + else: + title_alert = NamingAlert(self, get_bundle_path()) + title_alert.set_transient_for(self.get_toplevel()) + title_alert.show() + + def __realize_cb(self, window): + wm.set_bundle_id(window.window, self.get_bundle_id()) + wm.set_activity_id(window.window, str(self._activity_id)) + + def __delete_event_cb(self, widget, event): + self.close() + return True + + def get_metadata(self): + """Returns the jobject metadata or None if there is no jobject. + + Activities can set metadata in write_file() using: + self.metadata['MyKey'] = "Something" + + and retrieve metadata in read_file() using: + self.metadata.get('MyKey', 'aDefaultValue') + + Note: Make sure your activity works properly if one or more of the + metadata items is missing. Never assume they will all be present. + """ + if self._jobject: + return self._jobject.metadata + else: + return None + + metadata = property(get_metadata, None) + + def handle_view_source(self): + raise NotImplementedError + + def get_document_path(self, async_cb, async_err_cb): + async_err_cb(NotImplementedError()) + + # DEPRECATED + _shared_activity = property(lambda self: self.shared_activity, None) + +_session = None + +def _get_session(): + global _session + + if _session is None: + _session = _ActivitySession() + + return _session + +def get_bundle_name(): + """Return the bundle name for the current process' bundle""" + return os.environ['SUGAR_BUNDLE_NAME'] + +def get_bundle_path(): + """Return the bundle path for the current process' bundle""" + return os.environ['SUGAR_BUNDLE_PATH'] + +def get_activity_root(): + """Returns a path for saving Activity specific preferences, etc.""" + if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \ + os.environ['SUGAR_ACTIVITY_ROOT']: + return os.environ['SUGAR_ACTIVITY_ROOT'] + else: + raise RuntimeError("No SUGAR_ACTIVITY_ROOT set.") + +def show_object_in_journal(object_id): + bus = dbus.SessionBus() + obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) + journal = dbus.Interface(obj, J_DBUS_INTERFACE) + journal.ShowObject(object_id) diff --git a/src/sugar/activity/activityfactory.py b/src/sugar/activity/activityfactory.py new file mode 100644 index 0000000..e92314d --- /dev/null +++ b/src/sugar/activity/activityfactory.py @@ -0,0 +1,343 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Shell side object which manages request to start activity + +UNSTABLE. Activities are currently not allowed to run other activities so at +the moment there is no reason to stabilize this API. +""" + +import logging + +import dbus +import gobject + +from sugar.presence import presenceservice +from sugar.activity.activityhandle import ActivityHandle +from sugar import util +from sugar import env + +from errno import EEXIST, ENOSPC + +import os + +_SHELL_SERVICE = "org.laptop.Shell" +_SHELL_PATH = "/org/laptop/Shell" +_SHELL_IFACE = "org.laptop.Shell" + +_DS_SERVICE = "org.laptop.sugar.DataStore" +_DS_INTERFACE = "org.laptop.sugar.DataStore" +_DS_PATH = "/org/laptop/sugar/DataStore" + +_ACTIVITY_FACTORY_INTERFACE = "org.laptop.ActivityFactory" + +_RAINBOW_SERVICE_NAME = "org.laptop.security.Rainbow" +_RAINBOW_ACTIVITY_FACTORY_PATH = "/" +_RAINBOW_ACTIVITY_FACTORY_INTERFACE = "org.laptop.security.Rainbow" + +# helper method to close all filedescriptors +# borrowed from subprocess.py +try: + MAXFD = os.sysconf("SC_OPEN_MAX") +except ValueError: + MAXFD = 256 +def _close_fds(): + for i in xrange(3, MAXFD): + try: + os.close(i) + # pylint: disable-msg=W0704 + except Exception: + pass + +def create_activity_id(): + """Generate a new, unique ID for this activity""" + pservice = presenceservice.get_instance() + + # create a new unique activity ID + i = 0 + act_id = None + while i < 10: + act_id = util.unique_id() + i += 1 + + # check through network activities + found = False + activities = pservice.get_activities() + for act in activities: + if act_id == act.props.id: + found = True + break + if not found: + return act_id + raise RuntimeError("Cannot generate unique activity id.") + +def get_environment(activity): + environ = os.environ.copy() + + bin_path = os.path.join(activity.get_path(), 'bin') + + activity_root = env.get_profile_path(activity.get_bundle_id()) + if not os.path.exists(activity_root): + os.mkdir(activity_root) + + data_dir = os.path.join(activity_root, 'instance') + if not os.path.exists(data_dir): + os.mkdir(data_dir) + + data_dir = os.path.join(activity_root, 'data') + if not os.path.exists(data_dir): + os.mkdir(data_dir) + + tmp_dir = os.path.join(activity_root, 'tmp') + if not os.path.exists(tmp_dir): + os.mkdir(tmp_dir) + + environ['SUGAR_BUNDLE_PATH'] = activity.get_path() + environ['SUGAR_BUNDLE_ID'] = activity.get_bundle_id() + environ['SUGAR_ACTIVITY_ROOT'] = activity_root + environ['PATH'] = bin_path + ':' + environ['PATH'] + #environ['RAINBOW_STRACE_LOG'] = '1' + + if activity.get_path().startswith(env.get_user_activities_path()): + environ['SUGAR_LOCALEDIR'] = os.path.join(activity.get_path(), 'locale') + + if activity.get_bundle_id() in [ 'org.laptop.WebActivity', + 'org.laptop.GmailActivity', + 'org.laptop.WikiBrowseActivity' + ]: + environ['RAINBOW_CONSTANT_UID'] = 'yes' + + return environ + +def get_command(activity, activity_id=None, object_id=None, uri=None): + if not activity_id: + activity_id = create_activity_id() + + command = activity.get_command().split(' ') + command.extend(['-b', activity.get_bundle_id()]) + command.extend(['-a', activity_id]) + + if object_id is not None: + command.extend(['-o', object_id]) + if uri is not None: + command.extend(['-u', uri]) + + # if the command is in $BUNDLE_ROOT/bin, execute the absolute path so there + # is no need to mangle with the shell's PATH + if '/' not in command[0]: + bin_path = os.path.join(activity.get_path(), 'bin') + absolute_path = os.path.join(bin_path, command[0]) + if os.path.exists(absolute_path): + command[0] = absolute_path + + logging.debug('launching: %r' % command) + + return command + +def open_log_file(activity): + i = 1 + while True: + path = env.get_logs_path('%s-%s.log' % (activity.get_bundle_id(), i)) + try: + fd = os.open(path, os.O_EXCL | os.O_CREAT \ + | os.O_SYNC | os.O_WRONLY, 0644) + f = os.fdopen(fd, 'w', 0) + return (path, f) + except OSError, e: + if e.errno == EEXIST: + i += 1 + elif e.errno == ENOSPC: + # not the end of the world; let's try to keep going. + return ('/dev/null', open('/dev/null','w')) + else: + raise e + +class ActivityCreationHandler(gobject.GObject): + """Sugar-side activity creation interface + + This object uses a dbus method on the ActivityFactory + service to create the new activity. It generates + GObject events in response to the success/failure of + activity startup using callbacks to the service's + create call. + """ + + def __init__(self, bundle, handle): + """Initialise the handler + + bundle -- the ActivityBundle to launch + activity_handle -- stores the values which are to + be passed to the service to uniquely identify + the activity to be created and the sharing + service that may or may not be connected with it + + sugar.activity.activityhandle.ActivityHandle instance + + calls the "create" method on the service for this + particular activity type and registers the + _reply_handler and _error_handler methods on that + call's results. + + The specific service which creates new instances of this + particular type of activity is created during the activity + registration process in shell bundle registry which creates + service definition files for each registered bundle type. + + If the file '/etc/olpc-security' exists, then activity launching + will be delegated to the prototype 'Rainbow' security service. + """ + gobject.GObject.__init__(self) + + self._bundle = bundle + self._service_name = bundle.get_bundle_id() + self._handle = handle + + self._use_rainbow = os.path.exists('/etc/olpc-security') + if self._service_name in [ 'org.laptop.JournalActivity', + 'org.laptop.Terminal', + 'org.laptop.Log', + 'org.laptop.Analyze' + ]: + self._use_rainbow = False + + bus = dbus.SessionBus() + + bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH) + self._shell = dbus.Interface(bus_object, _SHELL_IFACE) + + if handle.activity_id is not None and \ + handle.object_id is None: + datastore = dbus.Interface( + bus.get_object(_DS_SERVICE, _DS_PATH), _DS_INTERFACE) + datastore.find({ 'activity_id': self._handle.activity_id }, [], + reply_handler=self._find_object_reply_handler, + error_handler=self._find_object_error_handler, + byte_arrays=True) + else: + self._launch_activity() + + def _launch_activity(self): + if self._handle.activity_id != None: + self._shell.ActivateActivity(self._handle.activity_id, + reply_handler=self._activate_reply_handler, + error_handler=self._activate_error_handler) + else: + self._create_activity() + + def _create_activity(self): + if self._handle.activity_id is None: + self._handle.activity_id = create_activity_id() + + self._shell.NotifyLaunch( + self._service_name, self._handle.activity_id, + reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_error_handler) + + environ = get_environment(self._bundle) + (log_path, log_file) = open_log_file(self._bundle) + command = get_command(self._bundle, self._handle.activity_id, + self._handle.object_id, + self._handle.uri) + + if not self._use_rainbow: + # use gobject spawn functionality, so that zombies are + # automatically reaped by the gobject event loop. + def child_setup(): + # clone logfile.fileno() onto stdout/stderr + os.dup2(log_file.fileno(), 1) + os.dup2(log_file.fileno(), 2) + # close all other fds + _close_fds() + # we need to sanitize and str-ize the various bits which + # dbus gives us. + gobject.spawn_async([str(s) for s in command], + envp=['%s=%s' % (k, str(v)) + for k, v in environ.items()], + working_directory=str(self._bundle.get_path()), + child_setup=child_setup, + flags=(gobject.SPAWN_SEARCH_PATH | + gobject.SPAWN_LEAVE_DESCRIPTORS_OPEN)) + log_file.close() + else: + log_file.close() + system_bus = dbus.SystemBus() + factory = system_bus.get_object(_RAINBOW_SERVICE_NAME, + _RAINBOW_ACTIVITY_FACTORY_PATH) + factory.CreateActivity( + log_path, + environ, + command, + environ['SUGAR_BUNDLE_PATH'], + environ['SUGAR_BUNDLE_ID'], + timeout=30, + reply_handler=self._create_reply_handler, + error_handler=self._create_error_handler, + dbus_interface=_RAINBOW_ACTIVITY_FACTORY_INTERFACE) + + def _no_reply_handler(self, *args): + pass + + def _notify_launch_failure_error_handler(self, err): + logging.error('Notify launch failure failed %s' % err) + + def _notify_launch_error_handler(self, err): + logging.debug('Notify launch failed %s' % err) + + def _activate_reply_handler(self, activated): + if not activated: + self._create_activity() + + def _activate_error_handler(self, err): + logging.error("Activity activation request failed %s" % err) + + def _create_reply_handler(self): + logging.debug("Activity created %s (%s)." % + (self._handle.activity_id, self._service_name)) + + def _create_error_handler(self, err): + logging.error("Couldn't create activity %s (%s): %s" % + (self._handle.activity_id, self._service_name, err)) + self._shell.NotifyLaunchFailure( + self._handle.activity_id, reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_failure_error_handler) + + def _find_object_reply_handler(self, jobjects, count): + if count > 0: + if count > 1: + logging.debug("Multiple objects has the same activity_id.") + self._handle.object_id = jobjects[0]['uid'] + self._create_activity() + + def _find_object_error_handler(self, err): + logging.error("Datastore find failed %s" % err) + self._create_activity() + +def create(bundle, activity_handle=None): + """Create a new activity from its name.""" + if not activity_handle: + activity_handle = ActivityHandle() + return ActivityCreationHandler(bundle, activity_handle) + +def create_with_uri(bundle, uri): + """Create a new activity and pass the uri as handle.""" + activity_handle = ActivityHandle(uri=uri) + return ActivityCreationHandler(bundle, activity_handle) + +def create_with_object_id(bundle, object_id): + """Create a new activity and pass the object id as handle.""" + activity_handle = ActivityHandle(object_id=object_id) + return ActivityCreationHandler(bundle, activity_handle) diff --git a/src/sugar/activity/activityhandle.py b/src/sugar/activity/activityhandle.py new file mode 100644 index 0000000..f255fd5 --- /dev/null +++ b/src/sugar/activity/activityhandle.py @@ -0,0 +1,70 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +""" +STABLE. +""" + +class ActivityHandle(object): + """Data structure storing simple activity metadata""" + def __init__( + self, activity_id=None, object_id=None, uri=None + ): + """Initialise the handle from activity_id + + activity_id -- unique id for the activity to be + created + object_id -- identity of the journal object + associated with the activity. It was used by + the journal prototype implementation, might + change when we do the real one. + + When you resume an activity from the journal + the object_id will be passed in. It's optional + since new activities does not have an + associated object (yet). + + XXX Not clear how this relates to the activity + id yet, i.e. not sure we really need both. TBF + uri -- URI associated with the activity. Used when + opening an external file or resource in the + activity, rather than a journal object + (downloads stored on the file system for + example or web pages) + """ + self.activity_id = activity_id + self.object_id = object_id + self.uri = uri + + def get_dict(self): + """Retrieve our settings as a dictionary""" + result = { 'activity_id' : self.activity_id } + if self.object_id: + result['object_id'] = self.object_id + if self.uri: + result['uri'] = self.uri + + return result + +def create_from_dict(handle_dict): + """Create a handle from a dictionary of parameters""" + result = ActivityHandle( + handle_dict['activity_id'], + object_id = handle_dict.get('object_id'), + uri = handle_dict.get('uri'), + ) + return result diff --git a/src/sugar/activity/activityservice.py b/src/sugar/activity/activityservice.py new file mode 100644 index 0000000..ff806f3 --- /dev/null +++ b/src/sugar/activity/activityservice.py @@ -0,0 +1,82 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +""" +UNSTABLE. It should really be internal to the Activity class. +""" + +import logging + +import dbus +import dbus.service + +_ACTIVITY_SERVICE_NAME = "org.laptop.Activity" +_ACTIVITY_SERVICE_PATH = "/org/laptop/Activity" +_ACTIVITY_INTERFACE = "org.laptop.Activity" + +class ActivityService(dbus.service.Object): + """Base dbus service object that each Activity uses to export dbus methods. + + The dbus service is separate from the actual Activity object so that we can + tightly control what stuff passes through the dbus python bindings.""" + + def __init__(self, activity): + """Initialise the service for the given activity + + activity -- sugar.activity.activity.Activity instance + + Creates dbus services that use the instance's activity_id + as discriminants among all active services + of this type. That is, the services are all available + as names/paths derived from the instance's activity_id. + + The various methods exposed on dbus are just forwarded + to the client Activity object's equally-named methods. + """ + activity.realize() + + activity_id = activity.get_id() + service_name = _ACTIVITY_SERVICE_NAME + activity_id + object_path = _ACTIVITY_SERVICE_PATH + "/" + activity_id + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(service_name, bus=bus) + dbus.service.Object.__init__(self, bus_name, object_path) + + self._activity = activity + + @dbus.service.method(_ACTIVITY_INTERFACE) + def SetActive(self, active): + logging.debug('ActivityService.set_active: %s.' % active) + self._activity.props.active = active + + @dbus.service.method(_ACTIVITY_INTERFACE) + def Invite(self, buddy_key): + self._activity.invite(buddy_key) + + @dbus.service.method(_ACTIVITY_INTERFACE) + def HandleViewSource(self): + self._activity.handle_view_source() + + @dbus.service.method(_ACTIVITY_INTERFACE, + async_callbacks=('async_cb', 'async_err_cb')) + def GetDocumentPath(self, async_cb, async_err_cb): + try: + self._activity.get_document_path(async_cb, async_err_cb) + except Exception, e: + async_err_cb(e) + diff --git a/src/sugar/activity/bundlebuilder.py b/src/sugar/activity/bundlebuilder.py new file mode 100644 index 0000000..ab3679b --- /dev/null +++ b/src/sugar/activity/bundlebuilder.py @@ -0,0 +1,398 @@ +# Copyright (C) 2008 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +""" +STABLE. +""" + +import os +import sys +import zipfile +import tarfile +import shutil +import subprocess +import re +import gettext +from optparse import OptionParser +import logging +from fnmatch import fnmatch + +from sugar import env +from sugar.bundle.activitybundle import ActivityBundle + +IGNORE_DIRS = ['dist', '.git'] +IGNORE_FILES = ['.gitignore', 'MANIFEST', '*.pyc', '*~', '*.bak', 'pseudo.po'] + +def list_files(base_dir, ignore_dirs=None, ignore_files=None): + result = [] + + for root, dirs, files in os.walk(base_dir): + if ignore_files: + for pattern in ignore_files: + files = [f for f in files if not fnmatch(f, pattern)] + + rel_path = root[len(base_dir) + 1:] + for f in files: + result.append(os.path.join(rel_path, f)) + + if ignore_dirs and root == base_dir: + for ignore in ignore_dirs: + if ignore in dirs: + dirs.remove(ignore) + + return result + +class Config(object): + def __init__(self, source_dir=None, dist_dir = None, dist_name = None): + self.source_dir = source_dir or os.getcwd() + self.dist_dir = dist_dir or os.path.join(self.source_dir, 'dist') + self.dist_name = dist_name + self.bundle = None + self.version = None + self.activity_name = None + self.bundle_id = None + self.bundle_name = None + self.bundle_root_dir = None + self.tar_root_dir = None + self.xo_name = None + self.tar_name = None + + self.update() + + def update(self): + self.bundle = bundle = ActivityBundle(self.source_dir) + self.version = bundle.get_activity_version() + self.activity_name = bundle.get_name() + self.bundle_id = bundle.get_bundle_id() + self.bundle_name = reduce(lambda x, y:x+y, self.activity_name.split()) + self.bundle_root_dir = self.bundle_name + '.activity' + self.tar_root_dir = '%s-%d' % (self.bundle_name, self.version) + + if self.dist_name: + self.xo_name = self.tar_name = self.dist_name + else: + self.xo_name = '%s-%d.xo' % (self.bundle_name, self.version) + self.tar_name = '%s-%d.tar.bz2' % (self.bundle_name, self.version) + +class Builder(object): + def __init__(self, config): + self.config = config + + def build(self): + self.build_locale() + + def build_locale(self): + po_dir = os.path.join(self.config.source_dir, 'po') + + if not self.config.bundle.is_dir(po_dir): + logging.warn("Missing po/ dir, cannot build_locale") + return + + locale_dir = os.path.join(self.config.source_dir, 'locale') + + if os.path.exists(locale_dir): + shutil.rmtree(locale_dir) + + for f in os.listdir(po_dir): + if not f.endswith('.po') or f == 'pseudo.po': + continue + + file_name = os.path.join(po_dir, f) + lang = f[:-3] + + localedir = os.path.join(self.config.source_dir, 'locale', lang) + mo_path = os.path.join(localedir, 'LC_MESSAGES') + if not os.path.isdir(mo_path): + os.makedirs(mo_path) + + mo_file = os.path.join(mo_path, "%s.mo" % self.config.bundle_id) + args = ["msgfmt", "--output-file=%s" % mo_file, file_name] + retcode = subprocess.call(args) + if retcode: + print 'ERROR - msgfmt failed with return code %i.' % retcode + + cat = gettext.GNUTranslations(open(mo_file, 'r')) + translated_name = cat.gettext(self.config.activity_name) + linfo_file = os.path.join(localedir, 'activity.linfo') + f = open(linfo_file, 'w') + f.write('[Activity]\nname = %s\n' % translated_name) + f.close() + + def get_files(self): + files = self.config.bundle.get_files() + + if not files: + logging.error('No files found, fixing the MANIFEST.') + self.fix_manifest() + files = self.config.bundle.get_files() + + return files + + def check_manifest(self): + missing_files = [] + + allfiles = list_files(self.config.source_dir, + IGNORE_DIRS, IGNORE_FILES) + for path in allfiles: + if path not in self.config.bundle.manifest: + missing_files.append(path) + + return missing_files + + def fix_manifest(self): + self.build() + + manifest = self.config.bundle.manifest + + for path in self.check_manifest(): + manifest.append(path) + + f = open(os.path.join(self.config.source_dir, "MANIFEST"), "wb") + for line in manifest: + f.write(line + "\n") + +class Packager(object): + def __init__(self, config): + self.config = config + self.package_path = None + + if not os.path.exists(self.config.dist_dir): + os.mkdir(self.config.dist_dir) + +class XOPackager(Packager): + def __init__(self, builder): + Packager.__init__(self, builder.config) + + self.builder = builder + self.package_path = os.path.join(self.config.dist_dir, + self.config.xo_name) + + def package(self): + bundle_zip = zipfile.ZipFile(self.package_path, 'w', + zipfile.ZIP_DEFLATED) + + missing_files = self.builder.check_manifest() + if missing_files: + logging.warn('These files are not included in the manifest ' \ + 'and will not be present in the bundle:\n\n' + + '\n'.join(missing_files) + + '\n\nUse fix_manifest if you want to add them.') + + for f in self.builder.get_files(): + bundle_zip.write(os.path.join(self.config.source_dir, f), + os.path.join(self.config.bundle_root_dir, f)) + + bundle_zip.close() + +class SourcePackager(Packager): + def __init__(self, config): + Packager.__init__(self, config) + self.package_path = os.path.join(self.config.dist_dir, + self.config.tar_name) + + def get_files(self): + git_ls = subprocess.Popen('git-ls-files', stdout=subprocess.PIPE, + cwd=self.config.source_dir) + if git_ls.wait(): + # Fall back to filtered list + return list_files(self.config.source_dir, + IGNORE_DIRS, IGNORE_FILES) + + return [path.strip() for path in git_ls.stdout.readlines()] + + def package(self): + tar = tarfile.open(self.package_path, 'w:bz2') + for f in self.get_files(): + tar.add(os.path.join(self.config.source_dir, f), + os.path.join(self.config.tar_root_dir, f)) + tar.close() + +class Installer(object): + IGNORES = [ 'po/*', 'MANIFEST', 'AUTHORS' ] + + def __init__(self, builder): + self.config = builder.config + self.builder = builder + + def should_ignore(self, f): + for pattern in self.IGNORES: + if fnmatch(f, pattern): + return True + return False + + def install(self, prefix): + self.builder.build() + + activity_path = os.path.join(prefix, 'share', 'sugar', 'activities', + self.config.bundle_root_dir) + + source_to_dest = {} + for f in self.builder.get_files(): + if self.should_ignore(f): + pass + elif f.startswith('locale/') and f.endswith('.mo'): + source_to_dest[f] = os.path.join(prefix, 'share', f) + else: + source_to_dest[f] = os.path.join(activity_path, f) + + for source, dest in source_to_dest.items(): + print 'Install %s to %s.' % (source, dest) + + path = os.path.dirname(dest) + if not os.path.exists(path): + os.makedirs(path) + + shutil.copy(source, dest) + +def cmd_dev(config, args): + '''Setup for development''' + + if args: + print 'Usage: %prog dev' + return + + bundle_path = env.get_user_activities_path() + if not os.path.isdir(bundle_path): + os.mkdir(bundle_path) + bundle_path = os.path.join(bundle_path, config.bundle_root_dir) + try: + os.symlink(config.source_dir, bundle_path) + except OSError: + if os.path.islink(bundle_path): + print 'ERROR - The bundle has been already setup for development.' + else: + print 'ERROR - A bundle with the same name is already installed.' + +def cmd_dist_xo(config, args): + '''Create a xo bundle package''' + + if args: + print 'Usage: %prog dist_xo' + return + + packager = XOPackager(Builder(config)) + packager.package() + +def cmd_fix_manifest(config, args): + '''Add missing files to the manifest''' + + if args: + print 'Usage: %prog fix_manifest' + return + + builder = Builder(config) + builder.fix_manifest() + +def cmd_dist_source(config, args): + '''Create a tar source package''' + + if args: + print 'Usage: %prog dist_source' + return + + packager = SourcePackager(config) + packager.package() + +def cmd_install(config, args): + '''Install the activity in the system''' + + parser = OptionParser(usage='usage: %prog install [options]') + parser.add_option('--prefix', dest='prefix', default=sys.prefix, + help='Prefix to install files to') + (suboptions, subargs) = parser.parse_args(args) + if subargs: + parser.print_help() + return + + installer = Installer(Builder(config)) + installer.install(suboptions.prefix) + +def cmd_genpot(config, args): + '''Generate the gettext pot file''' + + if args: + print 'Usage: %prog genpot' + return + + po_path = os.path.join(config.source_dir, 'po') + if not os.path.isdir(po_path): + os.mkdir(po_path) + + python_files = [] + for root_dummy, dirs_dummy, files in os.walk(config.source_dir): + for file_name in files: + if file_name.endswith('.py'): + python_files.append(file_name) + + # First write out a stub .pot file containing just the translated + # activity name, then have xgettext merge the rest of the + # translations into that. (We can't just append the activity name + # to the end of the .pot file afterwards, because that might + # create a duplicate msgid.) + pot_file = os.path.join('po', '%s.pot' % config.bundle_name) + escaped_name = re.sub('([\\\\"])', '\\\\\\1', config.activity_name) + f = open(pot_file, 'w') + f.write('#: activity/activity.info:2\n') + f.write('msgid "%s"\n' % escaped_name) + f.write('msgstr ""\n') + f.close() + + args = [ 'xgettext', '--join-existing', '--language=Python', + '--keyword=_', '--add-comments=TRANS:', '--output=%s' % pot_file ] + + args += python_files + retcode = subprocess.call(args) + if retcode: + print 'ERROR - xgettext failed with return code %i.' % retcode + +def cmd_build(config, args): + '''Build generated files''' + + if args: + print 'Usage: %prog build' + return + + builder = Builder(config) + builder.build() + +def print_commands(): + print 'Available commands:\n' + + for name, func in globals().items(): + if name.startswith('cmd_'): + print "%-20s %s" % (name.replace('cmd_', ''), func.__doc__) + + print '\n(Type "./setup.py --help" for help about a ' \ + 'particular command\'s options.' + +def start(bundle_name=None): + if bundle_name: + logging.warn("bundle_name deprecated, now comes from activity.info") + + parser = OptionParser(usage='[action] [options]') + parser.disable_interspersed_args() + (options_, args) = parser.parse_args() + + config = Config() + + try: + globals()['cmd_' + args[0]](config, args[1:]) + except (KeyError, IndexError): + print_commands() + +if __name__ == '__main__': + start() diff --git a/src/sugar/activity/main.py b/src/sugar/activity/main.py new file mode 100644 index 0000000..0295bf9 --- /dev/null +++ b/src/sugar/activity/main.py @@ -0,0 +1,140 @@ +# Copyright (C) 2008 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import sys +import gettext +from optparse import OptionParser + +import gtk +import dbus +import dbus.service +import dbus.glib + +from sugar.activity import activityhandle +from sugar.bundle.activitybundle import ActivityBundle +from sugar import logger + +def create_activity_instance(constructor, handle): + activity = constructor(handle) + activity.show() + +def get_single_process_name(bundle_id): + return bundle_id + +def get_single_process_path(bundle_id): + return '/' + bundle_id.replace('.', '/') + +class SingleProcess(dbus.service.Object): + def __init__(self, name_service, constructor): + self.constructor = constructor + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(name_service, bus=bus) + object_path = get_single_process_path(name_service) + dbus.service.Object.__init__(self, bus_name, object_path) + + @dbus.service.method("org.laptop.SingleProcess", in_signature="a{ss}") + def create(self, handle_dict): + handle = activityhandle.create_from_dict(handle_dict) + create_activity_instance(self.constructor, handle) + +def main(): + parser = OptionParser() + parser.add_option("-b", "--bundle-id", dest="bundle_id", + help="identifier of the activity bundle") + parser.add_option("-a", "--activity-id", dest="activity_id", + help="identifier of the activity instance") + parser.add_option("-o", "--object-id", dest="object_id", + help="identifier of the associated datastore object") + parser.add_option("-u", "--uri", dest="uri", + help="URI to load") + parser.add_option('-s', '--single-process', dest='single_process', + action='store_true', + help='start all the instances in the same process') + (options, args) = parser.parse_args() + + logger.start() + + if 'SUGAR_BUNDLE_PATH' not in os.environ: + print 'SUGAR_BUNDLE_PATH is not defined in the environment.' + sys.exit(1) + + if len(args) == 0: + print 'A python class must be specified as first argument.' + sys.exit(1) + + bundle_path = os.environ['SUGAR_BUNDLE_PATH'] + sys.path.append(bundle_path) + + bundle = ActivityBundle(bundle_path) + + os.environ['SUGAR_BUNDLE_ID'] = bundle.get_bundle_id() + os.environ['SUGAR_BUNDLE_NAME'] = bundle.get_name() + os.environ['SUGAR_BUNDLE_VERSION'] = str(bundle.get_activity_version()) + + gtk.icon_theme_get_default().append_search_path(bundle.get_icons_path()) + + locale_path = None + if 'SUGAR_LOCALEDIR' in os.environ: + locale_path = os.environ['SUGAR_LOCALEDIR'] + + gettext.bindtextdomain(bundle.get_bundle_id(), locale_path) + gettext.textdomain(bundle.get_bundle_id()) + + splitted_module = args[0].rsplit('.', 1) + module_name = splitted_module[0] + class_name = splitted_module[1] + + module = __import__(module_name) + for comp in module_name.split('.')[1:]: + module = getattr(module, comp) + + activity_constructor = getattr(module, class_name) + activity_handle = activityhandle.ActivityHandle( + activity_id=options.activity_id, + object_id=options.object_id, uri=options.uri) + + if options.single_process is True: + sessionbus = dbus.SessionBus() + + service_name = get_single_process_name(options.bundle_id) + service_path = get_single_process_path(options.bundle_id) + + bus_object = sessionbus.get_object( + 'org.freedesktop.DBus', '/org/freedesktop/DBus') + try: + name = bus_object.GetNameOwner( + service_name, dbus_interface='org.freedesktop.DBus') + except dbus.DBusException: + name = None + + if not name: + SingleProcess(service_name, activity_constructor) + else: + single_process = sessionbus.get_object(service_name, service_path) + single_process.create(activity_handle.get_dict()) + + print 'Created %s in a single process.' % service_name + sys.exit(0) + + if hasattr(module, 'start'): + module.start() + + create_activity_instance(activity_constructor, activity_handle) + + gtk.main() diff --git a/src/sugar/activity/namingalert.py b/src/sugar/activity/namingalert.py new file mode 100644 index 0000000..724d76a --- /dev/null +++ b/src/sugar/activity/namingalert.py @@ -0,0 +1,320 @@ +# Copyright (C) 2009 One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gettext + +import gtk +import gobject +import hippo +import gconf + +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.entry import CanvasEntry +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.canvastextview import CanvasTextView + +from sugar.bundle.activitybundle import ActivityBundle + +_ = lambda msg: gettext.dgettext('sugar-toolkit', msg) + +class NamingToolbar(gtk.Toolbar): + """ Toolbar of the naming alert + """ + __gtype_name__ = 'SugarNamingToolbar' + + __gsignals__ = { + 'keep-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + def __init__(self): + gtk.Toolbar.__init__(self) + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + icon = Icon() + icon.set_from_icon_name('activity-journal', + gtk.ICON_SIZE_LARGE_TOOLBAR) + icon.props.xo_color = color + self._add_widget(icon) + + self._add_separator() + + self._title = gtk.Label(_('Name this entry')) + self._add_widget(self._title) + + self._add_separator(True) + + self._keep_button = ToolButton('dialog-ok', tooltip=_('Keep')) + self._keep_button.props.accelerator = 'Return' + self._keep_button.connect('clicked', self.__keep_button_clicked_cb) + self.insert(self._keep_button, -1) + self._keep_button.show() + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __keep_button_clicked_cb(self, widget, data=None): + self.emit('keep-clicked') + +class FavoriteIcon(CanvasIcon): + def __init__(self, favorite): + CanvasIcon.__init__(self, icon_name='emblem-favorite', + box_width=style.GRID_CELL_SIZE * 3 / 5, + size=style.SMALL_ICON_SIZE) + self._favorite = None + self.set_favorite(favorite) + self.connect('button-release-event', self.__release_event_cb) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def set_favorite(self, favorite): + if favorite == self._favorite: + return + + self._favorite = favorite + if favorite: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.xo_color = color + else: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_WHITE.get_svg() + + def get_favorite(self): + return self._favorite + + favorite = gobject.property( + type=bool, default=False, getter=get_favorite, setter=set_favorite) + + def __release_event_cb(self, icon, event): + self.props.favorite = not self.props.favorite + + def __motion_notify_event_cb(self, icon, event): + if not self._favorite: + if event.detail == hippo.MOTION_DETAIL_ENTER: + icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + +class NamingAlert(gtk.Window): + __gtype_name__ = 'SugarNamingAlert' + + def __init__(self, activity, bundle_path): + gtk.Window.__init__(self) + + self._bundle_path = bundle_path + self._favorite_icon = None + self._title = None + self._description = None + self._tags = None + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self.set_border_width(style.LINE_WIDTH) + offset = style.GRID_CELL_SIZE + width = gtk.gdk.screen_width() - offset * 2 + height = gtk.gdk.screen_height() - offset * 2 + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + self.connect('realize', self.__realize_cb) + + self._activity = activity + + vbox = gtk.VBox() + self.add(vbox) + vbox.show() + + toolbar = NamingToolbar() + toolbar.connect('keep-clicked', self.__keep_cb) + vbox.pack_start(toolbar, False) + toolbar.show() + + canvas = hippo.Canvas() + self._root = hippo.CanvasBox() + self._root.props.background_color = style.COLOR_WHITE.get_int() + canvas.set_root(self._root) + vbox.pack_start(canvas) + canvas.show() + + body = self._create_body() + self._root.append(body, hippo.PACK_EXPAND) + + widget = self._title.get_property('widget') + widget.grab_focus() + + def _create_body(self): + body = hippo.CanvasBox() + body.props.orientation = hippo.ORIENTATION_VERTICAL + body.props.background_color = style.COLOR_WHITE.get_int() + body.props.padding_top = style.DEFAULT_SPACING * 3 + + header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + padding=style.DEFAULT_PADDING, + padding_right=style.GRID_CELL_SIZE, + spacing=style.DEFAULT_SPACING) + body.append(header) + + descriptions = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + spacing=style.DEFAULT_SPACING * 3, + padding_left=style.GRID_CELL_SIZE, + padding_right=style.GRID_CELL_SIZE, + padding_top=style.DEFAULT_SPACING * 3) + + body.append(descriptions, hippo.PACK_EXPAND) + + first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + descriptions.append(first_column) + + second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + descriptions.append(second_column, hippo.PACK_EXPAND) + + self._favorite_icon = self._create_favorite_icon() + header.append(self._favorite_icon) + + activity_icon = self._create_activity_icon() + header.append(activity_icon) + + self._title = self._create_title() + header.append(self._title, hippo.PACK_EXPAND) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + header.reverse() + + description_box, self._description = self._create_description() + second_column.append(description_box) + + tags_box, self._tags = self._create_tags() + second_column.append(tags_box) + + return body + + def _create_favorite_icon(self): + favorite_icon = FavoriteIcon(False) + return favorite_icon + + def _create_activity_icon(self): + activity_bundle = ActivityBundle(self._bundle_path) + activity_icon = CanvasIcon(file_name=activity_bundle.get_icon()) + if self._activity.metadata.has_key('icon-color') and \ + self._activity.metadata['icon-color']: + activity_icon.props.xo_color = XoColor( \ + self._activity.metadata['icon-color']) + return activity_icon + + def _create_title(self): + title = CanvasEntry() + title.set_background(style.COLOR_WHITE.get_html()) + title.props.text = self._activity.metadata.get('title', _('Untitled')) + return title + + def _create_description(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Description:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + description = self._activity.metadata.get('description', '') + text_view = CanvasTextView(description, + box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + + return vbox, text_view + + def _create_tags(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Tags:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + tags = self._activity.metadata.get('tags', '') + text_view = CanvasTextView(tags, box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + + return vbox, text_view + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def __keep_cb(self, widget): + is_favorite = self._favorite_icon.get_favorite() + if is_favorite: + self._activity.metadata['keep'] = 1 + else: + self._activity.metadata['keep'] = 0 + + self._activity.metadata['title'] = self._title.props.text + + new_tags = self._tags.text_view_widget.props.buffer.props.text + self._activity.metadata['tags'] = new_tags + + new_description = \ + self._description.text_view_widget.props.buffer.props.text + self._activity.metadata['description'] = new_description + + self._activity.metadata['title_set_by_user'] = '1' + self._activity.close() + self.destroy() -- cgit v0.9.1