Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomeu Vizoso <tomeu@sugarlabs.org>2009-07-26 17:42:53 (GMT)
committer Tomeu Vizoso <tomeu@sugarlabs.org>2009-07-26 17:42:53 (GMT)
commit89ba4f2d60436aaf0c17d595f76acf1bf498ad9d (patch)
tree8f77287239b6c2f6c8ab0b17e19134bd5c4f20e1
parentb895185c3729be77c1ecbfc9c8dce4f3d4f4d95e (diff)
Reimplement using PyQtHEADmaster
-rw-r--r--.gitignore2
-rw-r--r--activity.py42
-rw-r--r--activity/activity.info6
-rwxr-xr-xbin/sugar-activity-qt99
-rw-r--r--icons/activity-stop.svg6
-rw-r--r--icons/document-save.svg30
-rw-r--r--qtactivity.py799
-rw-r--r--style.py44
-rw-r--r--stylesheet.qss3
9 files changed, 1011 insertions, 20 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f836aa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*~
+*.pyc
diff --git a/activity.py b/activity.py
index 9a36f98..2990b34 100644
--- a/activity.py
+++ b/activity.py
@@ -1,4 +1,4 @@
-# Copyright 2009 Simon Schampijer
+# Copyright (C) 2009, Tomeu Vizoso
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,29 +14,37 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-"""HelloWorld Activity: A case study for developing an activity."""
-
-import gtk
+import os
+import sys
import logging
-
from gettext import gettext as _
-from sugar.activity import activity
+from PyQt4 import QtCore, QtGui
+
+from qtactivity import QActivity, QActivityToolBar
-class HelloWorldActivity(activity.Activity):
- """HelloWorldActivity class as specified in activity.info"""
+class HelloWorldQtActivity(QActivity):
def __init__(self, handle):
"""Set up the HelloWorld activity."""
- activity.Activity.__init__(self, handle)
+ QActivity.__init__(self, handle)
+
+ activity_tool_bar = QActivityToolBar(self)
+ toolbar = self.addToolBar(activity_tool_bar)
+
+ self._textEdit = QtGui.QTextEdit()
+ self.setCentralWidget(self._textEdit)
+
+ def read_file(self, file_path):
+ f = open(file_path, 'r')
+ text = f.read()
+ f.close()
- # top toolbar with close button
- toolbox = activity.ActivityToolbox(self)
- self.set_toolbox(toolbox)
- toolbox.show()
+ self._textEdit.setPlainText(text)
- # label with the text
- label = gtk.Label(_("Hello World!"))
- self.set_canvas(label)
- label.show()
+ def write_file(self, file_path):
+ text = self._textEdit.toPlainText()
+ f = open(file_path, 'w')
+ f.write(text)
+ f.close()
diff --git a/activity/activity.info b/activity/activity.info
index 2ab26a6..98934f1 100644
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,7 +1,7 @@
[Activity]
-name = HelloWorld
+name = HelloWorldQt
activity_version = 1
-bundle_id = org.sugarlabs.HelloWorld
-exec = sugar-activity activity.HelloWorldActivity
+bundle_id = org.sugarlabs.HelloWorldQt
+exec = sugar-activity-qt activity.HelloWorldQtActivity
icon = activity-helloworld
license = GPLv2+
diff --git a/bin/sugar-activity-qt b/bin/sugar-activity-qt
new file mode 100755
index 0000000..fc4d383
--- /dev/null
+++ b/bin/sugar-activity-qt
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import sys
+import gettext
+from optparse import OptionParser
+
+import sugar
+from sugar import logger
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.activity import activityhandle
+
+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.bindtextdomain('sugar-toolkit', sugar.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)
+
+from PyQt4 import QtGui
+app = QtGui.QApplication(sys.argv)
+
+print app.style().objectName()
+
+# TODO: the Sugar Gtk+ theme and engine should be enough?
+css_file = os.path.join(bundle_path, 'stylesheet.qss')
+if os.access(css_file, os.R_OK):
+ app.setStyleSheet(file(css_file).read())
+
+activity = activity_constructor(activity_handle)
+activity.show()
+
+sys.exit(app.exec_())
+
diff --git a/icons/activity-stop.svg b/icons/activity-stop.svg
new file mode 100644
index 0000000..11b82e8
--- /dev/null
+++ b/icons/activity-stop.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-stop">
+ <path d="M36.822,5H18.181L5.002,18.182V36.82L18.181,50h18.642l13.176-13.18V18.182L36.822,5z M35.75,35.414h-15.5v-15.5h15.5V35.414z" display="inline" fill="&fill_color;"/>
+</g></svg> \ No newline at end of file
diff --git a/icons/document-save.svg b/icons/document-save.svg
new file mode 100644
index 0000000..b84a374
--- /dev/null
+++ b/icons/document-save.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]>
+<svg enable-background="new 0 0 55 55" height="55px" id="Layer_1" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">
+<g display="block" id="document-save">
+ <g>
+ <g>
+ <g>
+ <path d="M6.736,49.002 h24.52c2.225,0,3.439-1.447,3.439-3.441V18.281c0-1.73-1.732-3.441-3.439-3.441h-4.389" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ </g>
+ </g>
+ <g>
+ <g>
+ <path d="M26.867,38.592 c0,1.836-1.345,3.201-3.441,4.047l-16.69,6.363V14.84l16.69-8.599c2.228-0.394,3.441,0.84,3.441,2.834V38.592z" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ </g>
+ </g>
+ <path d="M9.424,42.607 c0,0-1.351-0.543-2.702-0.543s-2.703,0.543-2.703,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
+ <path d="M9.424,32.006 c0,0-1.239-0.543-2.815-0.543c-1.577,0-2.59,0.543-2.59,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
+ <path d="M9.424,21.678 c0,0-1.125-0.544-2.927-0.544c-1.802,0-2.478,0.544-2.478,0.544" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
+
+ <line fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25" x1="13.209" x2="13.209" y1="46.533" y2="11.505"/>
+
+ <g>
+ <line fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" x1="41.17" x2="52.441" y1="16.188" y2="4.917"/>
+ <polyline fill="none" points=" 51.562,15.306 41.17,16.188 42.053,5.794 " stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ </g>
+ </g>
+</g>
+</svg> \ No newline at end of file
diff --git a/qtactivity.py b/qtactivity.py
new file mode 100644
index 0000000..edb84e2
--- /dev/null
+++ b/qtactivity.py
@@ -0,0 +1,799 @@
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+import time
+from hashlib import sha1
+import traceback
+from gettext import gettext as _
+
+from PyQt4 import QtCore, QtGui
+import gobject
+import gconf
+import dbus
+import dbus.service
+import cjson
+
+from sugar import util
+from sugar.presence import presenceservice
+from sugar.activity.activityservice import ActivityService
+from sugar.graphics.xocolor import XoColor
+from sugar.datastore import datastore
+from sugar.session import XSMPClient
+from sugar import wm
+
+import style
+
+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 QActivityToolBar(QtGui.QToolBar):
+ """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):
+ QtGui.QToolBar.__init__(self, 'Activity')
+
+ self.setIconSize(QtCore.QSize(style.STANDARD_ICON_SIZE,
+ style.STANDARD_ICON_SIZE))
+
+ spacer = QtGui.QWidget()
+ spacer.setMinimumSize(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
+ self.addWidget(spacer)
+
+ 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 = QtGui.QLineEdit()
+ self.title.setText(activity.metadata['title'])
+ """
+ self.title.set_size_request(int(gtk.gdk.screen_width() / 3), -1)
+ self.title.connect('changed', self.__title_changed_cb)
+ """
+ self.addWidget(self.title)
+
+ activity.metadata.connect('updated', self.__jobject_updated_cb)
+
+ spacer = QtGui.QWidget()
+ spacer.setSizePolicy(QtGui.QSizePolicy.Expanding,
+ QtGui.QSizePolicy.Expanding)
+ self.addWidget(spacer)
+
+ self.keep = QtGui.QAction(QtGui.QIcon('icons/document-save.svg'), _('Keep'), self)
+ self.keep.setShortcut('Ctrl+S')
+ activity.connect(self.keep, QtCore.SIGNAL('triggered()'), activity.save)
+ self.addAction(self.keep)
+
+ self.exit = QtGui.QAction(QtGui.QIcon('icons/activity-stop.svg'), _('Close'), self)
+ self.exit.setShortcut('Ctrl+Q')
+ activity.connect(self.exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
+ self.addAction(self.exit)
+
+ spacer = QtGui.QWidget()
+ spacer.setMinimumSize(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
+ self.addWidget(spacer)
+
+ 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.setText(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 __activity_shared_cb(self, activity):
+ self._update_share()
+
+ def __max_participants_changed_cb(self, activity, pspec):
+ self._update_share()
+
+class QActivity(QtGui.QMainWindow):
+ def __init__(self, handle):
+ QtGui.QMainWindow.__init__(self)
+
+ self._set_x11_property('_SUGAR_ACTIVITY_ID', handle.activity_id)
+ self._set_x11_property('_SUGAR_BUNDLE_ID', os.environ['SUGAR_BUNDLE_ID'])
+
+ # 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._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._read_file_called = False
+
+ 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)
+
+ self._bus = ActivityService(self)
+ self._owns_file = False
+
+ share_scope = SCOPE_PRIVATE
+
+ if handle.object_id:
+ self._jobject = datastore.get(handle.object_id)
+ self.setWindowTitle(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:
+ logging.debug('Creating a jobject.')
+ self._jobject = datastore.create()
+ title = _('%s Activity') % get_bundle_name()
+ self._jobject.metadata['title'] = title
+ self.setWindowTitle(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)
+ else:
+ QtCore.QTimer.singleShot(0, self._read_file_cb)
+
+ def _read_file_cb(self):
+ if self._jobject and self._jobject.file_path:
+ self.read_file(self._jobject.file_path)
+
+ 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()
+
+ def get_max_participants(self):
+ return self._max_participants
+
+ def set_max_participants(self, participants):
+ self._max_participants = 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 __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 __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):
+ """Returns an image representing the state of the activity. Generally
+ this is what the user is seeing in this moment.
+
+ Activities can override this method, which should return a str with the
+ binary content of a png image with a width of 300 and a height of 225
+ pixels.
+ """
+ if self.centralWidget() is None:
+ return None
+ pixmap = QtGui.QPixmap.grabWidget(self.centralWidget())
+ pixmap = pixmap.scaled(QtCore.QSize(style.zoom(300), style.zoom(225)))
+
+ byte_array = QtCore.QByteArray()
+ buf = QtCore.QBuffer(byte_array)
+ buf.open(QtCore.QIODevice.WriteOnly)
+ pixmap.save(buf, 'PNG')
+ return byte_array.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)
+
+ file_path = os.path.join(self.get_activity_root(), 'instance',
+ '%i' % time.time())
+ try:
+ self.write_file(file_path)
+ except NotImplementedError:
+ logging.debug('Activity.write_file is not implemented.')
+ else:
+ if os.path.exists(file_path):
+ self._owns_file = True
+ self._jobject.file_path = file_path
+
+ # 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:
+ 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 not self._closing:
+ if not self._prepare_close(skip_save):
+ return
+
+ if not self._updating_jobject:
+ self._complete_close()
+
+ 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)
+
+ def _set_x11_property(self, name, value):
+ import ctypes, ctypes.util
+
+ libX11 = ctypes.cdll.LoadLibrary(ctypes.util.find_library('X11'))
+
+ display = libX11.XOpenDisplay(None)
+
+ property_atom = libX11.XInternAtom(display, name, 0)
+ type_atom = libX11.XInternAtom(display, 'STRING', 0)
+
+ libX11.XChangeProperty(display, self.winId(), property_atom, type_atom,
+ 8, 0, value, len(value))
+
+ libX11.XFlush(display)
+
+ def realize(self):
+ # TODO
+ pass
+
+ def closeEvent(self, event):
+ try:
+ self.close()
+ event.accept()
+ except:
+ event.ignore()
+ raise
+
+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')
+
+_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/style.py b/style.py
new file mode 100644
index 0000000..6baba09
--- /dev/null
+++ b/style.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# 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 logging
+
+def _compute_zoom_factor():
+ if os.environ.has_key('SUGAR_SCALING'):
+ try:
+ scaling = int(os.environ['SUGAR_SCALING'])
+ return scaling / 100.0
+ except ValueError:
+ logging.error('Invalid SUGAR_SCALING.')
+
+ return 1.0
+
+def zoom(units):
+ return int(ZOOM_FACTOR * units)
+
+ZOOM_FACTOR = _compute_zoom_factor()
+
+GRID_CELL_SIZE = zoom(75)
+
+STANDARD_ICON_SIZE = zoom(55)
+SMALL_ICON_SIZE = zoom(55 * 0.5)
+MEDIUM_ICON_SIZE = zoom(55 * 1.5)
+LARGE_ICON_SIZE = zoom(55 * 2.0)
+XLARGE_ICON_SIZE = zoom(55 * 2.75)
+
diff --git a/stylesheet.qss b/stylesheet.qss
new file mode 100644
index 0000000..4aeccf1
--- /dev/null
+++ b/stylesheet.qss
@@ -0,0 +1,3 @@
+QToolBar { padding: 0px; spacing: 0px;}
+QTextEdit { border: 0px; background-color: white; }
+