Remove old cruft
[sugar-toolbox.git] / src / sugar / activity / activity.py
blobacefcf445dcf4b4d57265a12274aed6f8eb35f33
1 """Base class for activities written in Python
3 This is currently the only definitive reference for what an
4 activity must do to participate in the Sugar desktop.
6 A Basic Activity
8 All activities must implement a class derived from 'Activity' in this class.
9 The convention is to call it ActivitynameActivity, but this is not required as
10 the activity.info file associated with your activity will tell the sugar-shell
11 which class to start.
13 For example the most minimal Activity:
16 from sugar.activity import activity
18 class ReadActivity(activity.Activity):
19 pass
21 To get a real, working activity, you will at least have to implement:
22 __init__(), read_file() and write_file()
24 Aditionally, you will probably need a at least a Toolbar so you can have some
25 interesting buttons for the user, like for example 'exit activity'
27 See the methods of the Activity class below for more information on what you
28 will need for a real activity.
30 STABLE.
31 """
32 # Copyright (C) 2006-2007 Red Hat, Inc.
33 # Copyright (C) 2007-2008 One Laptop Per Child
35 # This library is free software; you can redistribute it and/or
36 # modify it under the terms of the GNU Lesser General Public
37 # License as published by the Free Software Foundation; either
38 # version 2 of the License, or (at your option) any later version.
40 # This library is distributed in the hope that it will be useful,
41 # but WITHOUT ANY WARRANTY; without even the implied warranty of
42 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
43 # Lesser General Public License for more details.
45 # You should have received a copy of the GNU Lesser General Public
46 # License along with this library; if not, write to the
47 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
48 # Boston, MA 02111-1307, USA.
50 import gettext
51 import logging
52 import os
53 import time
54 from hashlib import sha1
55 import traceback
56 import gconf
58 import gtk, gobject
59 import dbus
60 import dbus.service
61 import cjson
63 from sugar import util
64 from sugar.presence import presenceservice
65 from sugar.activity.activityservice import ActivityService
66 from sugar.graphics import style
67 from sugar.graphics.window import Window
68 from sugar.graphics.toolbox import Toolbox
69 from sugar.graphics.toolbutton import ToolButton
70 from sugar.graphics.toolcombobox import ToolComboBox
71 from sugar.graphics.alert import Alert
72 from sugar.graphics.icon import Icon
73 from sugar.graphics.xocolor import XoColor
74 from sugar.datastore import datastore
75 from sugar.session import XSMPClient
76 from sugar import wm
77 from sugar import _sugarext
79 _ = lambda msg: gettext.dgettext('sugar-toolkit', msg)
81 SCOPE_PRIVATE = "private"
82 SCOPE_INVITE_ONLY = "invite" # shouldn't be shown in UI, it's implicit
83 SCOPE_NEIGHBORHOOD = "public"
85 J_DBUS_SERVICE = 'org.laptop.Journal'
86 J_DBUS_PATH = '/org/laptop/Journal'
87 J_DBUS_INTERFACE = 'org.laptop.Journal'
89 class ActivityToolbar(gtk.Toolbar):
90 """The Activity toolbar with the Journal entry title, sharing,
91 Keep and Stop buttons
93 All activities should have this toolbar. It is easiest to add it to your
94 Activity by using the ActivityToolbox.
95 """
96 def __init__(self, activity):
97 gtk.Toolbar.__init__(self)
99 self._activity = activity
100 self._updating_share = False
102 activity.connect('shared', self.__activity_shared_cb)
103 activity.connect('joined', self.__activity_shared_cb)
104 activity.connect('notify::max_participants',
105 self.__max_participants_changed_cb)
107 if activity.metadata:
108 self.title = gtk.Entry()
109 self.title.set_size_request(int(gtk.gdk.screen_width() / 3), -1)
110 self.title.set_text(activity.metadata['title'])
111 self.title.connect('changed', self.__title_changed_cb)
112 self._add_widget(self.title)
114 activity.metadata.connect('updated', self.__jobject_updated_cb)
116 separator = gtk.SeparatorToolItem()
117 separator.props.draw = False
118 separator.set_expand(True)
119 self.insert(separator, -1)
120 separator.show()
122 self.share = ToolComboBox(label_text=_('Share with:'))
123 self.share.combo.connect('changed', self.__share_changed_cb)
124 self.share.combo.append_item(SCOPE_PRIVATE, _('Private'), 'zoom-home')
125 self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'),
126 'zoom-neighborhood')
127 self.insert(self.share, -1)
128 self.share.show()
130 self._update_share()
132 self.keep = ToolButton(tooltip=_('Keep'))
133 client = gconf.client_get_default()
134 color = XoColor(client.get_string('/desktop/sugar/user/color'))
135 keep_icon = Icon(icon_name='document-save', xo_color=color)
136 self.keep.set_icon_widget(keep_icon)
137 keep_icon.show()
138 self.keep.props.accelerator = '<Ctrl>S'
139 self.keep.connect('clicked', self.__keep_clicked_cb)
140 self.insert(self.keep, -1)
141 self.keep.show()
143 self.stop = ToolButton('activity-stop', tooltip=_('Stop'))
144 self.stop.props.accelerator = '<Ctrl>Q'
145 self.stop.connect('clicked', self.__stop_clicked_cb)
146 self.insert(self.stop, -1)
147 self.stop.show()
149 self._update_title_sid = None
151 def _update_share(self):
152 self._updating_share = True
154 if self._activity.props.max_participants == 1:
155 self.share.hide()
157 if self._activity.get_shared():
158 self.share.set_sensitive(False)
159 self.share.combo.set_active(1)
160 else:
161 self.share.set_sensitive(True)
162 self.share.combo.set_active(0)
164 self._updating_share = False
166 def __share_changed_cb(self, combo):
167 if self._updating_share:
168 return
170 model = self.share.combo.get_model()
171 it = self.share.combo.get_active_iter()
172 (scope, ) = model.get(it, 0)
173 if scope == SCOPE_NEIGHBORHOOD:
174 self._activity.share()
176 def __keep_clicked_cb(self, button):
177 self._activity.copy()
179 def __stop_clicked_cb(self, button):
180 self._activity.close()
182 def __jobject_updated_cb(self, jobject):
183 self.title.set_text(jobject['title'])
185 def __title_changed_cb(self, entry):
186 if not self._update_title_sid:
187 self._update_title_sid = gobject.timeout_add(
188 1000, self.__update_title_cb)
190 def __update_title_cb(self):
191 title = self.title.get_text()
193 self._activity.metadata['title'] = title
194 self._activity.metadata['title_set_by_user'] = '1'
195 self._activity.save()
197 shared_activity = self._activity.get_shared_activity()
198 if shared_activity:
199 shared_activity.props.name = title
201 self._update_title_sid = None
202 return False
204 def _add_widget(self, widget, expand=False):
205 tool_item = gtk.ToolItem()
206 tool_item.set_expand(expand)
208 tool_item.add(widget)
209 widget.show()
211 self.insert(tool_item, -1)
212 tool_item.show()
214 def __activity_shared_cb(self, activity):
215 self._update_share()
217 def __max_participants_changed_cb(self, activity, pspec):
218 self._update_share()
220 class EditToolbar(gtk.Toolbar):
221 """Provides the standard edit toolbar for Activities.
223 Members:
224 undo -- the undo button
225 redo -- the redo button
226 copy -- the copy button
227 paste -- the paste button
228 separator -- A separator between undo/redo and copy/paste
230 This class only provides the 'edit' buttons in a standard layout,
231 your activity will need to either hide buttons which make no sense for your
232 Activity, or you need to connect the button events to your own callbacks:
234 ## Example from Read.activity:
235 # Create the edit toolbar:
236 self._edit_toolbar = EditToolbar(self._view)
237 # Hide undo and redo, they're not needed
238 self._edit_toolbar.undo.props.visible = False
239 self._edit_toolbar.redo.props.visible = False
240 # Hide the separator too:
241 self._edit_toolbar.separator.props.visible = False
243 # As long as nothing is selected, copy needs to be insensitive:
244 self._edit_toolbar.copy.set_sensitive(False)
245 # When the user clicks the button, call _edit_toolbar_copy_cb()
246 self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb)
248 # Add the edit toolbar:
249 toolbox.add_toolbar(_('Edit'), self._edit_toolbar)
250 # And make it visible:
251 self._edit_toolbar.show()
253 def __init__(self):
254 gtk.Toolbar.__init__(self)
256 self.undo = ToolButton('edit-undo')
257 self.undo.set_tooltip(_('Undo'))
258 self.insert(self.undo, -1)
259 self.undo.show()
261 self.redo = ToolButton('edit-redo')
262 self.redo.set_tooltip(_('Redo'))
263 self.insert(self.redo, -1)
264 self.redo.show()
266 self.separator = gtk.SeparatorToolItem()
267 self.separator.set_draw(True)
268 self.insert(self.separator, -1)
269 self.separator.show()
271 self.copy = ToolButton('edit-copy')
272 self.copy.set_tooltip(_('Copy'))
273 self.insert(self.copy, -1)
274 self.copy.show()
276 self.paste = ToolButton('edit-paste')
277 self.paste.set_tooltip(_('Paste'))
278 self.insert(self.paste, -1)
279 self.paste.show()
281 class ActivityToolbox(Toolbox):
282 """Creates the Toolbox for the Activity
284 By default, the toolbox contains only the ActivityToolbar. After creating
285 the toolbox, you can add your activity specific toolbars, for example the
286 EditToolbar.
288 To add the ActivityToolbox to your Activity in MyActivity.__init__() do:
290 # Create the Toolbar with the ActivityToolbar:
291 toolbox = activity.ActivityToolbox(self)
292 ... your code, inserting all other toolbars you need, like EditToolbar
294 # Add the toolbox to the activity frame:
295 self.set_toolbox(toolbox)
296 # And make it visible:
297 toolbox.show()
299 def __init__(self, activity):
300 Toolbox.__init__(self)
302 self._activity_toolbar = ActivityToolbar(activity)
303 self.add_toolbar(_('Activity'), self._activity_toolbar)
304 self._activity_toolbar.show()
306 def get_activity_toolbar(self):
307 return self._activity_toolbar
309 class _ActivitySession(gobject.GObject):
310 __gsignals__ = {
311 'quit-requested': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
312 'quit': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([]))
315 def __init__(self):
316 gobject.GObject.__init__(self)
318 self._xsmp_client = XSMPClient()
319 self._xsmp_client.connect('quit-requested', self.__sm_quit_requested_cb)
320 self._xsmp_client.connect('quit', self.__sm_quit_cb)
321 self._xsmp_client.startup()
323 self._activities = []
324 self._will_quit = []
326 def register(self, activity):
327 self._activities.append(activity)
329 def unregister(self, activity):
330 self._activities.remove(activity)
332 if len(self._activities) == 0:
333 logging.debug('Quitting the activity process.')
334 gtk.main_quit()
336 def will_quit(self, activity, will_quit):
337 if will_quit:
338 self._will_quit.append(activity)
340 # We can quit only when all the instances agreed to
341 for activity in self._activities:
342 if activity not in self._will_quit:
343 return
345 self._xsmp_client.will_quit(True)
346 else:
347 self._will_quit = []
348 self._xsmp_client.will_quit(False)
350 def __sm_quit_requested_cb(self, client):
351 self.emit('quit-requested')
353 def __sm_quit_cb(self, client):
354 self.emit('quit')
356 class Activity(Window, gtk.Container):
357 """This is the base Activity class that all other Activities derive from.
358 This is where your activity starts.
360 To get a working Activity:
361 0. Derive your Activity from this class:
362 class MyActivity(activity.Activity):
365 1. implement an __init__() method for your Activity class.
367 Use your init method to create your own ActivityToolbar which will
368 contain some standard buttons:
369 toolbox = activity.ActivityToolbox(self)
371 Add extra Toolbars to your toolbox.
373 You should setup Activity sharing here too.
375 Finaly, your Activity may need some resources which you can claim
376 here too.
378 The __init__() method is also used to make the distinction between
379 being resumed from the Journal, or starting with a blank document.
381 2. Implement read_file() and write_file()
382 Most activities revolve around creating and storing Journal entries.
383 For example, Write: You create a document, it is saved to the Journal
384 and then later you resume working on the document.
386 read_file() and write_file() will be called by sugar to tell your
387 Activity that it should load or save the document the user is working
390 3. Implement our Activity Toolbars.
391 The Toolbars are added to your Activity in step 1 (the toolbox), but
392 you need to implement them somewhere. Now is a good time.
394 There are a number of standard Toolbars. The most basic one, the one
395 your almost absolutely MUST have is the ActivityToolbar. Without
396 this, you're not really making a proper Sugar Activity (which may be
397 okay, but you should really stop and think about why not!) You do
398 this with the ActivityToolbox(self) call in step 1.
400 Usually, you will also need the standard EditToolbar. This is the one
401 which has the standard copy and paste buttons. You need to derive
402 your own EditToolbar class from sugar.EditToolbar:
403 class EditToolbar(activity.EditToolbar):
406 See EditToolbar for the methods you should implement in your class.
408 Finaly, your Activity will very likely need some activity specific
409 buttons and options you can create your own toolbars by deriving a
410 class from gtk.Toolbar:
411 class MySpecialToolbar(gtk.Toolbar):
414 4. Use your creativity. Make your Activity something special and share
415 it with your friends!
417 Read through the methods of the Activity class below, to learn more about
418 how to make an Activity work.
420 Hint: A good and simple Activity to learn from is the Read activity. To
421 create your own activity, you may want to copy it and use it as a template.
423 __gtype_name__ = 'SugarActivity'
425 __gsignals__ = {
426 'shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
427 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([]))
430 def __init__(self, handle, create_jobject=True):
431 """Initialise the Activity
433 handle -- sugar.activity.activityhandle.ActivityHandle
434 instance providing the activity id and access to the
435 presence service which *may* provide sharing for this
436 application
438 create_jobject -- boolean
439 define if it should create a journal object if we are
440 not resuming
442 Side effects:
444 Sets the gdk screen DPI setting (resolution) to the
445 Sugar screen resolution.
447 Connects our "destroy" message to our _destroy_cb
448 method.
450 Creates a base gtk.Window within this window.
452 Creates an ActivityService (self._bus) servicing
453 this application.
455 Usage:
456 If your Activity implements __init__(), it should call
457 the base class __init()__ before doing Activity specific things.
460 Window.__init__(self)
462 # process titles will only show 15 characters
463 # but they get truncated anyway so if more characters
464 # are supported in the future we will get a better view
465 # of the processes
466 proc_title = "%s <%s>" % (get_bundle_name(), handle.activity_id)
467 util.set_proc_title(proc_title)
469 self.connect('realize', self.__realize_cb)
470 self.connect('delete-event', self.__delete_event_cb)
472 # watch visibility-notify-events to know when we can safely
473 # take a screenshot of the activity
474 self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
475 self.connect('visibility-notify-event',
476 self.__visibility_notify_event_cb)
477 self._fully_obscured = True
479 self._active = False
480 self._activity_id = handle.activity_id
481 self._pservice = presenceservice.get_instance()
482 self.shared_activity = None
483 self._share_id = None
484 self._join_id = None
485 self._preview = _sugarext.Preview()
486 self._updating_jobject = False
487 self._closing = False
488 self._quit_requested = False
489 self._deleting = False
490 self._max_participants = 0
491 self._invites_queue = []
492 self._jobject = None
494 self._session = _get_session()
495 self._session.register(self)
496 self._session.connect('quit-requested',
497 self.__session_quit_requested_cb)
498 self._session.connect('quit', self.__session_quit_cb)
500 accel_group = gtk.AccelGroup()
501 self.set_data('sugar-accel-group', accel_group)
502 self.add_accel_group(accel_group)
504 self._bus = ActivityService(self)
505 self._owns_file = False
507 share_scope = SCOPE_PRIVATE
509 if handle.object_id:
510 self._jobject = datastore.get(handle.object_id)
511 self.set_title(self._jobject.metadata['title'])
513 if self._jobject.metadata.has_key('share-scope'):
514 share_scope = self._jobject.metadata['share-scope']
516 # handle activity share/join
517 mesh_instance = self._pservice.get_activity(self._activity_id,
518 warn_if_none=False)
519 logging.debug("*** Act %s, mesh instance %r, scope %s",
520 self._activity_id, mesh_instance, share_scope)
521 if mesh_instance is not None:
522 # There's already an instance on the mesh, join it
523 logging.debug("*** Act %s joining existing mesh instance %r",
524 self._activity_id, mesh_instance)
525 self.shared_activity = mesh_instance
526 self.shared_activity.connect('notify::private',
527 self.__privacy_changed_cb)
528 self._join_id = self.shared_activity.connect("joined",
529 self.__joined_cb)
530 if not self.shared_activity.props.joined:
531 self.shared_activity.join()
532 else:
533 self.__joined_cb(self.shared_activity, True, None)
534 elif share_scope != SCOPE_PRIVATE:
535 logging.debug("*** Act %s no existing mesh instance, but used to " \
536 "be shared, will share" % self._activity_id)
537 # no existing mesh instance, but activity used to be shared, so
538 # restart the share
539 if share_scope == SCOPE_INVITE_ONLY:
540 self.share(private=True)
541 elif share_scope == SCOPE_NEIGHBORHOOD:
542 self.share(private=False)
543 else:
544 logging.debug("Unknown share scope %r" % share_scope)
546 if handle.object_id is None and create_jobject:
547 logging.debug('Creating a jobject.')
548 self._jobject = datastore.create()
549 title = _('%s Activity') % get_bundle_name()
550 self._jobject.metadata['title'] = title
551 self.set_title(self._jobject.metadata['title'])
552 self._jobject.metadata['title_set_by_user'] = '0'
553 self._jobject.metadata['activity'] = self.get_bundle_id()
554 self._jobject.metadata['activity_id'] = self.get_id()
555 self._jobject.metadata['keep'] = '0'
556 self._jobject.metadata['preview'] = ''
557 self._jobject.metadata['share-scope'] = SCOPE_PRIVATE
558 if self.shared_activity is not None:
559 icon_color = self.shared_activity.props.color
560 else:
561 client = gconf.client_get_default()
562 icon_color = client.get_string('/desktop/sugar/user/color')
563 self._jobject.metadata['icon-color'] = icon_color
565 self._jobject.file_path = ''
566 # Cannot call datastore.write async for creates:
567 # https://dev.laptop.org/ticket/3071
568 datastore.write(self._jobject)
570 def get_active(self):
571 return self._active
573 def set_active(self, active):
574 if self._active != active:
575 self._active = active
576 if not self._active and self._jobject:
577 self.save()
579 active = gobject.property(
580 type=bool, default=False, getter=get_active, setter=set_active)
582 def get_max_participants(self):
583 return self._max_participants
585 def set_max_participants(self, participants):
586 self._max_participants = participants
588 max_participants = gobject.property(
589 type=int, default=0, getter=get_max_participants,
590 setter=set_max_participants)
592 def get_id(self):
593 """Returns the activity id of the current instance of your activity.
595 The activity id is sort-of-like the unix process id (PID). However,
596 unlike PIDs it is only different for each new instance (with
597 create_jobject = True set) and stays the same everytime a user
598 resumes an activity. This is also the identity of your Activity to other
599 XOs for use when sharing.
601 return self._activity_id
603 def get_bundle_id(self):
604 """Returns the bundle_id from the activity.info file"""
605 return os.environ['SUGAR_BUNDLE_ID']
607 def set_canvas(self, canvas):
608 """Sets the 'work area' of your activity with the canvas of your choice.
610 One commonly used canvas is gtk.ScrolledWindow
612 Window.set_canvas(self, canvas)
613 canvas.connect('map', self.__canvas_map_cb)
615 def __session_quit_requested_cb(self, session):
616 self._quit_requested = True
618 if not self._prepare_close():
619 session.will_quit(self, False)
620 elif not self._updating_jobject:
621 session.will_quit(self, True)
623 def __session_quit_cb(self, client):
624 self._complete_close()
626 def __canvas_map_cb(self, canvas):
627 if self._jobject and self._jobject.file_path:
628 self.read_file(self._jobject.file_path)
630 def __jobject_create_cb(self):
631 pass
633 def __jobject_error_cb(self, err):
634 logging.debug("Error creating activity datastore object: %s" % err)
636 def get_activity_root(self):
637 """ FIXME: Deprecated. This part of the API has been moved
638 out of this class to the module itself
640 Returns a path for saving Activity specific preferences, etc.
642 Returns a path to the location in the filesystem where the activity can
643 store activity related data that doesn't pertain to the current
644 execution of the activity and thus cannot go into the DataStore.
646 Currently, this will return something like
647 ~/.sugar/default/MyActivityName/
649 Activities should ONLY save settings, user preferences and other data
650 which isn't specific to a journal item here. If (meta-)data is in anyway
651 specific to a journal entry, it MUST be stored in the DataStore.
653 if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \
654 os.environ['SUGAR_ACTIVITY_ROOT']:
655 return os.environ['SUGAR_ACTIVITY_ROOT']
656 else:
657 return '/'
659 def read_file(self, file_path):
661 Subclasses implement this method if they support resuming objects from
662 the journal. 'file_path' is the file to read from.
664 You should immediately open the file from the file_path, because the
665 file_name will be deleted immediately after returning from read_file().
666 Once the file has been opened, you do not have to read it immediately:
667 After you have opened it, the file will only be really gone when you
668 close it.
670 Although not required, this is also a good time to read all meta-data:
671 the file itself cannot be changed externally, but the title, description
672 and other metadata['tags'] may change. So if it is important for you to
673 notice changes, this is the time to record the originals.
675 raise NotImplementedError
677 def write_file(self, file_path):
679 Subclasses implement this method if they support saving data to objects
680 in the journal. 'file_path' is the file to write to.
682 If the user did make changes, you should create the file_path and save
683 all document data to it.
685 Additionally, you should also write any metadata needed to resume your
686 activity. For example, the Read activity saves the current page and zoom
687 level, so it can display the page.
689 Note: Currently, the file_path *WILL* be different from the one you
690 received in file_read(). Even if you kept the file_path from file_read()
691 open until now, you must still write the entire file to this file_path.
693 raise NotImplementedError
695 def __save_cb(self):
696 logging.debug('Activity.__save_cb')
697 self._updating_jobject = False
698 if self._quit_requested:
699 self._session.will_quit(self, True)
700 elif self._closing:
701 self._complete_close()
703 def __save_error_cb(self, err):
704 logging.debug('Activity.__save_error_cb')
705 self._updating_jobject = False
706 if self._quit_requested:
707 self._session.will_quit(self, False)
708 if self._closing:
709 self._show_keep_failed_dialog()
710 self._closing = False
711 logging.debug("Error saving activity object to datastore: %s" % err)
713 def _cleanup_jobject(self):
714 if self._jobject:
715 if self._owns_file and os.path.isfile(self._jobject.file_path):
716 logging.debug('_cleanup_jobject: removing %r' %
717 self._jobject.file_path)
718 os.remove(self._jobject.file_path)
719 self._owns_file = False
720 self._jobject.destroy()
721 self._jobject = None
723 def _get_preview(self):
724 pixbuf = self._preview.get_pixbuf()
725 if pixbuf is None:
726 return None
728 pixbuf = pixbuf.scale_simple(style.zoom(300), style.zoom(225),
729 gtk.gdk.INTERP_BILINEAR)
731 preview_data = []
732 def save_func(buf, data):
733 data.append(buf)
735 pixbuf.save_to_callback(save_func, 'png', user_data=preview_data)
736 preview_data = ''.join(preview_data)
738 self._preview.clear()
740 return preview_data
742 def _get_buddies(self):
743 if self.shared_activity is not None:
744 buddies = {}
745 for buddy in self.shared_activity.get_joined_buddies():
746 if not buddy.props.owner:
747 buddy_id = sha1(buddy.props.key).hexdigest()
748 buddies[buddy_id] = [buddy.props.nick, buddy.props.color]
749 return buddies
750 else:
751 return {}
753 def take_screenshot(self):
754 if self.canvas:
755 self._preview.take_screenshot(self.canvas)
757 def save(self):
758 """Request that the activity is saved to the Journal.
760 This method is called by the close() method below. In general,
761 activities should not override this method. This method is part of the
762 public API of an Acivity, and should behave in standard ways. Use your
763 own implementation of write_file() to save your Activity specific data.
766 if self._jobject is None:
767 logging.debug('Cannot save, no journal object.')
768 return
770 logging.debug('Activity.save: %r' % self._jobject.object_id)
772 if self._updating_jobject:
773 logging.info('Activity.save: still processing a previous request.')
774 return
776 buddies_dict = self._get_buddies()
777 if buddies_dict:
778 self.metadata['buddies_id'] = cjson.encode(buddies_dict.keys())
779 self.metadata['buddies'] = cjson.encode(self._get_buddies())
781 preview = self._get_preview()
782 if self._preview:
783 self.metadata['preview'] = dbus.ByteArray(preview)
785 try:
786 file_path = os.path.join(self.get_activity_root(), 'instance',
787 '%i' % time.time())
788 self.write_file(file_path)
789 self._owns_file = True
790 self._jobject.file_path = file_path
791 except NotImplementedError:
792 logging.debug('Activity.write_file is not implemented.')
794 # Cannot call datastore.write async for creates:
795 # https://dev.laptop.org/ticket/3071
796 if self._jobject.object_id is None:
797 datastore.write(self._jobject, transfer_ownership=True)
798 else:
799 self._updating_jobject = True
800 datastore.write(self._jobject,
801 transfer_ownership=True,
802 reply_handler=self.__save_cb,
803 error_handler=self.__save_error_cb)
805 def copy(self):
806 """Request that the activity 'Keep in Journal' the current state
807 of the activity.
809 Activities should not override this method. Instead, like save() do any
810 copy work that needs to be done in write_file()
812 logging.debug('Activity.copy: %r' % self._jobject.object_id)
813 if not self._fully_obscured:
814 self.take_screenshot()
815 self.save()
816 self._jobject.object_id = None
818 def __privacy_changed_cb(self, shared_activity, param_spec):
819 if shared_activity.props.private:
820 self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY
821 else:
822 self._jobject.metadata['share-scope'] = SCOPE_NEIGHBORHOOD
824 def __joined_cb(self, activity, success, err):
825 """Callback when join has finished"""
826 self.shared_activity.disconnect(self._join_id)
827 self._join_id = None
828 if not success:
829 logging.debug("Failed to join activity: %s" % err)
830 return
832 self.present()
833 self.emit('joined')
834 self.__privacy_changed_cb(self.shared_activity, None)
836 def get_shared_activity(self):
837 """Returns an instance of the shared Activity or None
839 The shared activity is of type sugar.presence.activity.Activity
841 return self._shared_activity
843 def get_shared(self):
844 """Returns TRUE if the activity is shared on the mesh."""
845 if not self.shared_activity:
846 return False
847 return self.shared_activity.props.joined
849 def __share_cb(self, ps, success, activity, err):
850 self._pservice.disconnect(self._share_id)
851 self._share_id = None
852 if not success:
853 logging.debug('Share of activity %s failed: %s.' %
854 (self._activity_id, err))
855 return
857 logging.debug('Share of activity %s successful, PS activity is %r.',
858 self._activity_id, activity)
860 activity.props.name = self._jobject.metadata['title']
862 self.shared_activity = activity
863 self.shared_activity.connect('notify::private',
864 self.__privacy_changed_cb)
865 self.emit('shared')
866 self.__privacy_changed_cb(self.shared_activity, None)
868 self._send_invites()
870 def _invite_response_cb(self, error):
871 if error:
872 logging.error('Invite failed: %s' % error)
874 def _send_invites(self):
875 while self._invites_queue:
876 buddy_key = self._invites_queue.pop()
877 buddy = self._pservice.get_buddy(buddy_key)
878 if buddy:
879 self.shared_activity.invite(
880 buddy, '', self._invite_response_cb)
881 else:
882 logging.error('Cannot invite %s, no such buddy.' % buddy_key)
884 def invite(self, buddy_key):
885 """Invite a buddy to join this Activity.
887 Side Effects:
888 Calls self.share(True) to privately share the activity if it wasn't
889 shared before.
891 self._invites_queue.append(buddy_key)
893 if (self.shared_activity is None
894 or not self.shared_activity.props.joined):
895 self.share(True)
896 else:
897 self._send_invites()
899 def share(self, private=False):
900 """Request that the activity be shared on the network.
902 private -- bool: True to share by invitation only,
903 False to advertise as shared to everyone.
905 Once the activity is shared, its privacy can be changed by setting
906 its 'private' property.
908 if self.shared_activity and self.shared_activity.props.joined:
909 raise RuntimeError("Activity %s already shared." %
910 self._activity_id)
911 verb = private and 'private' or 'public'
912 logging.debug('Requesting %s share of activity %s.' %
913 (verb, self._activity_id))
914 self._share_id = self._pservice.connect("activity-shared",
915 self.__share_cb)
916 self._pservice.share_activity(self, private=private)
918 def _show_keep_failed_dialog(self):
919 alert = Alert()
920 alert.props.title = _('Keep error')
921 alert.props.msg = _('Keep error: all changes will be lost')
923 cancel_icon = Icon(icon_name='dialog-cancel')
924 alert.add_button(gtk.RESPONSE_CANCEL, _('Don\'t stop'), cancel_icon)
926 stop_icon = Icon(icon_name='dialog-ok')
927 alert.add_button(gtk.RESPONSE_OK, _('Stop anyway'), stop_icon)
929 self.add_alert(alert)
930 alert.connect('response', self._keep_failed_dialog_response_cb)
932 self.present()
934 def _keep_failed_dialog_response_cb(self, alert, response_id):
935 self.remove_alert(alert)
936 if response_id == gtk.RESPONSE_OK:
937 self.close(skip_save=True)
939 def can_close(self):
940 """Activities should override this function if they want to perform
941 extra checks before actually closing."""
943 return True
945 def _prepare_close(self, skip_save=False):
946 if not skip_save:
947 try:
948 self.save()
949 except Exception:
950 logging.info(traceback.format_exc())
951 self._show_keep_failed_dialog()
952 return False
954 if self.shared_activity:
955 self.shared_activity.leave()
957 self._closing = True
959 return True
961 def _complete_close(self):
962 self._cleanup_jobject()
963 self.destroy()
965 # Make the exported object inaccessible
966 dbus.service.Object.remove_from_connection(self._bus)
968 self._session.unregister(self)
970 def close(self, skip_save=False):
971 """Request that the activity be stopped and saved to the Journal
973 Activities should not override this method, but should implement
974 write_file() to do any state saving instead. If the application wants
975 to control wether it can close, it should override can_close().
977 if not self._fully_obscured:
978 self.take_screenshot()
980 if not self.can_close():
981 return
983 if not self._closing:
984 if not self._prepare_close(skip_save):
985 return
987 if not self._updating_jobject:
988 self._complete_close()
990 def __realize_cb(self, window):
991 wm.set_bundle_id(window.window, self.get_bundle_id())
992 wm.set_activity_id(window.window, str(self._activity_id))
994 def __delete_event_cb(self, widget, event):
995 self.close()
996 return True
998 def __visibility_notify_event_cb(self, widget, event):
999 """Visibility state is used when deciding if we can take screenshots.
1000 Currently we allow screenshots whenever the activity window is fully
1001 visible or partially obscured."""
1002 if event.state is gtk.gdk.VISIBILITY_FULLY_OBSCURED:
1003 self._fully_obscured = True
1004 else:
1005 self._fully_obscured = False
1007 def get_metadata(self):
1008 """Returns the jobject metadata or None if there is no jobject.
1010 Activities can set metadata in write_file() using:
1011 self.metadata['MyKey'] = "Something"
1013 and retrieve metadata in read_file() using:
1014 self.metadata.get('MyKey', 'aDefaultValue')
1016 Note: Make sure your activity works properly if one or more of the
1017 metadata items is missing. Never assume they will all be present.
1019 if self._jobject:
1020 return self._jobject.metadata
1021 else:
1022 return None
1024 metadata = property(get_metadata, None)
1026 def handle_view_source(self):
1027 raise NotImplementedError
1029 def get_document_path(self, async_cb, async_err_cb):
1030 async_err_cb(NotImplementedError())
1032 # DEPRECATED
1033 _shared_activity = property(lambda self: self.shared_activity, None)
1035 _session = None
1037 def _get_session():
1038 global _session
1040 if _session is None:
1041 _session = _ActivitySession()
1043 return _session
1045 def get_bundle_name():
1046 """Return the bundle name for the current process' bundle"""
1047 return os.environ['SUGAR_BUNDLE_NAME']
1049 def get_bundle_path():
1050 """Return the bundle path for the current process' bundle"""
1051 return os.environ['SUGAR_BUNDLE_PATH']
1053 def get_activity_root():
1054 """Returns a path for saving Activity specific preferences, etc."""
1055 if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \
1056 os.environ['SUGAR_ACTIVITY_ROOT']:
1057 return os.environ['SUGAR_ACTIVITY_ROOT']
1058 else:
1059 raise RuntimeError("No SUGAR_ACTIVITY_ROOT set.")
1061 def show_object_in_journal(object_id):
1062 bus = dbus.SessionBus()
1063 obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH)
1064 journal = dbus.Interface(obj, J_DBUS_INTERFACE)
1065 journal.ShowObject(object_id)