From 89ba4f2d60436aaf0c17d595f76acf1bf498ad9d Mon Sep 17 00:00:00 2001 From: Tomeu Vizoso Date: Sun, 26 Jul 2009 17:42:53 +0000 Subject: Reimplement using PyQt --- 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 @@ + + +]> + + \ 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 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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; } + -- cgit v0.9.1