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.
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
13 For example the most minimal Activity:
16 from sugar.activity import activity
18 class ReadActivity(activity.Activity):
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.
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.
54 from hashlib
import sha1
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
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,
93 All activities should have this toolbar. It is easiest to add it to your
94 Activity by using the ActivityToolbox.
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)
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'),
127 self
.insert(self
.share
, -1)
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
)
138 self
.keep
.props
.accelerator
= '<Ctrl>S'
139 self
.keep
.connect('clicked', self
.__keep
_clicked
_cb
)
140 self
.insert(self
.keep
, -1)
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)
149 self
._update
_title
_sid
= None
151 def _update_share(self
):
152 self
._updating
_share
= True
154 if self
._activity
.props
.max_participants
== 1:
157 if self
._activity
.get_shared():
158 self
.share
.set_sensitive(False)
159 self
.share
.combo
.set_active(1)
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
:
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()
199 shared_activity
.props
.name
= title
201 self
._update
_title
_sid
= None
204 def _add_widget(self
, widget
, expand
=False):
205 tool_item
= gtk
.ToolItem()
206 tool_item
.set_expand(expand
)
208 tool_item
.add(widget
)
211 self
.insert(tool_item
, -1)
214 def __activity_shared_cb(self
, activity
):
217 def __max_participants_changed_cb(self
, activity
, pspec
):
220 class EditToolbar(gtk
.Toolbar
):
221 """Provides the standard edit toolbar for Activities.
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()
254 gtk
.Toolbar
.__init
__(self
)
256 self
.undo
= ToolButton('edit-undo')
257 self
.undo
.set_tooltip(_('Undo'))
258 self
.insert(self
.undo
, -1)
261 self
.redo
= ToolButton('edit-redo')
262 self
.redo
.set_tooltip(_('Redo'))
263 self
.insert(self
.redo
, -1)
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)
276 self
.paste
= ToolButton('edit-paste')
277 self
.paste
.set_tooltip(_('Paste'))
278 self
.insert(self
.paste
, -1)
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
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:
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
):
311 'quit-requested': (gobject
.SIGNAL_RUN_FIRST
, gobject
.TYPE_NONE
, ([])),
312 'quit': (gobject
.SIGNAL_RUN_FIRST
, gobject
.TYPE_NONE
, ([]))
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
= []
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.')
336 def will_quit(self
, activity
, 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
:
345 self
._xsmp
_client
.will_quit(True)
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
):
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
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'
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
438 create_jobject -- boolean
439 define if it should create a journal object if we are
444 Sets the gdk screen DPI setting (resolution) to the
445 Sugar screen resolution.
447 Connects our "destroy" message to our _destroy_cb
450 Creates a base gtk.Window within this window.
452 Creates an ActivityService (self._bus) servicing
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
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
480 self
._activity
_id
= handle
.activity_id
481 self
._pservice
= presenceservice
.get_instance()
482 self
.shared_activity
= None
483 self
._share
_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
= []
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
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
,
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",
530 if not self
.shared_activity
.props
.joined
:
531 self
.shared_activity
.join()
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
539 if share_scope
== SCOPE_INVITE_ONLY
:
540 self
.share(private
=True)
541 elif share_scope
== SCOPE_NEIGHBORHOOD
:
542 self
.share(private
=False)
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
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
):
573 def set_active(self
, active
):
574 if self
._active
!= active
:
575 self
._active
= active
576 if not self
._active
and self
._jobject
:
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
)
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
):
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']
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
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
696 logging
.debug('Activity.__save_cb')
697 self
._updating
_jobject
= False
698 if self
._quit
_requested
:
699 self
._session
.will_quit(self
, True)
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)
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
):
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()
723 def _get_preview(self
):
724 pixbuf
= self
._preview
.get_pixbuf()
728 pixbuf
= pixbuf
.scale_simple(style
.zoom(300), style
.zoom(225),
729 gtk
.gdk
.INTERP_BILINEAR
)
732 def save_func(buf
, data
):
735 pixbuf
.save_to_callback(save_func
, 'png', user_data
=preview_data
)
736 preview_data
= ''.join(preview_data
)
738 self
._preview
.clear()
742 def _get_buddies(self
):
743 if self
.shared_activity
is not None:
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
]
753 def take_screenshot(self
):
755 self
._preview
.take_screenshot(self
.canvas
)
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.')
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.')
776 buddies_dict
= self
._get
_buddies
()
778 self
.metadata
['buddies_id'] = cjson
.encode(buddies_dict
.keys())
779 self
.metadata
['buddies'] = cjson
.encode(self
._get
_buddies
())
781 preview
= self
._get
_preview
()
783 self
.metadata
['preview'] = dbus
.ByteArray(preview
)
786 file_path
= os
.path
.join(self
.get_activity_root(), 'instance',
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)
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
)
806 """Request that the activity 'Keep in Journal' the current state
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()
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
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
)
829 logging
.debug("Failed to join activity: %s" % err
)
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
:
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
853 logging
.debug('Share of activity %s failed: %s.' %
854 (self
._activity
_id
, err
))
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
)
866 self
.__privacy
_changed
_cb
(self
.shared_activity
, None)
870 def _invite_response_cb(self
, 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
)
879 self
.shared_activity
.invite(
880 buddy
, '', self
._invite
_response
_cb
)
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.
888 Calls self.share(True) to privately share the activity if it wasn't
891 self
._invites
_queue
.append(buddy_key
)
893 if (self
.shared_activity
is None
894 or not self
.shared_activity
.props
.joined
):
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." %
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",
916 self
._pservice
.share_activity(self
, private
=private
)
918 def _show_keep_failed_dialog(self
):
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
)
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)
940 """Activities should override this function if they want to perform
941 extra checks before actually closing."""
945 def _prepare_close(self
, skip_save
=False):
950 logging
.info(traceback
.format_exc())
951 self
._show
_keep
_failed
_dialog
()
954 if self
.shared_activity
:
955 self
.shared_activity
.leave()
961 def _complete_close(self
):
962 self
._cleanup
_jobject
()
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():
983 if not self
._closing
:
984 if not self
._prepare
_close
(skip_save
):
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
):
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
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.
1020 return self
._jobject
.metadata
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())
1033 _shared_activity
= property(lambda self
: self
.shared_activity
, None)
1040 if _session
is None:
1041 _session
= _ActivitySession()
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']
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
)