From 488402df7d37cf68d421229968632696a2a97bd7 Mon Sep 17 00:00:00 2001 From: Marco Pesenti Gritti Date: Wed, 06 Feb 2008 09:20:33 +0000 Subject: Split sugar-toolkit out of sugar shell. --- (limited to 'sugar') diff --git a/sugar/.gitignore b/sugar/.gitignore new file mode 100644 index 0000000..de24e35 --- /dev/null +++ b/sugar/.gitignore @@ -0,0 +1,4 @@ +sugar-marshal.c +sugar-marshal.h +_sugarext.c +_sugarext.c diff --git a/sugar/.license b/sugar/.license new file mode 100644 index 0000000..6989ebe --- /dev/null +++ b/sugar/.license @@ -0,0 +1 @@ +LGPL diff --git a/sugar/Makefile.am b/sugar/Makefile.am new file mode 100644 index 0000000..e45ead9 --- /dev/null +++ b/sugar/Makefile.am @@ -0,0 +1,64 @@ +SUBDIRS = activity bundle clipboard graphics presence datastore + +sugardir = $(pythondir)/sugar +sugar_PYTHON = \ + env.py \ + network.py \ + profile.py \ + util.py \ + wm.py + +pkgpyexecdir = $(pythondir)/sugar + +pkgpyexec_LTLIBRARIES = _sugarext.la + +_sugarext_la_CFLAGS = \ + $(EXT_CFLAGS) \ + $(PYTHON_INCLUDES) + +_sugarext_la_LDFLAGS = -module -avoid-version +_sugarext_la_LIBADD = $(EXT_LIBS) + +_sugarext_la_SOURCES = \ + $(BUILT_SOURCES) \ + _sugarextmodule.c \ + eggaccelerators.c \ + eggaccelerators.h \ + sexy-icon-entry.h \ + sexy-icon-entry.c \ + sugar-address-entry.c \ + sugar-address-entry.h \ + sugar-key-grabber.c \ + sugar-key-grabber.h \ + sugar-menu.h \ + sugar-menu.c \ + sugar-preview.h \ + sugar-preview.c + +BUILT_SOURCES = \ + _sugarext.c \ + sugar-marshal.c \ + sugar-marshal.h + +_sugarext.c: _sugarext.defs _sugarext.override + +.defs.c: + (cd $(srcdir)\ + && $(PYGTK_CODEGEN) \ + --register $(PYGTK_DEFSDIR)/gdk-types.defs \ + --register $(PYGTK_DEFSDIR)/gtk-types.defs \ + --override $*.override \ + --prefix py$* $*.defs) > gen-$*.c \ + && cp gen-$*.c $*.c \ + && rm -f gen-$*.c + +sugar-marshal.c: sugar-marshal.list + $(GLIB_GENMARSHAL) --prefix=sugar_marshal \ + $(srcdir)/sugar-marshal.list --header --body > sugar-marshal.c + +sugar-marshal.h: sugar-marshal.list + $(GLIB_GENMARSHAL) --prefix=sugar_marshal \ + $(srcdir)/sugar-marshal.list --header > sugar-marshal.h + +CLEANFILES = $(BUILT_SOURCES) +EXTRA_DIST = sugar-marshal.list _sugarext.defs _sugarext.override diff --git a/sugar/_sugarext.defs b/sugar/_sugarext.defs new file mode 100644 index 0000000..1c9812e --- /dev/null +++ b/sugar/_sugarext.defs @@ -0,0 +1,196 @@ +;; -*- scheme -*- +; object definitions + +(define-object AddressEntry + (in-module "Sugar") + (parent "GtkEntry") + (c-name "SugarAddressEntry") + (gtype-id "SUGAR_TYPE_ADDRESS_ENTRY") +) + +(define-object KeyGrabber + (in-module "Sugar") + (parent "GObject") + (c-name "SugarKeyGrabber") + (gtype-id "SUGAR_TYPE_KEY_GRABBER") +) + +(define-object Menu + (in-module "Sugar") + (parent "GtkMenu") + (c-name "SugarMenu") + (gtype-id "SUGAR_TYPE_MENU") +) + +(define-object Preview + (in-module "Sugar") + (parent "GObject") + (c-name "SugarPreview") + (gtype-id "SUGAR_TYPE_PREVIEW") +) + +(define-object IconEntry + (in-module "Sexy") + (parent "GtkEntry") + (c-name "SexyIconEntry") + (gtype-id "SEXY_TYPE_ICON_ENTRY") +) + +;; Enumerations and flags ... + +(define-enum IconEntryPosition + (in-module "Sexy") + (c-name "SexyIconEntryPosition") + (gtype-id "SEXY_TYPE_ICON_ENTRY_POSITION") + (values + '("primary" "SEXY_ICON_ENTRY_PRIMARY") + '("secondary" "SEXY_ICON_ENTRY_SECONDARY") + ) +) + +;; From sugar-menu.h + +(define-method set_active + (of-object "SugarMenu") + (c-name "sugar_menu_set_active") + (return-type "none") + (parameters + '("gboolean" "active") + ) +) + +(define-method embed + (of-object "SugarMenu") + (c-name "sugar_menu_embed") + (return-type "none") + (parameters + '("GtkContainer" "container") + ) +) + +(define-method unembed + (of-object "SugarMenu") + (c-name "sugar_menu_unembed") + (return-type "none") +) + +;; From sugar-key-grabber.h + +(define-function sugar_key_grabber_get_type + (c-name "sugar_key_grabber_get_type") + (return-type "GType") +) + +(define-method grab + (of-object "SugarKeyGrabber") + (c-name "sugar_key_grabber_grab") + (return-type "none") + (parameters + '("const-char*" "key") + ) +) + +(define-method get_key + (of-object "SugarKeyGrabber") + (c-name "sugar_key_grabber_get_key") + (return-type "char*") + (parameters + '("guint" "keycode") + '("guint" "state") + ) +) + +;; From sexy-icon-entry.h + +(define-function sexy_icon_entry_get_type + (c-name "sexy_icon_entry_get_type") + (return-type "GType") +) + +(define-function sexy_icon_entry_new + (c-name "sexy_icon_entry_new") + (is-constructor-of "SexyIconEntry") + (return-type "GtkWidget*") +) + +(define-method set_icon + (of-object "SexyIconEntry") + (c-name "sexy_icon_entry_set_icon") + (return-type "none") + (parameters + '("SexyIconEntryPosition" "position") + '("GtkImage*" "icon" (null-ok)) + ) +) + +(define-method set_icon_highlight + (of-object "SexyIconEntry") + (c-name "sexy_icon_entry_set_icon_highlight") + (return-type "none") + (parameters + '("SexyIconEntryPosition" "position") + '("gboolean" "highlight") + ) +) + +(define-method get_icon + (of-object "SexyIconEntry") + (c-name "sexy_icon_entry_get_icon") + (return-type "GtkImage*") + (parameters + '("SexyIconEntryPosition" "position") + ) +) + +(define-method get_icon_highlight + (of-object "SexyIconEntry") + (c-name "sexy_icon_entry_get_icon_highlight") + (return-type "gboolean") + (parameters + '("SexyIconEntryPosition" "position") + ) +) + +(define-method add_clear_button + (of-object "SexyIconEntry") + (c-name "sexy_icon_entry_add_clear_button") + (return-type "none") +) + +;; From sugar-preview.h + +(define-function sugar_preview_get_type + (c-name "sugar_preview_get_type") + (return-type "GType") +) + +(define-method take_screenshot + (of-object "SugarPreview") + (c-name "sugar_preview_take_screenshot") + (return-type "none") + (parameters + '("GdkDrawable" "drawable") + ) +) + +(define-method set_size + (of-object "SugarPreview") + (c-name "sugar_preview_set_size") + (return-type "none") + (parameters + '("int" "width") + '("int" "height") + ) +) + +(define-method clear + (of-object "SugarPreview") + (c-name "sugar_preview_clear") + (return-type "none") +) + +(define-method get_pixbuf + (of-object "SugarPreview") + (c-name "sugar_preview_get_pixbuf") + (return-type "GdkPixbuf*") +) diff --git a/sugar/_sugarext.override b/sugar/_sugarext.override new file mode 100644 index 0000000..61fb815 --- /dev/null +++ b/sugar/_sugarext.override @@ -0,0 +1,30 @@ +/* -*- Mode: C; c-basic-offset: 4 -*- */ +%% +headers +#include + +#include "pygobject.h" +#include "sugar-address-entry.h" +#include "sugar-key-grabber.h" +#include "sugar-menu.h" +#include "sugar-preview.h" +#include "sexy-icon-entry.h" + +#include +#include + +%% +modulename _sugarext +%% +import gobject.GObject as PyGObject_Type +import gtk.Entry as PyGtkEntry_Type +import gtk.Menu as PyGtkMenu_Type +import gtk.Container as PyGtkContainer_Type +import gtk.gdk.Window as PyGdkWindow_Type +import gtk.gdk.Drawable as PyGdkDrawable_Type +import gtk.Image as PyGtkImage_Type +%% +ignore-glob + *_get_type + _* +%% diff --git a/sugar/_sugarextmodule.c b/sugar/_sugarextmodule.c new file mode 100644 index 0000000..6f6af6d --- /dev/null +++ b/sugar/_sugarextmodule.c @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2006-2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +/* include this first, before NO_IMPORT_PYGOBJECT is defined */ +#include + +extern PyMethodDef py_sugarext_functions[]; + +void py_sugarext_register_classes (PyObject *d); +void py_sugarext_add_constants (PyObject *module, const gchar *strip_prefix); + +DL_EXPORT(void) +init_sugarext(void) +{ + PyObject *m, *d; + + init_pygobject (); + + m = Py_InitModule ("_sugarext", py_sugarext_functions); + d = PyModule_GetDict (m); + + py_sugarext_register_classes (d); + py_sugarext_add_constants(m, "SEXY_"); + + if (PyErr_Occurred ()) { + Py_FatalError ("can't initialise module _sugarext"); + } +} diff --git a/sugar/activity/Makefile.am b/sugar/activity/Makefile.am new file mode 100644 index 0000000..9dfc8de --- /dev/null +++ b/sugar/activity/Makefile.am @@ -0,0 +1,9 @@ +sugardir = $(pythondir)/sugar/activity +sugar_PYTHON = \ + __init__.py \ + activity.py \ + activityfactory.py \ + activityhandle.py \ + activityservice.py \ + bundlebuilder.py \ + registry.py diff --git a/sugar/activity/__init__.py b/sugar/activity/__init__.py new file mode 100644 index 0000000..8a984ad --- /dev/null +++ b/sugar/activity/__init__.py @@ -0,0 +1,58 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Activity implementation code for Sugar-based activities + +Each activity within the OLPC environment must provide two +dbus services. The first, patterned after the + + sugar.activity.activityfactory.ActivityFactory + +class is responsible for providing a "create" method which +takes a small dictionary with values corresponding to a + + sugar.activity.activityhandle.ActivityHandle + +describing an individual instance of the activity. + +Each activity so registered is described by a + + sugar.activity.bundle.Bundle + +instance, which parses a specially formatted activity.info +file (stored in the activity directory's ./activity +subdirectory). The + + sugar.activity.bundlebuilder + +module provides facilities for the standard setup.py module +which produces and registers bundles from activity source +directories. + +Once instantiated by the ActivityFactory's create method, +each activity must provide an introspection API patterned +after the + + sugar.activity.activityservice.ActivityService + +class. This class allows for querying the ID of the root +window, requesting sharing across the network, and basic +"what type of application are you" queries. +""" +from sugar.activity.registry import ActivityRegistry +from sugar.activity.registry import get_registry +from sugar.activity.registry import ActivityInfo diff --git a/sugar/activity/__init__py b/sugar/activity/__init__py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/sugar/activity/__init__py diff --git a/sugar/activity/activity.py b/sugar/activity/activity.py new file mode 100644 index 0000000..9d87e9e --- /dev/null +++ b/sugar/activity/activity.py @@ -0,0 +1,933 @@ +"""Base class for activities written in Python + +This is currently the only definitive reference for what an +activity must do to participate in the Sugar desktop. + + A Basic Activity + +All activities must implement a class derived from 'Activity' in this class. +The convention is to call it ActivitynameActivity, but this is not required as +the activity.info file associated with your activity will tell the sugar-shell +which class to start. + +For example the most minimal Activity: + + + from sugar.activity import activity + + class ReadActivity(activity.Activity): + pass + +To get a real, working activity, you will at least have to implement: + __init__(), read_file() and write_file() + +Aditionally, you will probably need a at least a Toolbar so you can have some +interesting buttons for the user, like for example 'exit activity' + +See the methods of the Activity class below for more information on what you +will need for a real activity. +""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from gettext import gettext as _ +import logging +import os +import time +import tempfile +from hashlib import sha1 +import traceback + +import gtk, gobject +import dbus +import dbus.service +import json + +from sugar import util +from sugar.presence import presenceservice +from sugar.activity.activityservice import ActivityService +from sugar.graphics import style +from sugar.graphics.window import Window +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.alert import Alert +from sugar.graphics.icon import Icon +from sugar.datastore import datastore +from sugar import wm +from sugar import profile +from sugar import _sugarbaseext +from sugar import _sugarext + +SCOPE_PRIVATE = "private" +SCOPE_INVITE_ONLY = "invite" # shouldn't be shown in UI, it's implicit when you invite somebody +SCOPE_NEIGHBORHOOD = "public" + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' + +class ActivityToolbar(gtk.Toolbar): + """The Activity toolbar with the Journal entry title, sharing, Keep and Stop buttons + + All activities should have this toolbar. It is easiest to add it to your + Activity by using the ActivityToolbox. + """ + def __init__(self, activity): + gtk.Toolbar.__init__(self) + + self._activity = activity + self._updating_share = False + + activity.connect('shared', self.__activity_shared_cb) + activity.connect('joined', self.__activity_shared_cb) + activity.connect('notify::max_participants', + self.__max_participants_changed_cb) + + if activity.metadata: + self.title = gtk.Entry() + self.title.set_size_request(int(gtk.gdk.screen_width() / 6), -1) + self.title.set_text(activity.metadata['title']) + self.title.connect('changed', self.__title_changed_cb) + self._add_widget(self.title) + + activity.metadata.connect('updated', self.__jobject_updated_cb) + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.share = ToolComboBox(label_text=_('Share with:')) + self.share.combo.connect('changed', self.__share_changed_cb) + self.share.combo.append_item(SCOPE_PRIVATE, _('Private'), 'zoom-home') + self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'), + 'zoom-neighborhood') + self.insert(self.share, -1) + self.share.show() + + self._update_share() + + self.keep = ToolButton('document-save') + self.keep.set_tooltip(_('Keep')) + self.keep.connect('clicked', self.__keep_clicked_cb) + self.insert(self.keep, -1) + self.keep.show() + + self.stop = ToolButton('activity-stop') + self.stop.set_tooltip(_('Stop')) + self.stop.connect('clicked', self.__stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + self._update_title_sid = None + + def _update_share(self): + self._updating_share = True + + if self._activity.props.max_participants == 1: + self.share.hide() + + if self._activity.get_shared(): + self.share.set_sensitive(False) + self.share.combo.set_active(1) + else: + self.share.set_sensitive(True) + self.share.combo.set_active(0) + + self._updating_share = False + + def __share_changed_cb(self, combo): + if self._updating_share: + return + + model = self.share.combo.get_model() + it = self.share.combo.get_active_iter() + (scope, ) = model.get(it, 0) + if scope == SCOPE_NEIGHBORHOOD: + self._activity.share() + + def __keep_clicked_cb(self, button): + self._activity.copy() + + def __stop_clicked_cb(self, button): + self._activity.take_screenshot() + self._activity.close() + + def __jobject_updated_cb(self, jobject): + self.title.set_text(jobject['title']) + + def __title_changed_cb(self, entry): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add(1000, 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._shared_activity + if shared_activity: + shared_activity.props.name = title + + self._update_title_sid = None + return False + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __activity_shared_cb(self, activity): + self._update_share() + + def __max_participants_changed_cb(self, activity, pspec): + self._update_share() + +class EditToolbar(gtk.Toolbar): + """Provides the standard edit toolbar for Activities. + + Members: + undo -- the undo button + redo -- the redo button + copy -- the copy button + paste -- the paste button + separator -- A separator between undo/redo and copy/paste + + This class only provides the 'edit' buttons in a standard layout, your activity + will need to either hide buttons which make no sense for your Activity, or you + need to connect the button events to your own callbacks: + + ## Example from Read.activity: + # Create the edit toolbar: + self._edit_toolbar = EditToolbar(self._view) + # Hide undo and redo, they're not needed + self._edit_toolbar.undo.props.visible = False + self._edit_toolbar.redo.props.visible = False + # Hide the separator too: + self._edit_toolbar.separator.props.visible = False + + # As long as nothing is selected, copy needs to be insensitive: + self._edit_toolbar.copy.set_sensitive(False) + # When the user clicks the button, call _edit_toolbar_copy_cb() + self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb) + + # Add the edit toolbar: + toolbox.add_toolbar(_('Edit'), self._edit_toolbar) + # And make it visible: + self._edit_toolbar.show() + """ + def __init__(self): + gtk.Toolbar.__init__(self) + + self.undo = ToolButton('edit-undo') + self.undo.set_tooltip(_('Undo')) + self.insert(self.undo, -1) + self.undo.show() + + self.redo = ToolButton('edit-redo') + self.redo.set_tooltip(_('Redo')) + self.insert(self.redo, -1) + self.redo.show() + + self.separator = gtk.SeparatorToolItem() + self.separator.set_draw(True) + self.insert(self.separator, -1) + self.separator.show() + + self.copy = ToolButton('edit-copy') + self.copy.set_tooltip(_('Copy')) + self.insert(self.copy, -1) + self.copy.show() + + self.paste = ToolButton('edit-paste') + self.paste.set_tooltip(_('Paste')) + self.insert(self.paste, -1) + self.paste.show() + +class ActivityToolbox(Toolbox): + """Creates the Toolbox for the Activity + + By default, the toolbox contains only the ActivityToolbar. After creating the + toolbox, you can add your activity specific toolbars, for example the + EditToolbar. + + To add the ActivityToolbox to your Activity in MyActivity.__init__() do: + + # Create the Toolbar with the ActivityToolbar: + toolbox = activity.ActivityToolbox(self) + ... your code, inserting all other toolbars you need, like EditToolbar ... + + # Add the toolbox to the activity frame: + self.set_toolbox(toolbox) + # And make it visible: + toolbox.show() + """ + def __init__(self, activity): + Toolbox.__init__(self) + + self._activity_toolbar = ActivityToolbar(activity) + self.add_toolbar('Activity', self._activity_toolbar) + self._activity_toolbar.show() + + def get_activity_toolbar(self): + return self._activity_toolbar + +class Activity(Window, gtk.Container): + """This is the base Activity class that all other Activities derive from. This is where your activity starts. + + To get a working Activity: + 0. Derive your Activity from this class: + class MyActivity(activity.Activity): + ... + + 1. implement an __init__() method for your Activity class. + + Use your init method to create your own ActivityToolbar which will + contain some standard buttons: + toolbox = activity.ActivityToolbox(self) + + Add extra Toolbars to your toolbox. + + You should setup Activity sharing here too. + + Finaly, your Activity may need some resources which you can claim + here too. + + The __init__() method is also used to make the distinction between + being resumed from the Journal, or starting with a blank document. + + 2. Implement read_file() and write_file() + Most activities revolve around creating and storing Journal entries. + For example, Write: You create a document, it is saved to the Journal + and then later you resume working on the document. + + read_file() and write_file() will be called by sugar to tell your + Activity that it should load or save the document the user is working + on. + + 3. Implement our Activity Toolbars. + The Toolbars are added to your Activity in step 1 (the toolbox), but + you need to implement them somewhere. Now is a good time. + + There are a number of standard Toolbars. The most basic one, the one + your almost absolutely MUST have is the ActivityToolbar. Without + this, you're not really making a proper Sugar Activity (which may be + okay, but you should really stop and think about why not!) You do + this with the ActivityToolbox(self) call in step 1. + + Usually, you will also need the standard EditToolbar. This is the one + which has the standard copy and paste buttons. You need to derive + your own EditToolbar class from sugar.EditToolbar: + class EditToolbar(activity.EditToolbar): + ... + + See EditToolbar for the methods you should implement in your class. + + Finaly, your Activity will very likely need some activity specific + buttons and options you can create your own toolbars by deriving a + class from gtk.Toolbar: + class MySpecialToolbar(gtk.Toolbar): + ... + + 4. Use your creativity. Make your Activity something special and share + it with your friends! + + Read through the methods of the Activity class below, to learn more about + how to make an Activity work. + + Hint: A good and simple Activity to learn from is the Read activity. To + create your own activity, you may want to copy it and use it as a template. + """ + __gtype_name__ = 'SugarActivity' + + __gsignals__ = { + 'shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + __gproperties__ = { + 'active' : (bool, None, None, False, + gobject.PARAM_READWRITE), + 'max-participants': (int, None, None, 0, 1000, 0, + gobject.PARAM_READWRITE) + } + + def __init__(self, handle, create_jobject=True): + """Initialise the Activity + + handle -- sugar.activity.activityhandle.ActivityHandle + instance providing the activity id and access to the + presence service which *may* provide sharing for this + application + + create_jobject -- boolean + define if it should create a journal object if we are + not resuming + + Side effects: + + Sets the gdk screen DPI setting (resolution) to the + Sugar screen resolution. + + Connects our "destroy" message to our _destroy_cb + method. + + Creates a base gtk.Window within this window. + + Creates an ActivityService (self._bus) servicing + this application. + + Usage: + If your Activity implements __init__(), it should call + the base class __init()__ before doing Activity specific things. + + """ + Window.__init__(self) + + # process titles will only show 15 characters + # but they get truncated anyway so if more characters + # are supported in the future we will get a better view + # of the processes + proc_title = "%s <%s>" % (get_bundle_name(), handle.activity_id) + util.set_proc_title(proc_title) + + self.connect('realize', self.__realize_cb) + self.connect('delete-event', self.__delete_event_cb) + self.connect("key_press_event", self.__key_press_event_cb) + + self._active = False + self._activity_id = handle.activity_id + self._pservice = presenceservice.get_instance() + self._shared_activity = None + self._share_id = None + self._join_id = None + self._preview = _sugarext.Preview() + self._updating_jobject = False + self._closing = False + self._deleting = False + self._max_participants = 0 + self._invites_queue = [] + + self._bus = ActivityService(self) + self._owns_file = False + + share_scope = SCOPE_PRIVATE + + if handle.object_id: + self._jobject = datastore.get(handle.object_id) + # TODO: Don't create so many objects until we have versioning + # support in the datastore + #self._jobject.object_id = '' + #del self._jobject.metadata['ctime'] + del self._jobject.metadata['mtime'] + + self.set_title(self._jobject.metadata['title']) + + if self._jobject.metadata.has_key('share-scope'): + share_scope = self._jobject.metadata['share-scope'] + + elif create_jobject: + logging.debug('Creating a jobject.') + self._jobject = datastore.create() + self._jobject.metadata['title'] = _('%s Activity') % get_bundle_name() + self.set_title(self._jobject.metadata['title']) + self._jobject.metadata['title_set_by_user'] = '0' + self._jobject.metadata['activity'] = self.get_bundle_id() + self._jobject.metadata['activity_id'] = self.get_id() + self._jobject.metadata['keep'] = '0' + self._jobject.metadata['preview'] = '' + self._jobject.metadata['share-scope'] = SCOPE_PRIVATE + + if self._shared_activity is not None: + icon_color = self._shared_activity.props.color + else: + icon_color = profile.get_color().to_string() + + 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: + self._jobject = None + + # 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) + + def do_set_property(self, pspec, value): + if pspec.name == 'active': + if self._active != value: + self._active = value + if not self._active and self._jobject: + self.save() + elif pspec.name == 'max-participants': + self._max_participants = value + else: + Window.do_set_property(self, pspec, value) + + def do_get_property(self, pspec): + if pspec.name == 'active': + return self._active + elif pspec.name == 'max-participants': + return self._max_participants + else: + return Window.do_get_property(self, pspec) + + def get_id(self): + """Returns the activity id of the current instance of your activity. + + The activity id is sort-of-like the unix process id (PID). However, + unlike PIDs it is only different for each new instance (with + create_jobject = True set) and stays the same everytime a user + resumes an activity. This is also the identity of your Activity to other + XOs for use when sharing. + """ + return self._activity_id + + def get_bundle_id(self): + """Returns the bundle_id from the activity.info file""" + return os.environ['SUGAR_BUNDLE_ID'] + + def set_canvas(self, canvas): + """Sets the 'work area' of your activity with the canvas of your choice. + + One commonly used canvas is gtk.ScrolledWindow + """ + Window.set_canvas(self, canvas) + canvas.connect('map', self.__canvas_map_cb) + + def __canvas_map_cb(self, canvas): + if self._jobject and self._jobject.file_path: + self.read_file(self._jobject.file_path) + + def __jobject_create_cb(self): + pass + + def __jobject_error_cb(self, err): + logging.debug("Error creating activity datastore object: %s" % err) + + def get_activity_root(self): + """ FIXME: Deprecated. This part of the API has been moved + out of this class to the module itself + + Returns a path for saving Activity specific preferences, etc. + + Returns a path to the location in the filesystem where the activity can + store activity related data that doesn't pertain to the current + execution of the activity and thus cannot go into the DataStore. + + Currently, this will return something like ~/.sugar/default/MyActivityName/ + + Activities should ONLY save settings, user preferences and other data + which isn't specific to a journal item here. If (meta-)data is in anyway + specific to a journal entry, it MUST be stored in the DataStore. + """ + if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \ + os.environ['SUGAR_ACTIVITY_ROOT']: + return os.environ['SUGAR_ACTIVITY_ROOT'] + else: + return '/' + + def read_file(self, file_path): + """ + Subclasses implement this method if they support resuming objects from + the journal. 'file_path' is the file to read from. + + You should immediately open the file from the file_path, because the + file_name will be deleted immediately after returning from read_file(). + Once the file has been opened, you do not have to read it immediately: + After you have opened it, the file will only be really gone when you + close it. + + Although not required, this is also a good time to read all meta-data: + the file itself cannot be changed externally, but the title, description + and other metadata['tags'] may change. So if it is important for you to + notice changes, this is the time to record the originals. + """ + raise NotImplementedError + + def write_file(self, file_path): + """ + Subclasses implement this method if they support saving data to objects + in the journal. 'file_path' is the file to write to. + + If the user did make changes, you should create the file_path and save + all document data to it. + + Additionally, you should also write any metadata needed to resume your + activity. For example, the Read activity saves the current page and zoom + level, so it can display the page. + + Note: Currently, the file_path *WILL* be different from the one you + received in file_read(). Even if you kept the file_path from file_read() + open until now, you must still write the entire file to this file_path. + """ + raise NotImplementedError + + def __save_cb(self): + logging.debug('Activity.__save_cb') + self._updating_jobject = False + if self._closing: + self._cleanup_jobject() + self.destroy() + + def __save_error_cb(self, err): + logging.debug('Activity.__save_error_cb') + self._updating_jobject = False + if self._closing: + self._cleanup_jobject() + self.destroy() + 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): + pixbuf = self._preview.get_pixbuf() + if pixbuf is None: + return None + + pixbuf = pixbuf.scale_simple(style.zoom(300), style.zoom(225), + gtk.gdk.INTERP_BILINEAR) + + # TODO: Find a way of taking a png out of the pixbuf without saving + # to a temp file. Impementing gtk.gdk.Pixbuf.save_to_buffer in pygtk + # would solve this. + fd, file_path = tempfile.mkstemp('.png') + del fd + + pixbuf.save(file_path, 'png') + f = open(file_path) + try: + preview_data = f.read() + finally: + f.close() + os.remove(file_path) + + self._preview.clear() + + return preview_data + + def _get_buddies(self): + if self._shared_activity is not None: + buddies = {} + for buddy in self._shared_activity.get_joined_buddies(): + if not buddy.props.owner: + buddy_id = sha1(buddy.props.key).hexdigest() + buddies[buddy_id] = [buddy.props.nick, buddy.props.color] + return buddies + else: + return {} + + def take_screenshot(self): + if self.canvas and self.canvas.window: + self._preview.take_screenshot(self.canvas.window) + + 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. + """ + + 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'] = json.write(buddies_dict.keys()) + self.metadata['buddies'] = json.write(self._get_buddies()) + + preview = self._get_preview() + if self._preview: + self.metadata['preview'] = dbus.ByteArray(preview) + + try: + file_path = os.path.join(self.get_activity_root(), 'instance', + '%i' % time.time()) + self.write_file(file_path) + self._owns_file = True + self._jobject.file_path = file_path + except NotImplementedError: + pass + + # 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(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 _display_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) + + 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 close(self, force=False, 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 force: + if not self.can_close(): + return + + try: + if not skip_save: + self.save() + except: + logging.info(traceback.format_exc()) + self._display_keep_failed_dialog() + return + + if self._shared_activity: + self._shared_activity.leave() + + if self._updating_jobject: + self._closing = True + else: + self.destroy() + + # Make the exported object inaccessible + dbus.service.Object.remove_from_connection(self._bus) + + def __realize_cb(self, window): + wm.set_bundle_id(window.window, self.get_bundle_id()) + wm.set_activity_id(window.window, str(self._activity_id)) + + def __delete_event_cb(self, widget, event): + self.close() + return True + + def get_metadata(self): + """Returns the jobject metadata or None if there is no jobject. + + Activities can set metadata in write_file() using: + self.metadata['MyKey'] = "Something" + + and retrieve metadata in read_file() using: + self.metadata.get('MyKey', 'aDefaultValue') + + Note: Make sure your activity works properly if one or more of the + metadata items is missing. Never assume they will all be present. + """ + if self._jobject: + return self._jobject.metadata + else: + return None + + metadata = property(get_metadata, None) + + def __key_press_event_cb(self, widget, event): + key = gtk.gdk.keyval_name(event.keyval) + if key == 's' and (event.state & gtk.gdk.CONTROL_MASK): + logging.debug('Keep requested') + self.copy() + return True + +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/sugar/activity/activityfactory.py b/sugar/activity/activityfactory.py new file mode 100644 index 0000000..1638197 --- /dev/null +++ b/sugar/activity/activityfactory.py @@ -0,0 +1,316 @@ +"""Shell side object which manages request to start activity""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging +import subprocess +import signal + +import dbus +import gobject + +from sugar.presence import presenceservice +from sugar.activity.activityhandle import ActivityHandle +from sugar.activity import registry +from sugar import util +from sugar import env + +from errno import EEXIST + +import os + +# #3903 - this constant can be removed and assumed to be 1 when dbus-python +# 0.82.3 is the only version used +if dbus.version >= (0, 82, 3): + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1 +else: + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000 + +_SHELL_SERVICE = "org.laptop.Shell" +_SHELL_PATH = "/org/laptop/Shell" +_SHELL_IFACE = "org.laptop.Shell" + +_DS_SERVICE = "org.laptop.sugar.DataStore" +_DS_INTERFACE = "org.laptop.sugar.DataStore" +_DS_PATH = "/org/laptop/sugar/DataStore" + +_ACTIVITY_FACTORY_INTERFACE = "org.laptop.ActivityFactory" + +_RAINBOW_SERVICE_NAME = "org.laptop.security.Rainbow" +_RAINBOW_ACTIVITY_FACTORY_PATH = "/" +_RAINBOW_ACTIVITY_FACTORY_INTERFACE = "org.laptop.security.Rainbow" + +_children_pid = [] + +def _sigchild_handler(signum, frame): + for child_pid in _children_pid: + pid, status = os.waitpid(child_pid, os.WNOHANG) + if pid > 0: + _children_pid.remove(pid) + +signal.signal(signal.SIGCHLD, _sigchild_handler) + +def create_activity_id(): + """Generate a new, unique ID for this activity""" + pservice = presenceservice.get_instance() + + # create a new unique activity ID + i = 0 + act_id = None + while i < 10: + act_id = util.unique_id() + i += 1 + + # check through network activities + found = False + activities = pservice.get_activities() + for act in activities: + if act_id == act.props.id: + found = True + break + if not found: + return act_id + raise RuntimeError("Cannot generate unique activity id.") + +def get_environment(activity): + environ = os.environ.copy() + + bin_path = os.path.join(activity.path, 'bin') + + activity_root = env.get_profile_path(activity.bundle_id) + if not os.path.exists(activity_root): + os.mkdir(activity_root) + + data_dir = os.path.join(activity_root, 'instance') + if not os.path.exists(data_dir): + os.mkdir(data_dir) + + data_dir = os.path.join(activity_root, 'data') + if not os.path.exists(data_dir): + os.mkdir(data_dir) + + tmp_dir = os.path.join(activity_root, 'tmp') + if not os.path.exists(tmp_dir): + os.mkdir(tmp_dir) + + environ['SUGAR_BUNDLE_PATH'] = activity.path + environ['SUGAR_BUNDLE_ID'] = activity.bundle_id + environ['SUGAR_ACTIVITY_ROOT'] = activity_root + environ['PATH'] = bin_path + ':' + environ['PATH'] + #environ['RAINBOW_STRACE_LOG'] = '1' + + if activity.bundle_id in [ 'org.laptop.WebActivity', + 'org.laptop.GmailActivity', + 'org.laptop.WikiBrowseActivity' + ]: + environ['RAINBOW_CONSTANT_UID'] = 'yes' + + return environ + +def get_command(activity, activity_id=None, object_id=None, uri=None): + if not activity_id: + activity_id = create_activity_id() + + command = activity.command.split(' ') + command.extend(['-b', activity.bundle_id]) + command.extend(['-a', activity_id]) + + if object_id is not None: + command.extend(['-o', object_id]) + if uri is not None: + command.extend(['-u', uri]) + + print command + + return command + +def open_log_file(activity): + i = 1 + while True: + path = env.get_logs_path('%s-%s.log' % (activity.bundle_id, i)) + try: + fd = os.open(path, os.O_EXCL | os.O_CREAT \ + | os.O_SYNC | os.O_WRONLY, 0644) + f = os.fdopen(fd, 'w', 0) + return (path, f) + except OSError, e: + if e.errno == EEXIST: + i += 1 + else: + raise e + +class ActivityCreationHandler(gobject.GObject): + """Sugar-side activity creation interface + + This object uses a dbus method on the ActivityFactory + service to create the new activity. It generates + GObject events in response to the success/failure of + activity startup using callbacks to the service's + create call. + """ + + def __init__(self, service_name, handle): + """Initialise the handler + + service_name -- the service name of the bundle factory + activity_handle -- stores the values which are to + be passed to the service to uniquely identify + the activity to be created and the sharing + service that may or may not be connected with it + + sugar.activity.activityhandle.ActivityHandle instance + + calls the "create" method on the service for this + particular activity type and registers the + _reply_handler and _error_handler methods on that + call's results. + + The specific service which creates new instances of this + particular type of activity is created during the activity + registration process in shell bundle registry which creates + service definition files for each registered bundle type. + + If the file '/etc/olpc-security' exists, then activity launching + will be delegated to the prototype 'Rainbow' security service. + """ + gobject.GObject.__init__(self) + + self._service_name = service_name + self._handle = handle + + self._use_rainbow = os.path.exists('/etc/olpc-security') + if service_name in [ 'org.laptop.JournalActivity', + 'org.laptop.Terminal', + 'org.laptop.LogViewer', + 'org.laptop.Analyze' + ]: + self._use_rainbow = False + + bus = dbus.SessionBus() + + bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH) + self._shell = dbus.Interface(bus_object, _SHELL_IFACE) + + if handle.activity_id is not None and \ + handle.object_id is None: + datastore = dbus.Interface( + bus.get_object(_DS_SERVICE, _DS_PATH), _DS_INTERFACE) + datastore.find({ 'activity_id': self._handle.activity_id }, [], + reply_handler=self._find_object_reply_handler, + error_handler=self._find_object_error_handler) + else: + self._launch_activity() + + def _launch_activity(self): + if self._handle.activity_id != None: + self._shell.ActivateActivity(self._handle.activity_id, + reply_handler=self._activate_reply_handler, + error_handler=self._activate_error_handler) + else: + self._create_activity() + + def _create_activity(self): + if self._handle.activity_id is None: + self._handle.activity_id = create_activity_id() + + self._shell.NotifyLaunch( + self._service_name, self._handle.activity_id, + reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_error_handler) + + activity_registry = registry.get_registry() + activity = activity_registry.get_activity(self._service_name) + if activity: + environ = get_environment(activity) + (log_path, log_file) = open_log_file(activity) + command = get_command(activity, self._handle.activity_id, + self._handle.object_id, + self._handle.uri) + + if not self._use_rainbow: + p = subprocess.Popen(command, env=environ, cwd=activity.path, + stdout=log_file, stderr=log_file) + _children_pid.append(p.pid) + else: + log_file.close() + system_bus = dbus.SystemBus() + factory = system_bus.get_object(_RAINBOW_SERVICE_NAME, + _RAINBOW_ACTIVITY_FACTORY_PATH) + factory.CreateActivity( + log_path, + environ, + command, + environ['SUGAR_BUNDLE_PATH'], + environ['SUGAR_BUNDLE_ID'], + timeout=30 * DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND, + reply_handler=self._create_reply_handler, + error_handler=self._create_error_handler, + dbus_interface=_RAINBOW_ACTIVITY_FACTORY_INTERFACE) + + def _no_reply_handler(self, *args): + pass + + def _notify_launch_failure_error_handler(self, err): + logging.error('Notify launch failure failed %s' % err) + + def _notify_launch_error_handler(self, err): + logging.debug('Notify launch failed %s' % err) + + def _activate_reply_handler(self, activated): + if not activated: + self._create_activity() + + def _activate_error_handler(self, err): + logging.error("Activity activation request failed %s" % err) + + def _create_reply_handler(self): + logging.debug("Activity created %s (%s)." % + (self._handle.activity_id, self._service_name)) + + def _create_error_handler(self, err): + logging.error("Couldn't create activity %s (%s): %s" % + (self._handle.activity_id, self._service_name, err)) + self._shell.NotifyLaunchFailure( + self._handle.activity_id, reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_failure_error_handler) + + def _find_object_reply_handler(self, jobjects, count): + if count > 0: + if count > 1: + logging.debug("Multiple objects has the same activity_id.") + self._handle.object_id = jobjects[0]['uid'] + self._create_activity() + + def _find_object_error_handler(self, err): + logging.error("Datastore find failed %s" % err) + self._create_activity() + +def create(service_name, activity_handle=None): + """Create a new activity from its name.""" + if not activity_handle: + activity_handle = ActivityHandle() + return ActivityCreationHandler(service_name, activity_handle) + +def create_with_uri(service_name, uri): + """Create a new activity and pass the uri as handle.""" + activity_handle = ActivityHandle(uri=uri) + return ActivityCreationHandler(service_name, activity_handle) + +def create_with_object_id(service_name, object_id): + """Create a new activity and pass the object id as handle.""" + activity_handle = ActivityHandle(object_id=object_id) + return ActivityCreationHandler(service_name, activity_handle) diff --git a/sugar/activity/activityhandle.py b/sugar/activity/activityhandle.py new file mode 100644 index 0000000..f91651e --- /dev/null +++ b/sugar/activity/activityhandle.py @@ -0,0 +1,68 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from sugar.presence import presenceservice + +class ActivityHandle(object): + """Data structure storing simple activity metadata""" + def __init__( + self, activity_id=None, object_id=None, uri=None + ): + """Initialise the handle from activity_id + + activity_id -- unique id for the activity to be + created + object_id -- identity of the journal object + associated with the activity. It was used by + the journal prototype implementation, might + change when we do the real one. + + When you resume an activity from the journal + the object_id will be passed in. It's optional + since new activities does not have an + associated object (yet). + + XXX Not clear how this relates to the activity + id yet, i.e. not sure we really need both. TBF + uri -- URI associated with the activity. Used when + opening an external file or resource in the + activity, rather than a journal object + (downloads stored on the file system for + example or web pages) + """ + self.activity_id = activity_id + self.object_id = object_id + self.uri = uri + + def get_dict(self): + """Retrieve our settings as a dictionary""" + result = { 'activity_id' : self.activity_id } + if self.object_id: + result['object_id'] = self.object_id + if self.uri: + result['uri'] = self.uri + + return result + +def create_from_dict(handle_dict): + """Create a handle from a dictionary of parameters""" + result = ActivityHandle( + handle_dict['activity_id'], + object_id = handle_dict.get('object_id'), + uri = handle_dict.get('uri'), + ) + return result diff --git a/sugar/activity/activityservice.py b/sugar/activity/activityservice.py new file mode 100644 index 0000000..c884fcb --- /dev/null +++ b/sugar/activity/activityservice.py @@ -0,0 +1,70 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import dbus.service + +_ACTIVITY_SERVICE_NAME = "org.laptop.Activity" +_ACTIVITY_SERVICE_PATH = "/org/laptop/Activity" +_ACTIVITY_INTERFACE = "org.laptop.Activity" + +class ActivityService(dbus.service.Object): + """Base dbus service object that each Activity uses to export dbus methods. + + The dbus service is separate from the actual Activity object so that we can + tightly control what stuff passes through the dbus python bindings.""" + + def __init__(self, activity): + """Initialise the service for the given activity + + activity -- sugar.activity.activity.Activity instance + + Creates dbus services that use the instance's activity_id + as discriminants among all active services + of this type. That is, the services are all available + as names/paths derived from the instance's activity_id. + + The various methods exposed on dbus are just forwarded + to the client Activity object's equally-named methods. + """ + activity.realize() + + activity_id = activity.get_id() + service_name = _ACTIVITY_SERVICE_NAME + activity_id + object_path = _ACTIVITY_SERVICE_PATH + "/" + activity_id + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(service_name, bus=bus) + dbus.service.Object.__init__(self, bus_name, object_path) + + self._activity = activity + + @dbus.service.method(_ACTIVITY_INTERFACE) + def SetActive(self, active): + logging.debug('ActivityService.set_active: %s.' % active) + self._activity.props.active = active + + @dbus.service.method(_ACTIVITY_INTERFACE) + def Invite(self, buddy_key): + self._activity.invite(buddy_key) + + @dbus.service.method(_ACTIVITY_INTERFACE) + def TakeScreenshot(self): + self._activity.take_screenshot() + diff --git a/sugar/activity/bundlebuilder.py b/sugar/activity/bundlebuilder.py new file mode 100644 index 0000000..e992557 --- /dev/null +++ b/sugar/activity/bundlebuilder.py @@ -0,0 +1,382 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import sys +import os +import zipfile +import shutil +import subprocess +import re +import gettext + +from sugar import env +from sugar.bundle.activitybundle import ActivityBundle + +class _SvnFileList(list): + def __init__(self): + f = os.popen('svn list -R') + for line in f.readlines(): + filename = line.strip() + if os.path.isfile(filename): + self.append(filename) + f.close() + +class _GitFileList(list): + def __init__(self): + f = os.popen('git-ls-files') + for line in f.readlines(): + filename = line.strip() + if not filename.startswith('.'): + self.append(filename) + f.close() + +class _DefaultFileList(list): + def __init__(self): + for name in os.listdir('activity'): + if name.endswith('.svg'): + self.append(os.path.join('activity', name)) + + self.append('activity/activity.info') + + if os.path.isfile(_get_source_path('NEWS')): + self.append('NEWS') + +class _ManifestFileList(_DefaultFileList): + def __init__(self, manifest): + _DefaultFileList.__init__(self) + self.append(manifest) + + f = open(manifest,'r') + for line in f.readlines(): + stripped_line = line.strip() + if stripped_line and not stripped_line in self: + self.append(stripped_line) + f.close() + +def _extract_bundle(source_file, dest_dir): + if not os.path.exists(dest_dir): + os.mkdir(dest_dir) + + zf = zipfile.ZipFile(source_file) + + for i, name in enumerate(zf.namelist()): + path = os.path.join(dest_dir, name) + + if not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + outfile = open(path, 'wb') + outfile.write(zf.read(name)) + outfile.flush() + outfile.close() + +def _get_source_path(path=None): + if path: + return os.path.join(os.getcwd(), path) + else: + return os.getcwd() + +def _get_bundle_dir(): + bundle_name = os.path.basename(_get_source_path()) + return bundle_name + '.activity' + +def _get_install_dir(prefix): + return os.path.join(prefix, 'share/activities') + +def _get_package_name(bundle_name): + bundle = ActivityBundle(_get_source_path()) + zipname = '%s-%d.xo' % (bundle_name, bundle.get_activity_version()) + return zipname + +def _delete_backups(arg, dirname, names): + for name in names: + if name.endswith('~') or name.endswith('pyc'): + os.remove(os.path.join(dirname, name)) + +def _get_bundle_id(): + bundle = ActivityBundle(_get_source_path()) + return bundle.get_bundle_id() + +def cmd_help(): + print 'Usage: \n\ +setup.py dev - setup for development \n\ +setup.py dist - create a bundle package \n\ +setup.py install [dirname] - install the bundle \n\ +setup.py uninstall [dirname] - uninstall the bundle \n\ +setup.py genpot - generate the gettext pot file \n\ +setup.py genl10n - generate localization files \n\ +setup.py clean - clean the directory \n\ +setup.py release - do a new release of the bundle \n\ +setup.py help - print this message \n\ +' + +def cmd_dev(): + bundle_path = env.get_user_activities_path() + if not os.path.isdir(bundle_path): + os.mkdir(bundle_path) + bundle_path = os.path.join(bundle_path, _get_bundle_dir()) + try: + os.symlink(_get_source_path(), bundle_path) + except OSError: + if os.path.islink(bundle_path): + print 'ERROR - The bundle has been already setup for development.' + else: + print 'ERROR - A bundle with the same name is already installed.' + +def _get_file_list(manifest): + if os.path.isfile(manifest): + return _ManifestFileList(manifest) + elif os.path.isdir('.git'): + return _GitFileList() + elif os.path.isdir('.svn'): + return _SvnFileList() + else: + return _DefaultFileList() + +def _get_po_list(manifest): + file_list = {} + + po_regex = re.compile("po/(.*)\.po$") + for file_name in _get_file_list(manifest): + match = po_regex.match(file_name) + if match: + file_list[match.group(1)] = file_name + + return file_list + +def _get_l10n_list(manifest): + l10n_list = [] + + for lang in _get_po_list(manifest).keys(): + filename = _get_bundle_id() + '.mo' + l10n_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename)) + l10n_list.append(os.path.join('locale', lang, 'activity.linfo')) + + return l10n_list + +def _get_activity_name(): + info_path = os.path.join(_get_source_path(), 'activity', 'activity.info') + f = open(info_path,'r') + info = f.read() + f.close() + match = re.search('^name\s*=\s*(.*)$', info, flags = re.MULTILINE) + return match.group(1) + +def cmd_dist(bundle_name, manifest): + cmd_genl10n(bundle_name, manifest) + file_list = _get_file_list(manifest) + + zipname = _get_package_name(bundle_name) + bundle_zip = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) + base_dir = bundle_name + '.activity' + + for filename in file_list: + bundle_zip.write(filename, os.path.join(base_dir, filename)) + + for filename in _get_l10n_list(manifest): + bundle_zip.write(filename, os.path.join(base_dir, filename)) + + bundle_zip.close() + +def cmd_install(bundle_name, manifest, prefix): + cmd_dist(bundle_name, manifest) + cmd_uninstall(prefix) + + _extract_bundle(_get_package_name(bundle_name), + _get_install_dir(prefix)) + +def cmd_uninstall(prefix): + path = os.path.join(_get_install_dir(prefix), _get_bundle_dir()) + if os.path.isdir(path): + shutil.rmtree(path) + +def cmd_genpot(bundle_name, manifest): + po_path = os.path.join(_get_source_path(), 'po') + if not os.path.isdir(po_path): + os.mkdir(po_path) + + python_files = [] + file_list = _get_file_list(manifest) + for file_name in file_list: + if file_name.endswith('.py'): + python_files.append(file_name) + + # First write out a stub .pot file containing just the translated + # activity name, then have xgettext merge the rest of the + # translations into that. (We can't just append the activity name + # to the end of the .pot file afterwards, because that might + # create a duplicate msgid.) + pot_file = os.path.join('po', '%s.pot' % bundle_name) + activity_name = _get_activity_name() + escaped_name = re.sub('([\\\\"])', '\\\\\\1', activity_name) + f = open(pot_file, 'w') + f.write('#: activity/activity.info:2\n') + f.write('msgid "%s"\n' % escaped_name) + f.write('msgstr ""\n') + f.close() + + args = [ 'xgettext', '--join-existing', '--language=Python', + '--keyword=_', '--add-comments=TRANS:', '--output=%s' % pot_file ] + + args += python_files + retcode = subprocess.call(args) + if retcode: + print 'ERROR - xgettext failed with return code %i.' % retcode + + +def cmd_genl10n(bundle_name, manifest): + source_path = _get_source_path() + activity_name = _get_activity_name() + + po_list = _get_po_list(manifest) + for lang in po_list.keys(): + file_name = po_list[lang] + + localedir = os.path.join(source_path, 'locale', lang) + mo_path = os.path.join(localedir, 'LC_MESSAGES') + if not os.path.isdir(mo_path): + os.makedirs(mo_path) + + mo_file = os.path.join(mo_path, "%s.mo" % _get_bundle_id()) + args = ["msgfmt", "--output-file=%s" % mo_file, file_name] + retcode = subprocess.call(args) + if retcode: + print 'ERROR - msgfmt failed with return code %i.' % retcode + + cat = gettext.GNUTranslations(open(mo_file, 'r')) + translated_name = cat.gettext(activity_name) + linfo_file = os.path.join(localedir, 'activity.linfo') + f = open(linfo_file, 'w') + f.write('[Activity]\nname = %s\n' % translated_name) + f.close() + +def cmd_release(bundle_name, manifest): + if not os.path.isdir('.git'): + print 'ERROR - this command works only for git repositories' + + retcode = subprocess.call(['git', 'pull']) + if retcode: + print 'ERROR - cannot pull from git' + + print 'Bumping activity version...' + + info_path = os.path.join(_get_source_path(), 'activity', 'activity.info') + f = open(info_path,'r') + info = f.read() + f.close() + + exp = re.compile('activity_version\s?=\s?([0-9]*)') + match = re.search(exp, info) + version = int(match.group(1)) + 1 + info = re.sub(exp, 'activity_version = %d' % version, info) + + f = open(info_path, 'w') + f.write(info) + f.close() + + news_path = os.path.join(_get_source_path(), 'NEWS') + + if os.environ.has_key('SUGAR_NEWS'): + print 'Update NEWS.sugar...' + + sugar_news_path = os.environ['SUGAR_NEWS'] + if os.path.isfile(sugar_news_path): + f = open(sugar_news_path,'r') + sugar_news = f.read() + f.close() + else: + sugar_news = '' + + sugar_news += '%s - %d\n\n' % (bundle_name, version) + + f = open(news_path,'r') + for line in f.readlines(): + if len(line.strip()) > 0: + sugar_news += line + else: + break + f.close() + + sugar_news += '\n' + + f = open(sugar_news_path, 'w') + f.write(sugar_news) + f.close() + + print 'Update NEWS...' + + f = open(news_path,'r') + news = f.read() + f.close() + + news = '%d\n\n' % version + news + + f = open(news_path, 'w') + f.write(news) + f.close() + + print 'Committing to git...' + + changelog = 'Release version %d.' % version + retcode = subprocess.call(['git', 'commit', '-a', '-m % s' % changelog]) + if retcode: + print 'ERROR - cannot commit to git' + + retcode = subprocess.call(['git', 'push']) + if retcode: + print 'ERROR - cannot push to git' + + print 'Creating the bundle...' + cmd_dist(bundle_name, manifest) + + print 'Done.' + +def cmd_clean(): + os.path.walk('.', _delete_backups, None) + +def sanity_check(): + if not os.path.isfile(_get_source_path('NEWS')): + print 'WARNING: NEWS file is missing.' + +def start(bundle_name, manifest='MANIFEST'): + sanity_check() + + if len(sys.argv) < 2: + cmd_help() + elif sys.argv[1] == 'build': + pass + elif sys.argv[1] == 'dev': + cmd_dev() + elif sys.argv[1] == 'dist': + cmd_dist(bundle_name, manifest) + elif sys.argv[1] == 'install' and len(sys.argv) == 3: + cmd_install(bundle_name, manifest, sys.argv[2]) + elif sys.argv[1] == 'uninstall' and len(sys.argv) == 3: + cmd_uninstall(sys.argv[2]) + elif sys.argv[1] == 'genpot': + cmd_genpot(bundle_name, manifest) + elif sys.argv[1] == 'genl10n': + cmd_genl10n(bundle_name, manifest) + elif sys.argv[1] == 'clean': + cmd_clean() + elif sys.argv[1] == 'release': + cmd_release(bundle_name, manifest) + else: + cmd_help() + +if __name__ == '__main__': + start() diff --git a/sugar/activity/registry.py b/sugar/activity/registry.py new file mode 100644 index 0000000..ac672d5 --- /dev/null +++ b/sugar/activity/registry.py @@ -0,0 +1,167 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2007 One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import gobject + +_ACTIVITY_REGISTRY_SERVICE_NAME = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_IFACE = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_PATH = '/org/laptop/ActivityRegistry' + +def _activity_info_from_dict(info_dict): + if not info_dict: + return None + return ActivityInfo(info_dict['name'], info_dict['icon'], + info_dict['bundle_id'], info_dict['version'], + info_dict['path'], info_dict['show_launcher'], + info_dict['command']) + +class ActivityInfo(object): + def __init__(self, name, icon, bundle_id, version, + path, show_launcher, command): + self.name = name + self.icon = icon + self.bundle_id = bundle_id + self.version = version + self.path = path + self.command = command + self.show_launcher = show_launcher + +class ActivityRegistry(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self): + gobject.GObject.__init__(self) + + bus = dbus.SessionBus() + + # NOTE: We need to follow_name_owner_changes here + # because we can not connect to a signal unless + # we follow the changes or we start the service + # before we connect. Starting the service here + # causes a major bottleneck during startup + bus_object = bus.get_object(_ACTIVITY_REGISTRY_SERVICE_NAME, + _ACTIVITY_REGISTRY_PATH, + follow_name_owner_changes = True) + self._registry = dbus.Interface(bus_object, _ACTIVITY_REGISTRY_IFACE) + self._registry.connect_to_signal('ActivityAdded', self._activity_added_cb) + self._registry.connect_to_signal('ActivityRemoved', self._activity_removed_cb) + + # Two caches fo saving some travel across dbus. + self._service_name_to_activity_info = {} + self._mime_type_to_activities = {} + + def _convert_info_list(self, info_list): + result = [] + + for info_dict in info_list: + result.append(_activity_info_from_dict(info_dict)) + + return result + + def get_activities(self): + info_list = self._registry.GetActivities() + return self._convert_info_list(info_list) + + def _get_activities_cb(self, reply_handler, info_list): + result = [] + i = 0 + for info_dict in info_list: + result.append(_activity_info_from_dict(info_dict)) + + reply_handler(result) + + def _get_activities_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + logging.error('Error getting activities async: %s' % str(e)) + + def get_activities_async(self, reply_handler=None, error_handler=None): + if not reply_handler: + logging.error('Function get_activities_async called without a reply handler. Can not run.') + return + + self._registry.GetActivities( + reply_handler=lambda info_list:self._get_activities_cb(reply_handler, info_list), + error_handler=lambda e:self._get_activities_error_cb(error_handler, e)) + + def get_activity(self, service_name): + if self._service_name_to_activity_info.has_key(service_name): + return self._service_name_to_activity_info[service_name] + + info_dict = self._registry.GetActivity(service_name) + activity_info = _activity_info_from_dict(info_dict) + + self._service_name_to_activity_info[service_name] = activity_info + return activity_info + + def find_activity(self, name): + info_list = self._registry.FindActivity(name) + return self._convert_info_list(info_list) + + def get_activities_for_type(self, mime_type): + if self._mime_type_to_activities.has_key(mime_type): + return self._mime_type_to_activities[mime_type] + + info_list = self._registry.GetActivitiesForType(mime_type) + activities = self._convert_info_list(info_list) + + self._mime_type_to_activities[mime_type] = activities + return activities + + def add_bundle(self, bundle_path): + result = self._registry.AddBundle(bundle_path) + # Need to invalidate here because get_activity could be called after + # add_bundle and before we receive activity-added, causing a race. + self._invalidate_cache() + return result + + def _activity_added_cb(self, info_dict): + logging.debug('ActivityRegistry._activity_added_cb: invalidating cache') + self._invalidate_cache() + self.emit('activity-added', _activity_info_from_dict(info_dict)) + + def _invalidate_cache(self): + self._service_name_to_activity_info.clear() + self._mime_type_to_activities.clear() + + def remove_bundle(self, bundle_path): + self._service_name_to_activity_info.clear() + self._mime_type_to_activities.clear() + return self._registry.RemoveBundle(bundle_path) + + def _activity_removed_cb(self, info_dict): + logging.debug('ActivityRegistry._activity_removed_cb: flushing caches') + self._service_name_to_activity_info.clear() + self._mime_type_to_activities.clear() + self.emit('activity-removed', _activity_info_from_dict(info_dict)) + +_registry = None + +def get_registry(): + global _registry + if not _registry: + _registry = ActivityRegistry() + return _registry diff --git a/sugar/bundle/Makefile.am b/sugar/bundle/Makefile.am new file mode 100644 index 0000000..f1af791 --- /dev/null +++ b/sugar/bundle/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/sugar/bundle +sugar_PYTHON = \ + __init__.py \ + bundle.py \ + activitybundle.py \ + contentbundle.py diff --git a/sugar/bundle/__init__.py b/sugar/bundle/__init__.py new file mode 100644 index 0000000..85ebced --- /dev/null +++ b/sugar/bundle/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. diff --git a/sugar/bundle/activitybundle.py b/sugar/bundle/activitybundle.py new file mode 100644 index 0000000..ee72f80 --- /dev/null +++ b/sugar/bundle/activitybundle.py @@ -0,0 +1,321 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Sugar activity bundles""" + +from ConfigParser import ConfigParser +import locale +import os +import tempfile + +from sugar.bundle.bundle import Bundle, MalformedBundleException, \ + AlreadyInstalledException, RegistrationException, \ + NotInstalledException + +from sugar import activity +from sugar import env + +import logging + +class ActivityBundle(Bundle): + """A Sugar activity bundle + + See http://wiki.laptop.org/go/Activity_bundles for details + """ + + MIME_TYPE = 'application/vnd.olpc-sugar' + DEPRECATED_MIME_TYPE = 'application/vnd.olpc-x-sugar' + + _zipped_extension = '.xo' + _unzipped_extension = '.activity' + _infodir = 'activity' + + def __init__(self, path): + Bundle.__init__(self, path) + self.activity_class = None + self.bundle_exec = None + + self._name = None + self._icon = None + self._bundle_id = None + self._mime_types = None + self._show_launcher = True + self._activity_version = 0 + + info_file = self._get_file('activity/activity.info') + if info_file is None: + raise MalformedBundleException('No activity.info file') + self._parse_info(info_file) + + linfo_file = self._get_linfo_file() + if linfo_file: + self._parse_linfo(linfo_file) + + def _parse_info(self, info_file): + cp = ConfigParser() + cp.readfp(info_file) + + section = 'Activity' + + if cp.has_option(section, 'bundle_id'): + self._bundle_id = cp.get(section, 'bundle_id') + # FIXME deprecated + elif cp.has_option(section, 'service_name'): + self._bundle_id = cp.get(section, 'service_name') + else: + raise MalformedBundleException( + 'Activity bundle %s does not specify a bundle id' % + self._path) + + if cp.has_option(section, 'name'): + self._name = cp.get(section, 'name') + else: + raise MalformedBundleException( + 'Activity bundle %s does not specify a name' % self._path) + + # FIXME class is deprecated + if cp.has_option(section, 'class'): + self.activity_class = cp.get(section, 'class') + elif cp.has_option(section, 'exec'): + self.bundle_exec = cp.get(section, 'exec') + else: + raise MalformedBundleException( + 'Activity bundle %s must specify either class or exec' % + self._path) + + if cp.has_option(section, 'mime_types'): + mime_list = cp.get(section, 'mime_types').strip(';') + self._mime_types = [ mime.strip() for mime in mime_list.split(';') ] + + if cp.has_option(section, 'show_launcher'): + if cp.get(section, 'show_launcher') == 'no': + self._show_launcher = False + + if cp.has_option(section, 'icon'): + self._icon = cp.get(section, 'icon') + + if cp.has_option(section, 'activity_version'): + version = cp.get(section, 'activity_version') + try: + self._activity_version = int(version) + except ValueError: + raise MalformedBundleException( + 'Activity bundle %s has invalid version number %s' % + (self._path, version)) + + def _get_linfo_file(self): + lang = locale.getdefaultlocale()[0] + if not lang: + return None + + linfo_path = os.path.join('locale', lang, 'activity.linfo') + linfo_file = self._get_file(linfo_path) + if linfo_file is not None: + return linfo_file + + linfo_path = os.path.join('locale', lang[:2], 'activity.linfo') + linfo_file = self._get_file(linfo_path) + if linfo_file is not None: + return linfo_file + + return None + + def _parse_linfo(self, linfo_file): + cp = ConfigParser() + cp.readfp(linfo_file) + + section = 'Activity' + + if cp.has_option(section, 'name'): + self._name = cp.get(section, 'name') + + def get_locale_path(self): + """Get the locale path inside the (installed) activity bundle.""" + if not self._unpacked: + raise NotInstalledException + return os.path.join(self._path, 'locale') + + def get_icons_path(self): + """Get the icons path inside the (installed) activity bundle.""" + if not self._unpacked: + raise NotInstalledException + return os.path.join(self._path, 'icons') + + def get_path(self): + """Get the activity bundle path.""" + return self._path + + def get_name(self): + """Get the activity user visible name.""" + return self._name + + def get_bundle_id(self): + """Get the activity bundle id""" + return self._bundle_id + + # FIXME: this should return the icon data, not a filename, so that + # we don't need to create a temp file in the zip case + def get_icon(self): + """Get the activity icon name""" + icon_path = os.path.join('activity', self._icon + '.svg') + if self._unpacked: + return os.path.join(self._path, icon_path) + else: + icon_data = self._get_file(icon_path).read() + temp_file, temp_file_path = tempfile.mkstemp(self._icon) + os.write(temp_file, icon_data) + os.close(temp_file) + return temp_file_path + + def get_activity_version(self): + """Get the activity version""" + return self._activity_version + + def get_command(self): + """Get the command to execute to launch the activity factory""" + if self.bundle_exec: + command = os.path.expandvars(self.bundle_exec) + else: + command = 'sugar-activity ' + self.activity_class + + return command + + + def get_mime_types(self): + """Get the MIME types supported by the activity""" + return self._mime_types + + def get_show_launcher(self): + """Get whether there should be a visible launcher for the activity""" + return self._show_launcher + + def is_installed(self): + if activity.get_registry().get_activity(self._bundle_id): + return True + else: + return False + + def need_upgrade(self): + act = activity.get_registry().get_activity(self._bundle_id) + if act is None or act.version != self._activity_version: + return True + else: + return False + + def install(self): + act = activity.get_registry().get_activity(self._bundle_id) + if act is not None and act.path.startswith(env.get_user_activities_path()): + raise AlreadyInstalledException + + install_dir = env.get_user_activities_path() + self._unzip(install_dir) + + install_path = os.path.join(install_dir, self._zip_root_dir) + + xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) + + mime_path = os.path.join(install_path, 'activity', 'mimetypes.xml') + if os.path.isfile(mime_path): + mime_dir = os.path.join(xdg_data_home, 'mime') + mime_pkg_dir = os.path.join(mime_dir, 'packages') + if not os.path.isdir(mime_pkg_dir): + os.makedirs(mime_pkg_dir) + installed_mime_path = os.path.join(mime_pkg_dir, '%s.xml' % self._bundle_id) + os.symlink(mime_path, installed_mime_path) + os.spawnlp(os.P_WAIT, 'update-mime-database', + 'update-mime-database', mime_dir) + + mime_types = self.get_mime_types() + if mime_types is not None: + installed_icons_dir = os.path.join(xdg_data_home, + 'icons/sugar/scalable/mimetypes') + if not os.path.isdir(installed_icons_dir): + os.makedirs(installed_icons_dir) + + for mime_type in mime_types: + mime_icon_base = os.path.join(install_path, 'activity', + mime_type.replace('/', '-')) + svg_file = mime_icon_base + '.svg' + info_file = mime_icon_base + '.icon' + if os.path.isfile(svg_file): + os.symlink(svg_file, + os.path.join(installed_icons_dir, + os.path.basename(svg_file))) + if os.path.isfile(info_file): + os.symlink(info_file, + os.path.join(installed_icons_dir, + os.path.basename(info_file))) + + if not activity.get_registry().add_bundle(install_path): + raise RegistrationException + + def uninstall(self, force=False): + if self._unpacked: + install_path = self._path + else: + if not self.is_installed(): + raise NotInstalledException + + act = activity.get_registry().get_activity(self._bundle_id) + if not force and act.version != self._activity_version: + logging.warning('Not uninstalling because different bundle present') + return + elif not act.path.startswith(env.get_user_activities_path()): + logging.warning('Not uninstalling system activity') + return + + install_path = os.path.join(env.get_user_activities_path(), + self._zip_root_dir) + + xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) + + mime_dir = os.path.join(xdg_data_home, 'mime') + installed_mime_path = os.path.join(mime_dir, 'packages', '%s.xml' % self._bundle_id) + if os.path.exists(installed_mime_path): + os.remove(installed_mime_path) + os.spawnlp(os.P_WAIT, 'update-mime-database', + 'update-mime-database', mime_dir) + + mime_types = self.get_mime_types() + if mime_types is not None: + installed_icons_dir = os.path.join(xdg_data_home, + 'icons/sugar/scalable/mimetypes') + for file in os.listdir(installed_icons_dir): + path = os.path.join(installed_icons_dir, file) + if os.path.islink(path) and \ + os.readlink(path).startswith(install_path): + os.remove(path) + + self._uninstall(install_path) + + if not activity.get_registry().remove_bundle(install_path): + raise RegistrationException + + def upgrade(self): + act = activity.get_registry().get_activity(self._bundle_id) + if act is None: + logging.warning('Activity not installed') + elif act.path.startswith(env.get_user_activities_path()): + try: + self.uninstall(force=True) + except Exception, e: + logging.warning('Uninstall failed (%s), still trying to install newer bundle', e) + else: + logging.warning('Unable to uninstall system activity, installing upgraded version in user activities') + + self.install() + diff --git a/sugar/bundle/bundle.py b/sugar/bundle/bundle.py new file mode 100644 index 0000000..f7f18c9 --- /dev/null +++ b/sugar/bundle/bundle.py @@ -0,0 +1,146 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Sugar bundle file handler""" + +import os +import StringIO +import zipfile + +class AlreadyInstalledException(Exception): pass +class NotInstalledException(Exception): pass +class InvalidPathException(Exception): pass +class ZipExtractException(Exception): pass +class RegistrationException(Exception): pass +class MalformedBundleException(Exception): pass + +class Bundle: + """A Sugar activity, content module, etc. + + The bundle itself may be either a zip file or a directory + hierarchy, with metadata about the bundle stored various files + inside it. + + This is an abstract base class. See ActivityBundle and + ContentBundle for more details on those bundle types. + """ + def __init__(self, path): + self._path = path + + if os.path.isdir(self._path): + self._unpacked = True + else: + self._unpacked = False + self._check_zip_bundle() + + # manifest = self._get_file(self._infodir + '/contents') + # if manifest is None: + # raise MalformedBundleException('No manifest file') + # + # signature = self._get_file(self._infodir + '/contents.sig') + # if signature is None: + # raise MalformedBundleException('No signature file') + + def _check_zip_bundle(self): + zip_file = zipfile.ZipFile(self._path) + file_names = zip_file.namelist() + if len(file_names) == 0: + raise MalformedBundleException('Empty zip file') + + if file_names[0] == 'mimetype': + del file_names[0] + + self._zip_root_dir = file_names[0].split('/')[0] + if self._unzipped_extension is not None: + (name, ext) = os.path.splitext(self._zip_root_dir) + if ext != self._unzipped_extension: + raise MalformedBundleException( + 'All files in the bundle must be inside a single ' + + 'directory whose name ends with "%s"' % + self._unzipped_extension) + + for file_name in file_names: + if not file_name.startswith(self._zip_root_dir): + raise MalformedBundleException( + 'All files in the bundle must be inside a single ' + + 'top-level directory') + + def _get_file(self, filename): + file = None + + if self._unpacked: + path = os.path.join(self._path, filename) + if os.path.isfile(path): + file = open(path) + else: + zip_file = zipfile.ZipFile(self._path) + path = os.path.join(self._zip_root_dir, filename) + try: + data = zip_file.read(path) + file = StringIO.StringIO(data) + except KeyError: + # == "file not found" + pass + zip_file.close() + + return file + + def get_path(self): + """Get the bundle path.""" + return self._path + + def _unzip(self, install_dir): + if self._unpacked: + raise AlreadyInstalledException + + if not os.path.isdir(install_dir): + os.mkdir(install_dir, 0775) + + # zipfile provides API that in theory would let us do this + # correctly by hand, but handling all the oddities of + # Windows/UNIX mappings, extension attributes, deprecated + # features, etc makes it impractical. + # FIXME: use manifest + if os.spawnlp(os.P_WAIT, 'unzip', 'unzip', '-o', self._path, + '-x', 'mimetype', '-d', install_dir): + raise ZipExtractException + + def _zip(self, bundle_path): + if not self._unpacked: + raise NotInstalledException + + # FIXME: use manifest + zip = zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) + for root, dirs, files in os.walk(self._path): + for name in files: + zip.write(filename, os.path.join(base_dir, filename)) + zip.close() + + def _uninstall(self, install_path): + if not os.path.isdir(install_path): + raise InvalidPathException + if self._unzipped_extension is not None: + ext = os.path.splitext(install_path)[1] + if ext != self._unzipped_extension: + raise InvalidPathException + + for root, dirs, files in os.walk(install_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(install_path) diff --git a/sugar/bundle/contentbundle.py b/sugar/bundle/contentbundle.py new file mode 100644 index 0000000..9e2d36e --- /dev/null +++ b/sugar/bundle/contentbundle.py @@ -0,0 +1,190 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Sugar content bundles""" + +from ConfigParser import ConfigParser +import os + +from sugar import env +from sugar.bundle.bundle import Bundle, NotInstalledException, \ + MalformedBundleException + +class ContentBundle(Bundle): + """A Sugar content bundle + + See http://wiki.laptop.org/go/Content_bundles for details + """ + + MIME_TYPE = 'application/vnd.olpc-content' + + _zipped_extension = '.xol' + _unzipped_extension = None + _infodir = 'library' + + def __init__(self, path): + Bundle.__init__(self, path) + + info_file = self._get_file('library/library.info') + if info_file is None: + raise MalformedBundleException('No library.info file') + self._parse_info(info_file) + + if (self._get_file('index.html') is None and + self._get_file('library/library.xml') is None): + raise MalformedBundleException( + 'Content bundle %s has neither index.html nor library.xml' % + self._path) + + def _parse_info(self, info_file): + cp = ConfigParser() + cp.readfp(info_file) + + section = 'Library' + + if cp.has_option(section, 'host_version'): + version = cp.get(section, 'host_version') + try: + if int(version) != 1: + raise MalformedBundleException( + 'Content bundle %s has unknown host_version number %s' % + (self._path, version)) + except ValueError: + raise MalformedBundleException( + 'Content bundle %s has invalid host_version number %s' % + (self._path, version)) + + if cp.has_option(section, 'name'): + self._name = cp.get(section, 'name') + else: + raise MalformedBundleException( + 'Content bundle %s does not specify a name' % self._path) + + if cp.has_option(section, 'library_version'): + version = cp.get(section, 'library_version') + try: + self._library_version = int(version) + except ValueError: + raise MalformedBundleException( + 'Content bundle %s has invalid version number %s' % + (self._path, version)) + + if cp.has_option(section, 'l10n'): + l10n = cp.get(section, 'l10n') + if l10n == 'true': + self._l10n = True + elif l10n == 'false': + self._l10n = False + else: + raise MalformedBundleException( + 'Content bundle %s has invalid l10n key "%s"' % + (self._path, l10n)) + else: + raise MalformedBundleException( + 'Content bundle %s does not specify if it is localized' % + self._path) + + if cp.has_option(section, 'locale'): + self._locale = cp.get(section, 'locale') + else: + raise MalformedBundleException( + 'Content bundle %s does not specify a locale' % self._path) + + if cp.has_option(section, 'category'): + self._category = cp.get(section, 'category') + else: + raise MalformedBundleException( + 'Content bundle %s does not specify a category' % self._path) + + if cp.has_option(section, 'category_icon'): + self._category_icon = cp.get(section, 'category_icon') + else: + self._category_icon = None + + if cp.has_option(section, 'category_class'): + self._category_class = cp.get(section, 'category_class') + else: + self._category_class = None + + if cp.has_option(section, 'subcategory'): + self._subcategory = cp.get(section, 'subcategory') + else: + self._subcategory = None + + if cp.has_option(section, 'bundle_class'): + self._bundle_class = cp.get(section, 'bundle_class') + else: + self._bundle_class = None + + def get_name(self): + return self._name + + def get_library_version(self): + return self._library_version + + def get_l10n(self): + return self._l10n + + def get_locale(self): + return self._locale + + def get_category(self): + return self._category + + def get_category(self): + return self._category + + def get_category_icon(self): + return self._category_icon + + def get_category_class(self): + return self._category_class + + def get_subcategory(self): + return self._subcategory + + def get_bundle_class(self): + return self._bundle_class + + def _run_indexer(self): + os.spawnlp(os.P_WAIT, 'python', + 'python', + env.get_prefix_path('share/library-common/make_index.py')) + + def is_installed(self): + if self._unpacked: + return True + elif os.path.isdir(os.path.join(env.get_user_library_path(), + self._zip_root_dir)): + return True + else: + return False + + def install(self): + self._unzip(env.get_user_library_path()) + self._run_indexer() + + def uninstall(self): + if self._unpacked: + if not self.is_installed(): + raise NotInstalledException + install_dir = self._path + else: + install_dir = os.path.join(env.get_user_library_path(), + self._zip_root_dir) + self._uninstall(install_dir) + self._run_indexer() diff --git a/sugar/clipboard/Makefile.am b/sugar/clipboard/Makefile.am new file mode 100644 index 0000000..0d61c29 --- /dev/null +++ b/sugar/clipboard/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/sugar/clipboard +sugar_PYTHON = \ + __init__.py \ + clipboardservice.py + diff --git a/sugar/clipboard/__init__.py b/sugar/clipboard/__init__.py new file mode 100644 index 0000000..deee8dd --- /dev/null +++ b/sugar/clipboard/__init__.py @@ -0,0 +1,22 @@ +"""Client-code's interface to the ClipboardService + +Provides a simplified API for accessing the dbus service +which coordinates clipboard operations within Sugar. +""" +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + diff --git a/sugar/clipboard/clipboardservice.py b/sugar/clipboard/clipboardservice.py new file mode 100644 index 0000000..d975330 --- /dev/null +++ b/sugar/clipboard/clipboardservice.py @@ -0,0 +1,229 @@ +"""UI class to access system-level clipboard object""" +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +import logging +import dbus +import gobject + +NAME_KEY = 'NAME' +PERCENT_KEY = 'PERCENT' +ICON_KEY = 'ICON' +PREVIEW_KEY = 'PREVIEW' +ACTIVITIES_KEY = 'ACTIVITIES' +FORMATS_KEY = 'FORMATS' + +TYPE_KEY = 'TYPE' +DATA_KEY = 'DATA' +ON_DISK_KEY = 'ON_DISK' + +DBUS_SERVICE = "org.laptop.Clipboard" +DBUS_INTERFACE = "org.laptop.Clipboard" +DBUS_PATH = "/org/laptop/Clipboard" + +class ClipboardService(gobject.GObject): + """GUI interfaces for the system clipboard dbus service + + This object is used to provide convenient access to the clipboard + service (see source/services/clipboard/clipboardservice.py). It + provides utility methods for adding/getting/removing objects from + the clipboard as well as generating events when such events occur. + + Meaning is source/services/clipboard/clipboardobject.py + objects when describing "objects" on the clipboard. + """ + __gsignals__ = { + 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str, int, str, str, object])), + } + + def __init__(self): + """Initialise the ClipboardService instance + + If the service is not yet active in the background uses + a signal watcher to connect when the service appears. + """ + gobject.GObject.__init__(self) + + self._dbus_service = None + + bus = dbus.SessionBus() + self._nameOwnerChangedHandler = bus.add_signal_receiver( + self._name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus", + arg0=DBUS_SERVICE) + + self._connected = False + # Try to register to ClipboardService, if we fail, we'll try later. + try: + self._connect_clipboard_signals() + except dbus.DBusException, exception: + logging.debug(exception) + + def _connect_clipboard_signals(self): + """Connect dbus signals to our GObject signal generating callbacks""" + bus = dbus.SessionBus() + if not self._connected: + # NOTE: We need to follow_name_owner_changes here + # because we can not connect to a signal unless + # we follow the changes or we start the service + # before we connect. Starting the service here + # causes a major bottleneck during startup + proxy_obj = bus.get_object(DBUS_SERVICE, + DBUS_PATH, + follow_name_owner_changes=True) + self._dbus_service = dbus.Interface(proxy_obj, DBUS_SERVICE) + self._dbus_service.connect_to_signal('object_added', + self._object_added_cb) + self._dbus_service.connect_to_signal('object_deleted', + self._object_deleted_cb) + self._dbus_service.connect_to_signal('object_state_changed', + self._object_state_changed_cb) + self._connected = True + + bus.remove_signal_receiver(self._nameOwnerChangedHandler) + + def _name_owner_changed_cb(self, name, old, new): + """On backend service creation, connect to the server""" + if not old and new: + # ClipboardService started up + self._connect_clipboard_signals() + + def _object_added_cb(self, object_id, name): + """Emit an object-added GObject event when dbus event arrives""" + self.emit('object-added', str(object_id), name) + + def _object_deleted_cb(self, object_id): + """Emit an object-deleted GObject event when dbus event arrives""" + self.emit('object-deleted', str(object_id)) + + def _object_state_changed_cb(self, object_id, values): + """Emit an object-state-changed GObject event when dbus event arrives + + GObject event has: + + object_id + name + percent + icon + preview + activities + + From the ClipboardObject instance which is being described. + """ + self.emit('object-state-changed', str(object_id), values[NAME_KEY], + values[PERCENT_KEY], values[ICON_KEY], values[PREVIEW_KEY], + values[ACTIVITIES_KEY]) + + def add_object(self, name): + """Add a new object to the path + + returns dbus path-name for the new object's cliboard service, + this is used for all future references to the cliboard object. + + Note: + That service is actually provided by the clipboard + service object, not the ClipboardObject + """ + return str(self._dbus_service.add_object(name)) + + def add_object_format(self, object_id, formatType, data, on_disk): + """Annotate given object on the clipboard with new information + + object_id -- dbus path as returned from add_object + formatType -- XXX what should this be? mime type? + data -- storage format for the clipped object? + on_disk -- whether the data is on-disk (non-volatile) or in + memory (volatile) + + Last three arguments are just passed directly to the + clipboardobject.Format instance on the server side. + + returns None + """ + self._dbus_service.add_object_format(dbus.ObjectPath(object_id), + formatType, + data, + on_disk) + + def delete_object(self, object_id): + """Remove the given object from the clipboard + + object_id -- dbus path as returned from add_object + """ + self._dbus_service.delete_object(dbus.ObjectPath(object_id)) + + def set_object_percent(self, object_id, percent): + """Set the "percentage" for the given clipboard object + + object_id -- dbus path as returned from add_object + percentage -- numeric value from 0 to 100 inclusive + + Object percentages which are set to 100% trigger "file-completed" + operations, see the backend ClipboardService's + _handle_file_completed method for details. + + returns None + """ + self._dbus_service.set_object_percent(dbus.ObjectPath(object_id), percent) + + def get_object(self, object_id): + """Retrieve the clipboard object structure for given object + + object_id -- dbus path as returned from add_object + + Retrieves the metadata description of a given object, but + *not* the data for the object. Use get_object_data passing + one of the values in the FORMATS_KEY value in order to + retrieve the data. + + returns dictionary with + NAME_KEY: str, + PERCENT_KEY: number, + ICON_KEY: str, + PREVIEW_KEY: XXX what is it?, + ACTIVITIES_KEY: activities that can open this object, + FORMATS_KEY: list of XXX what is it? + """ + return self._dbus_service.get_object(dbus.ObjectPath(object_id),) + + def get_object_data(self, object_id, formatType): + """Retrieve object's data in the given formatType + + object_id -- dbus path as returned from add_object + formatType -- format specifier XXX of what description + + returns dictionary with + TYPE_KEY: str, + DATA_KEY: str, + ON_DISK_KEY: bool + """ + return self._dbus_service.get_object_data(dbus.ObjectPath(object_id), + formatType, + byte_arrays=True) + +_clipboard_service = None +def get_instance(): + """Retrieve this process's interface to the clipboard service""" + global _clipboard_service + if not _clipboard_service: + _clipboard_service = ClipboardService() + return _clipboard_service diff --git a/sugar/datastore/Makefile.am b/sugar/datastore/Makefile.am new file mode 100644 index 0000000..a5f16b7 --- /dev/null +++ b/sugar/datastore/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/sugar/datastore +sugar_PYTHON = \ + __init__.py \ + dbus_helpers.py \ + datastore.py diff --git a/sugar/datastore/__init__.py b/sugar/datastore/__init__.py new file mode 100644 index 0000000..bdb658b --- /dev/null +++ b/sugar/datastore/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. diff --git a/sugar/datastore/datastore.py b/sugar/datastore/datastore.py new file mode 100644 index 0000000..334c866 --- /dev/null +++ b/sugar/datastore/datastore.py @@ -0,0 +1,323 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging +import time +from datetime import datetime +import os + +import gobject + +from sugar.datastore import dbus_helpers +from sugar import activity +from sugar.activity.activityhandle import ActivityHandle +from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.contentbundle import ContentBundle +from sugar import mime + +class DSMetadata(gobject.GObject): + __gsignals__ = { + 'updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])) + } + + def __init__(self, props=None): + gobject.GObject.__init__(self) + if not props: + self._props = {} + else: + self._props = props + + default_keys = ['activity', 'activity_id', + 'mime_type', 'title_set_by_user'] + for key in default_keys: + if not self._props.has_key(key): + self._props[key] = '' + + def __getitem__(self, key): + return self._props[key] + + def __setitem__(self, key, value): + if not self._props.has_key(key) or self._props[key] != value: + self._props[key] = value + self.emit('updated') + + def __delitem__(self, key): + del self._props[key] + + def __contains__(self, key): + return self._props.__contains__(key) + + def has_key(self, key): + return self._props.has_key(key) + + def keys(self): + return self._props.keys() + + def get_dictionary(self): + return self._props + + def copy(self): + return DSMetadata(self._props.copy()) + + def get(self, key, default=None): + if self._props.has_key(key): + return self._props[key] + else: + return default + +class DSObject(object): + def __init__(self, object_id, metadata=None, file_path=None): + self.object_id = object_id + self._metadata = metadata + self._file_path = file_path + self._destroyed = False + self._owns_file = False + + def get_metadata(self): + if self._metadata is None and not self.object_id is None: + metadata = DSMetadata(dbus_helpers.get_properties(self.object_id)) + self._metadata = metadata + return self._metadata + + def set_metadata(self, metadata): + if self._metadata != metadata: + self._metadata = metadata + + metadata = property(get_metadata, set_metadata) + + def get_file_path(self): + if self._file_path is None and not self.object_id is None: + self.set_file_path(dbus_helpers.get_filename(self.object_id)) + self._owns_file = True + return self._file_path + + def set_file_path(self, file_path): + if self._file_path != file_path: + if self._file_path and self._owns_file: + if os.path.isfile(self._file_path): + os.remove(self._file_path) + self._owns_file = False + self._file_path = file_path + + file_path = property(get_file_path, set_file_path) + + def _get_activities_for_mime(self, mime_type): + registry = activity.get_registry() + result = registry.get_activities_for_type(mime_type) + if not result: + for parent_mime in mime.get_mime_parents(mime_type): + result.extend(registry.get_activities_for_type(parent_mime)) + return result + + def get_activities(self): + activities = [] + + bundle_id = self.metadata.get('activity', '') + if bundle_id: + activity_info = activity.get_registry().get_activity(bundle_id) + if activity_info: + activities.append(activity_info) + + mime_type = self.metadata.get('mime_type', '') + if mime_type: + activities_info = self._get_activities_for_mime(mime_type) + for activity_info in activities_info: + if activity_info.bundle_id != bundle_id: + activities.append(activity_info) + + return activities + + def is_activity_bundle(self): + return self.metadata['mime_type'] in \ + [ActivityBundle.MIME_TYPE, ActivityBundle.DEPRECATED_MIME_TYPE] + + def is_content_bundle(self): + return self.metadata['mime_type'] == ContentBundle.MIME_TYPE + + def is_bundle(self): + return self.is_activity_bundle() or self.is_content_bundle() + + def resume(self, bundle_id=None): + from sugar.activity import activityfactory + + if self.is_activity_bundle(): + if bundle_id is not None: + raise ValueError('Object is a bundle, cannot be resumed as an activity.') + + logging.debug('Creating activity bundle') + bundle = ActivityBundle(self.file_path) + if not bundle.is_installed(): + logging.debug('Installing activity bundle') + bundle.install() + elif bundle.need_upgrade(): + logging.debug('Upgrading activity bundle') + bundle.upgrade() + + logging.debug('activityfactory.creating bundle with id %r', bundle.get_bundle_id()) + activityfactory.create(bundle.get_bundle_id()) + else: + if not self.get_activities() and bundle_id is None: + logging.warning('No activity can open this object, %s.' % + self.metadata.get('mime_type', None)) + return + if bundle_id is None: + bundle_id = self.get_activities()[0].bundle_id + + activity_id = self.metadata['activity_id'] + object_id = self.object_id + + if activity_id: + handle = ActivityHandle(object_id=object_id, + activity_id=activity_id) + activityfactory.create(bundle_id, handle) + else: + activityfactory.create_with_object_id(bundle_id, object_id) + + def destroy(self): + if self._destroyed: + logging.warning('This DSObject has already been destroyed!.') + import traceback;traceback.print_stack() + return + self._destroyed = True + if self._file_path and self._owns_file: + if os.path.isfile(self._file_path): + os.remove(self._file_path) + self._owns_file = False + self._file_path = None + + def __del__(self): + if not self._destroyed: + logging.warning('DSObject was deleted without cleaning up first. ' \ + 'Please call DSObject.destroy() before disposing it.') + self.destroy() + + def copy(self): + return DSObject(None, self._metadata.copy(), self._file_path) + +def get(object_id): + logging.debug('datastore.get') + metadata = dbus_helpers.get_properties(object_id) + + ds_object = DSObject(object_id, DSMetadata(metadata), None) + # TODO: register the object for updates + return ds_object + +def create(): + metadata = DSMetadata() + metadata['mtime'] = datetime.now().isoformat() + metadata['timestamp'] = int(time.time()) + return DSObject(object_id=None, metadata=metadata, file_path=None) + +def write(ds_object, update_mtime=True, transfer_ownership=False, reply_handler=None, error_handler=None, timeout=-1): + logging.debug('datastore.write') + + properties = ds_object.metadata.get_dictionary().copy() + + if update_mtime: + properties['mtime'] = datetime.now().isoformat() + properties['timestamp'] = int(time.time()) + + if ds_object._file_path is None: + file_path = '' + else: + file_path = ds_object._file_path + + # FIXME: this func will be sync for creates regardless of the handlers + # supplied. This is very bad API, need to decide what to do here. + if ds_object.object_id: + dbus_helpers.update(ds_object.object_id, + properties, + file_path, + transfer_ownership, + reply_handler=reply_handler, + error_handler=error_handler, + timeout=timeout) + else: + if reply_handler or error_handler: + logging.warning('datastore.write() cannot currently be called async' \ + ' for creates, see https://dev.laptop.org/ticket/3071') + ds_object.object_id = dbus_helpers.create(properties, + file_path, + transfer_ownership) + # TODO: register the object for updates + logging.debug('Written object %s to the datastore.' % ds_object.object_id) + +def delete(object_id): + logging.debug('datastore.delete') + dbus_helpers.delete(object_id) + +def find(query, sorting=None, limit=None, offset=None, properties=[], + reply_handler=None, error_handler=None): + + query = query.copy() + + if sorting: + query['order_by'] = sorting + if limit: + query['limit'] = limit + if offset: + query['offset'] = offset + + props_list, total_count = dbus_helpers.find(query, properties, reply_handler, error_handler) + + objects = [] + for props in props_list: + object_id = props['uid'] + del props['uid'] + + ds_object = DSObject(object_id, DSMetadata(props), None) + objects.append(ds_object) + + return objects, total_count + +def copy(jobject, mount_point): + + new_jobject = jobject.copy() + new_jobject.metadata['mountpoint'] = mount_point + + if jobject.metadata.has_key('title'): + filename = jobject.metadata['title'] + + if jobject.metadata.has_key('mime_type'): + mime_type = jobject.metadata['mime_type'] + extension = mime.get_primary_extension(mime_type) + if extension: + filename += '.' + extension + + new_jobject.metadata['suggested_filename'] = filename + + # this will cause the file be retrieved from the DS + new_jobject.file_path = jobject.file_path + + write(new_jobject) + +def mount(uri, options, timeout=-1): + return dbus_helpers.mount(uri, options, timeout=timeout) + +def unmount(mount_point_id): + dbus_helpers.unmount(mount_point_id) + +def mounts(): + return dbus_helpers.mounts() + +def complete_indexing(): + return dbus_helpers.complete_indexing() + +def get_unique_values(key): + return dbus_helpers.get_unique_values(key) diff --git a/sugar/datastore/dbus_helpers.py b/sugar/datastore/dbus_helpers.py new file mode 100644 index 0000000..a5ce9c8 --- /dev/null +++ b/sugar/datastore/dbus_helpers.py @@ -0,0 +1,99 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import dbus.glib +import gobject + +from sugar import util + +DS_DBUS_SERVICE = "org.laptop.sugar.DataStore" +DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore" +DS_DBUS_PATH = "/org/laptop/sugar/DataStore" + +_data_store = None + +def _get_data_store(): + global _data_store + + if not _data_store: + _bus = dbus.SessionBus() + _data_store = dbus.Interface(_bus.get_object(DS_DBUS_SERVICE, + DS_DBUS_PATH), + DS_DBUS_INTERFACE) + return _data_store + +def create(properties, filename, transfer_ownership=False): + object_id = _get_data_store().create(dbus.Dictionary(properties), filename, + transfer_ownership) + logging.debug('dbus_helpers.create: ' + object_id) + return object_id + +def update(uid, properties, filename, transfer_ownership=False, + reply_handler=None, error_handler=None, timeout=-1): + debug_props = properties.copy() + if debug_props.has_key("preview"): + debug_props["preview"] = "" + logging.debug('dbus_helpers.update: %s, %s, %s, %s' % (uid, filename, debug_props, transfer_ownership)) + if reply_handler and error_handler: + _get_data_store().update(uid, dbus.Dictionary(properties), filename, + transfer_ownership, + reply_handler=reply_handler, + error_handler=error_handler, + timeout=timeout) + else: + _get_data_store().update(uid, dbus.Dictionary(properties), filename, transfer_ownership) + +def delete(uid): + logging.debug('dbus_helpers.delete: %r' % uid) + _get_data_store().delete(uid) + +def get_properties(uid): + logging.debug('dbus_helpers.get_properties: %s' % uid) + return _get_data_store().get_properties(uid, byte_arrays=True) + +def get_filename(uid): + filename = _get_data_store().get_filename(uid) + logging.debug('dbus_helpers.get_filename: %s, %s' % (uid, filename)) + return filename + +def find(query, properties, reply_handler, error_handler): + logging.debug('dbus_helpers.find: %r %r' % (query, properties)) + if reply_handler and error_handler: + return _get_data_store().find(query, properties, + reply_handler=reply_handler, error_handler=error_handler) + else: + return _get_data_store().find(query, properties) + +def mount(uri, options, timeout=-1): + return _get_data_store().mount(uri, options, timeout=timeout) + +def unmount(mount_point_id): + _get_data_store().unmount(mount_point_id) + +def mounts(): + return _get_data_store().mounts() + +def get_unique_values(key): + return _get_data_store().get_uniquevaluesfor(key, dbus.Dictionary({}, signature='ss')) + +def complete_indexing(): + return _get_data_store().complete_indexing() + diff --git a/sugar/eggaccelerators.c b/sugar/eggaccelerators.c new file mode 100644 index 0000000..0a39d51 --- /dev/null +++ b/sugar/eggaccelerators.c @@ -0,0 +1,702 @@ +/* eggaccelerators.c + * Copyright (C) 2002 Red Hat, Inc.; Copyright 1998, 2001 Tim Janik + * Developed by Havoc Pennington, Tim Janik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library 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. + */ + +#include "eggaccelerators.h" + +#include +#include +#include +#include + +enum +{ + EGG_MODMAP_ENTRY_SHIFT = 0, + EGG_MODMAP_ENTRY_LOCK = 1, + EGG_MODMAP_ENTRY_CONTROL = 2, + EGG_MODMAP_ENTRY_MOD1 = 3, + EGG_MODMAP_ENTRY_MOD2 = 4, + EGG_MODMAP_ENTRY_MOD3 = 5, + EGG_MODMAP_ENTRY_MOD4 = 6, + EGG_MODMAP_ENTRY_MOD5 = 7, + EGG_MODMAP_ENTRY_LAST = 8 +}; + +#define MODMAP_ENTRY_TO_MODIFIER(x) (1 << (x)) + +typedef struct +{ + EggVirtualModifierType mapping[EGG_MODMAP_ENTRY_LAST]; + +} EggModmap; + +const EggModmap* egg_keymap_get_modmap (GdkKeymap *keymap); + +static inline gboolean +is_alt (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'a' || string[1] == 'A') && + (string[2] == 'l' || string[2] == 'L') && + (string[3] == 't' || string[3] == 'T') && + (string[4] == '>')); +} + +static inline gboolean +is_ctl (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'c' || string[1] == 'C') && + (string[2] == 't' || string[2] == 'T') && + (string[3] == 'l' || string[3] == 'L') && + (string[4] == '>')); +} + +static inline gboolean +is_modx (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'm' || string[1] == 'M') && + (string[2] == 'o' || string[2] == 'O') && + (string[3] == 'd' || string[3] == 'D') && + (string[4] >= '1' && string[4] <= '5') && + (string[5] == '>')); +} + +static inline gboolean +is_ctrl (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'c' || string[1] == 'C') && + (string[2] == 't' || string[2] == 'T') && + (string[3] == 'r' || string[3] == 'R') && + (string[4] == 'l' || string[4] == 'L') && + (string[5] == '>')); +} + +static inline gboolean +is_shft (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 's' || string[1] == 'S') && + (string[2] == 'h' || string[2] == 'H') && + (string[3] == 'f' || string[3] == 'F') && + (string[4] == 't' || string[4] == 'T') && + (string[5] == '>')); +} + +static inline gboolean +is_shift (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 's' || string[1] == 'S') && + (string[2] == 'h' || string[2] == 'H') && + (string[3] == 'i' || string[3] == 'I') && + (string[4] == 'f' || string[4] == 'F') && + (string[5] == 't' || string[5] == 'T') && + (string[6] == '>')); +} + +static inline gboolean +is_control (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'c' || string[1] == 'C') && + (string[2] == 'o' || string[2] == 'O') && + (string[3] == 'n' || string[3] == 'N') && + (string[4] == 't' || string[4] == 'T') && + (string[5] == 'r' || string[5] == 'R') && + (string[6] == 'o' || string[6] == 'O') && + (string[7] == 'l' || string[7] == 'L') && + (string[8] == '>')); +} + +static inline gboolean +is_release (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'r' || string[1] == 'R') && + (string[2] == 'e' || string[2] == 'E') && + (string[3] == 'l' || string[3] == 'L') && + (string[4] == 'e' || string[4] == 'E') && + (string[5] == 'a' || string[5] == 'A') && + (string[6] == 's' || string[6] == 'S') && + (string[7] == 'e' || string[7] == 'E') && + (string[8] == '>')); +} + +static inline gboolean +is_meta (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'm' || string[1] == 'M') && + (string[2] == 'e' || string[2] == 'E') && + (string[3] == 't' || string[3] == 'T') && + (string[4] == 'a' || string[4] == 'A') && + (string[5] == '>')); +} + +static inline gboolean +is_super (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 's' || string[1] == 'S') && + (string[2] == 'u' || string[2] == 'U') && + (string[3] == 'p' || string[3] == 'P') && + (string[4] == 'e' || string[4] == 'E') && + (string[5] == 'r' || string[5] == 'R') && + (string[6] == '>')); +} + +static inline gboolean +is_hyper (const gchar *string) +{ + return ((string[0] == '<') && + (string[1] == 'h' || string[1] == 'H') && + (string[2] == 'y' || string[2] == 'Y') && + (string[3] == 'p' || string[3] == 'P') && + (string[4] == 'e' || string[4] == 'E') && + (string[5] == 'r' || string[5] == 'R') && + (string[6] == '>')); +} + +static inline gboolean +is_keycode (const gchar *string) +{ + return ((string[0] == '0') && + (string[1] == 'x')); +} + +/** + * egg_accelerator_parse_virtual: + * @accelerator: string representing an accelerator + * @accelerator_key: return location for accelerator keyval + * @accelerator_mods: return location for accelerator modifier mask + * + * Parses a string representing a virtual accelerator. The format + * looks like "<Control>a" or "<Shift><Alt>F1" or + * "<Release>z" (the last one is for key release). The parser + * is fairly liberal and allows lower or upper case, and also + * abbreviations such as "<Ctl>" and "<Ctrl>". + * + * If the parse fails, @accelerator_key and @accelerator_mods will + * be set to 0 (zero) and %FALSE will be returned. If the string contains + * only modifiers, @accelerator_key will be set to 0 but %TRUE will be + * returned. + * + * The virtual vs. concrete accelerator distinction is a relic of + * how the X Window System works; there are modifiers Mod2-Mod5 that + * can represent various keyboard keys (numlock, meta, hyper, etc.), + * the virtual modifier represents the keyboard key, the concrete + * modifier the actual Mod2-Mod5 bits in the key press event. + * + * Returns: %TRUE on success. + */ +gboolean +egg_accelerator_parse_virtual (const gchar *accelerator, + guint *accelerator_key, + guint *keycode, + EggVirtualModifierType *accelerator_mods) +{ + guint keyval; + GdkModifierType mods; + gint len; + gboolean bad_keyval; + + if (accelerator_key) + *accelerator_key = 0; + if (accelerator_mods) + *accelerator_mods = 0; + if (keycode) + *keycode = 0; + + g_return_val_if_fail (accelerator != NULL, FALSE); + + bad_keyval = FALSE; + + keyval = 0; + mods = 0; + len = strlen (accelerator); + while (len) + { + if (*accelerator == '<') + { + if (len >= 9 && is_release (accelerator)) + { + accelerator += 9; + len -= 9; + mods |= EGG_VIRTUAL_RELEASE_MASK; + } + else if (len >= 9 && is_control (accelerator)) + { + accelerator += 9; + len -= 9; + mods |= EGG_VIRTUAL_CONTROL_MASK; + } + else if (len >= 7 && is_shift (accelerator)) + { + accelerator += 7; + len -= 7; + mods |= EGG_VIRTUAL_SHIFT_MASK; + } + else if (len >= 6 && is_shft (accelerator)) + { + accelerator += 6; + len -= 6; + mods |= EGG_VIRTUAL_SHIFT_MASK; + } + else if (len >= 6 && is_ctrl (accelerator)) + { + accelerator += 6; + len -= 6; + mods |= EGG_VIRTUAL_CONTROL_MASK; + } + else if (len >= 6 && is_modx (accelerator)) + { + static const guint mod_vals[] = { + EGG_VIRTUAL_ALT_MASK, EGG_VIRTUAL_MOD2_MASK, EGG_VIRTUAL_MOD3_MASK, + EGG_VIRTUAL_MOD4_MASK, EGG_VIRTUAL_MOD5_MASK + }; + + len -= 6; + accelerator += 4; + mods |= mod_vals[*accelerator - '1']; + accelerator += 2; + } + else if (len >= 5 && is_ctl (accelerator)) + { + accelerator += 5; + len -= 5; + mods |= EGG_VIRTUAL_CONTROL_MASK; + } + else if (len >= 5 && is_alt (accelerator)) + { + accelerator += 5; + len -= 5; + mods |= EGG_VIRTUAL_ALT_MASK; + } + else if (len >= 6 && is_meta (accelerator)) + { + accelerator += 6; + len -= 6; + mods |= EGG_VIRTUAL_META_MASK; + } + else if (len >= 7 && is_hyper (accelerator)) + { + accelerator += 7; + len -= 7; + mods |= EGG_VIRTUAL_HYPER_MASK; + } + else if (len >= 7 && is_super (accelerator)) + { + accelerator += 7; + len -= 7; + mods |= EGG_VIRTUAL_SUPER_MASK; + } + else + { + gchar last_ch; + + last_ch = *accelerator; + while (last_ch && last_ch != '>') + { + last_ch = *accelerator; + accelerator += 1; + len -= 1; + } + } + } + else + { + keyval = gdk_keyval_from_name (accelerator); + + if (keyval == 0) + { + /* If keyval is 0, than maybe it's a keycode. Check for 0x## */ + if (len >= 4 && is_keycode (accelerator)) + { + char keystring[5]; + gchar *endptr; + gint tmp_keycode; + + memcpy (keystring, accelerator, 4); + keystring [4] = '\000'; + + tmp_keycode = strtol (keystring, &endptr, 16); + + if (endptr == NULL || *endptr != '\000') + { + bad_keyval = TRUE; + } + else if (keycode != NULL) + { + *keycode = tmp_keycode; + /* 0x00 is an invalid keycode too. */ + if (*keycode == 0) + bad_keyval = TRUE; + } + } + } else if (keycode != NULL) + *keycode = XKeysymToKeycode (GDK_DISPLAY(), keyval); + + accelerator += len; + len -= len; + } + } + + if (accelerator_key) + *accelerator_key = gdk_keyval_to_lower (keyval); + if (accelerator_mods) + *accelerator_mods = mods; + + return !bad_keyval; +} + + +/** + * egg_virtual_accelerator_name: + * @accelerator_key: accelerator keyval + * @accelerator_mods: accelerator modifier mask + * @returns: a newly-allocated accelerator name + * + * Converts an accelerator keyval and modifier mask + * into a string parseable by egg_accelerator_parse_virtual(). + * For example, if you pass in #GDK_q and #EGG_VIRTUAL_CONTROL_MASK, + * this function returns "<Control>q". + * + * The caller of this function must free the returned string. + */ +gchar* +egg_virtual_accelerator_name (guint accelerator_key, + guint keycode, + EggVirtualModifierType accelerator_mods) +{ + static const gchar text_release[] = ""; + static const gchar text_shift[] = ""; + static const gchar text_control[] = ""; + static const gchar text_mod1[] = ""; + static const gchar text_mod2[] = ""; + static const gchar text_mod3[] = ""; + static const gchar text_mod4[] = ""; + static const gchar text_mod5[] = ""; + static const gchar text_meta[] = ""; + static const gchar text_super[] = ""; + static const gchar text_hyper[] = ""; + guint l; + gchar *keyval_name; + gchar *accelerator; + + accelerator_mods &= EGG_VIRTUAL_MODIFIER_MASK; + + if (!accelerator_key) + { + keyval_name = g_strdup_printf ("0x%02x", keycode); + } + else + { + keyval_name = gdk_keyval_name (gdk_keyval_to_lower (accelerator_key)); + if (!keyval_name) + keyval_name = ""; + } + + l = 0; + if (accelerator_mods & EGG_VIRTUAL_RELEASE_MASK) + l += sizeof (text_release) - 1; + if (accelerator_mods & EGG_VIRTUAL_SHIFT_MASK) + l += sizeof (text_shift) - 1; + if (accelerator_mods & EGG_VIRTUAL_CONTROL_MASK) + l += sizeof (text_control) - 1; + if (accelerator_mods & EGG_VIRTUAL_ALT_MASK) + l += sizeof (text_mod1) - 1; + if (accelerator_mods & EGG_VIRTUAL_MOD2_MASK) + l += sizeof (text_mod2) - 1; + if (accelerator_mods & EGG_VIRTUAL_MOD3_MASK) + l += sizeof (text_mod3) - 1; + if (accelerator_mods & EGG_VIRTUAL_MOD4_MASK) + l += sizeof (text_mod4) - 1; + if (accelerator_mods & EGG_VIRTUAL_MOD5_MASK) + l += sizeof (text_mod5) - 1; + if (accelerator_mods & EGG_VIRTUAL_META_MASK) + l += sizeof (text_meta) - 1; + if (accelerator_mods & EGG_VIRTUAL_HYPER_MASK) + l += sizeof (text_hyper) - 1; + if (accelerator_mods & EGG_VIRTUAL_SUPER_MASK) + l += sizeof (text_super) - 1; + l += strlen (keyval_name); + + accelerator = g_new (gchar, l + 1); + + l = 0; + accelerator[l] = 0; + if (accelerator_mods & EGG_VIRTUAL_RELEASE_MASK) + { + strcpy (accelerator + l, text_release); + l += sizeof (text_release) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_SHIFT_MASK) + { + strcpy (accelerator + l, text_shift); + l += sizeof (text_shift) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_CONTROL_MASK) + { + strcpy (accelerator + l, text_control); + l += sizeof (text_control) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_ALT_MASK) + { + strcpy (accelerator + l, text_mod1); + l += sizeof (text_mod1) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_MOD2_MASK) + { + strcpy (accelerator + l, text_mod2); + l += sizeof (text_mod2) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_MOD3_MASK) + { + strcpy (accelerator + l, text_mod3); + l += sizeof (text_mod3) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_MOD4_MASK) + { + strcpy (accelerator + l, text_mod4); + l += sizeof (text_mod4) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_MOD5_MASK) + { + strcpy (accelerator + l, text_mod5); + l += sizeof (text_mod5) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_META_MASK) + { + strcpy (accelerator + l, text_meta); + l += sizeof (text_meta) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_HYPER_MASK) + { + strcpy (accelerator + l, text_hyper); + l += sizeof (text_hyper) - 1; + } + if (accelerator_mods & EGG_VIRTUAL_SUPER_MASK) + { + strcpy (accelerator + l, text_super); + l += sizeof (text_super) - 1; + } + + strcpy (accelerator + l, keyval_name); + + return accelerator; +} + +void +egg_keymap_resolve_virtual_modifiers (GdkKeymap *keymap, + EggVirtualModifierType virtual_mods, + GdkModifierType *concrete_mods) +{ + GdkModifierType concrete; + int i; + const EggModmap *modmap; + + g_return_if_fail (GDK_IS_KEYMAP (keymap)); + g_return_if_fail (concrete_mods != NULL); + + modmap = egg_keymap_get_modmap (keymap); + + /* Not so sure about this algorithm. */ + + concrete = 0; + i = 0; + while (i < EGG_MODMAP_ENTRY_LAST) + { + if (modmap->mapping[i] & virtual_mods) + concrete |= (1 << i); + + ++i; + } + + *concrete_mods = concrete; +} + +void +egg_keymap_virtualize_modifiers (GdkKeymap *keymap, + GdkModifierType concrete_mods, + EggVirtualModifierType *virtual_mods) +{ + GdkModifierType virtual; + int i; + const EggModmap *modmap; + + g_return_if_fail (GDK_IS_KEYMAP (keymap)); + g_return_if_fail (virtual_mods != NULL); + + modmap = egg_keymap_get_modmap (keymap); + + /* Not so sure about this algorithm. */ + + virtual = 0; + i = 0; + while (i < EGG_MODMAP_ENTRY_LAST) + { + if ((1 << i) & concrete_mods) + { + EggVirtualModifierType cleaned; + + cleaned = modmap->mapping[i] & ~(EGG_VIRTUAL_MOD2_MASK | + EGG_VIRTUAL_MOD3_MASK | + EGG_VIRTUAL_MOD4_MASK | + EGG_VIRTUAL_MOD5_MASK); + + if (cleaned != 0) + { + virtual |= cleaned; + } + else + { + /* Rather than dropping mod2->mod5 if not bound, + * go ahead and use the concrete names + */ + virtual |= modmap->mapping[i]; + } + } + + ++i; + } + + *virtual_mods = virtual; +} + +static void +reload_modmap (GdkKeymap *keymap, + EggModmap *modmap) +{ + XModifierKeymap *xmodmap; + int map_size; + int i; + + /* FIXME multihead */ + xmodmap = XGetModifierMapping (gdk_x11_get_default_xdisplay ()); + + memset (modmap->mapping, 0, sizeof (modmap->mapping)); + + /* there are 8 modifiers, and the first 3 are shift, shift lock, + * and control + */ + map_size = 8 * xmodmap->max_keypermod; + i = 3 * xmodmap->max_keypermod; + while (i < map_size) + { + /* get the key code at this point in the map, + * see if its keysym is one we're interested in + */ + int keycode = xmodmap->modifiermap[i]; + GdkKeymapKey *keys; + guint *keyvals; + int n_entries; + int j; + EggVirtualModifierType mask; + + keys = NULL; + keyvals = NULL; + n_entries = 0; + + gdk_keymap_get_entries_for_keycode (keymap, + keycode, + &keys, &keyvals, &n_entries); + + mask = 0; + j = 0; + while (j < n_entries) + { + if (keyvals[j] == GDK_Num_Lock) + mask |= EGG_VIRTUAL_NUM_LOCK_MASK; + else if (keyvals[j] == GDK_Scroll_Lock) + mask |= EGG_VIRTUAL_SCROLL_LOCK_MASK; + else if (keyvals[j] == GDK_Meta_L || + keyvals[j] == GDK_Meta_R) + mask |= EGG_VIRTUAL_META_MASK; + else if (keyvals[j] == GDK_Hyper_L || + keyvals[j] == GDK_Hyper_R) + mask |= EGG_VIRTUAL_HYPER_MASK; + else if (keyvals[j] == GDK_Super_L || + keyvals[j] == GDK_Super_R) + mask |= EGG_VIRTUAL_SUPER_MASK; + else if (keyvals[j] == GDK_Mode_switch) + mask |= EGG_VIRTUAL_MODE_SWITCH_MASK; + + ++j; + } + + /* Mod1Mask is 1 << 3 for example, i.e. the + * fourth modifier, i / keyspermod is the modifier + * index + */ + modmap->mapping[i/xmodmap->max_keypermod] |= mask; + + g_free (keyvals); + g_free (keys); + + ++i; + } + + /* Add in the not-really-virtual fixed entries */ + modmap->mapping[EGG_MODMAP_ENTRY_SHIFT] |= EGG_VIRTUAL_SHIFT_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_CONTROL] |= EGG_VIRTUAL_CONTROL_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_LOCK] |= EGG_VIRTUAL_LOCK_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_MOD1] |= EGG_VIRTUAL_ALT_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_MOD2] |= EGG_VIRTUAL_MOD2_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_MOD3] |= EGG_VIRTUAL_MOD3_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_MOD4] |= EGG_VIRTUAL_MOD4_MASK; + modmap->mapping[EGG_MODMAP_ENTRY_MOD5] |= EGG_VIRTUAL_MOD5_MASK; + + XFreeModifiermap (xmodmap); +} + +const EggModmap* +egg_keymap_get_modmap (GdkKeymap *keymap) +{ + EggModmap *modmap; + + /* This is all a hack, much simpler when we can just + * modify GDK directly. + */ + + modmap = g_object_get_data (G_OBJECT (keymap), + "egg-modmap"); + + if (modmap == NULL) + { + modmap = g_new0 (EggModmap, 1); + + /* FIXME modify keymap change events with an event filter + * and force a reload if we get one + */ + + reload_modmap (keymap, modmap); + + g_object_set_data_full (G_OBJECT (keymap), + "egg-modmap", + modmap, + g_free); + } + + g_assert (modmap != NULL); + + return modmap; +} diff --git a/sugar/eggaccelerators.h b/sugar/eggaccelerators.h new file mode 100644 index 0000000..d2276d2 --- /dev/null +++ b/sugar/eggaccelerators.h @@ -0,0 +1,89 @@ +/* eggaccelerators.h + * Copyright (C) 2002 Red Hat, Inc. + * Developed by Havoc Pennington + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library 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. + */ + +#ifndef __EGG_ACCELERATORS_H__ +#define __EGG_ACCELERATORS_H__ + +#include +#include + +G_BEGIN_DECLS + +/* Where a value is also in GdkModifierType we coincide, + * otherwise we don't overlap. + */ +typedef enum +{ + EGG_VIRTUAL_SHIFT_MASK = 1 << 0, + EGG_VIRTUAL_LOCK_MASK = 1 << 1, + EGG_VIRTUAL_CONTROL_MASK = 1 << 2, + + EGG_VIRTUAL_ALT_MASK = 1 << 3, /* fixed as Mod1 */ + + EGG_VIRTUAL_MOD2_MASK = 1 << 4, + EGG_VIRTUAL_MOD3_MASK = 1 << 5, + EGG_VIRTUAL_MOD4_MASK = 1 << 6, + EGG_VIRTUAL_MOD5_MASK = 1 << 7, + +#if 0 + GDK_BUTTON1_MASK = 1 << 8, + GDK_BUTTON2_MASK = 1 << 9, + GDK_BUTTON3_MASK = 1 << 10, + GDK_BUTTON4_MASK = 1 << 11, + GDK_BUTTON5_MASK = 1 << 12, + /* 13, 14 are used by Xkb for the keyboard group */ +#endif + + EGG_VIRTUAL_META_MASK = 1 << 24, + EGG_VIRTUAL_SUPER_MASK = 1 << 25, + EGG_VIRTUAL_HYPER_MASK = 1 << 26, + EGG_VIRTUAL_MODE_SWITCH_MASK = 1 << 27, + EGG_VIRTUAL_NUM_LOCK_MASK = 1 << 28, + EGG_VIRTUAL_SCROLL_LOCK_MASK = 1 << 29, + + /* Also in GdkModifierType */ + EGG_VIRTUAL_RELEASE_MASK = 1 << 30, + + /* 28-31 24-27 20-23 16-19 12-15 8-11 4-7 0-3 + * 7 f 0 0 0 0 f f + */ + EGG_VIRTUAL_MODIFIER_MASK = 0x7f0000ff + +} EggVirtualModifierType; + +gboolean egg_accelerator_parse_virtual (const gchar *accelerator, + guint *accelerator_key, + guint *keycode, + EggVirtualModifierType *accelerator_mods); +void egg_keymap_resolve_virtual_modifiers (GdkKeymap *keymap, + EggVirtualModifierType virtual_mods, + GdkModifierType *concrete_mods); +void egg_keymap_virtualize_modifiers (GdkKeymap *keymap, + GdkModifierType concrete_mods, + EggVirtualModifierType *virtual_mods); + +gchar* egg_virtual_accelerator_name (guint accelerator_key, + guint keycode, + EggVirtualModifierType accelerator_mods); + +G_END_DECLS + + +#endif /* __EGG_ACCELERATORS_H__ */ diff --git a/sugar/env.py b/sugar/env.py new file mode 100644 index 0000000..e0b6fa9 --- /dev/null +++ b/sugar/env.py @@ -0,0 +1,56 @@ +"""Calculates file-paths for the Sugar working environment""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os + +def is_emulator(): + if os.environ.has_key('SUGAR_EMULATOR'): + if os.environ['SUGAR_EMULATOR'] == 'yes': + return True + return False + +def get_profile_path(path=None): + if os.environ.has_key('SUGAR_PROFILE'): + profile_id = os.environ['SUGAR_PROFILE'] + else: + profile_id = 'default' + + base = os.path.join(os.path.expanduser('~/.sugar'), profile_id) + if not os.path.isdir(base): + try: + os.makedirs(base, 0770) + except OSError, exc: + print "Could not create user directory." + + if path != None: + return os.path.join(base, path) + else: + return base + +def get_logs_path(path=None): + base = get_profile_path('logs') + if path != None: + return os.path.join(base, path) + else: + return base + +def get_user_activities_path(): + return os.path.expanduser('~/Activities') + +def get_user_library_path(): + return os.path.expanduser('~/Library') diff --git a/sugar/graphics/Makefile.am b/sugar/graphics/Makefile.am new file mode 100644 index 0000000..0a3a846 --- /dev/null +++ b/sugar/graphics/Makefile.am @@ -0,0 +1,25 @@ +sugardir = $(pythondir)/sugar/graphics +sugar_PYTHON = \ + __init__.py \ + alert.py \ + animator.py \ + combobox.py \ + entry.py \ + icon.py \ + iconentry.py \ + menuitem.py \ + notebook.py \ + objectchooser.py \ + radiotoolbutton.py \ + palette.py \ + palettegroup.py \ + panel.py \ + roundbox.py \ + style.py \ + toggletoolbutton.py \ + toolbox.py \ + toolbutton.py \ + toolcombobox.py \ + tray.py \ + window.py \ + xocolor.py diff --git a/sugar/graphics/__init__.py b/sugar/graphics/__init__.py new file mode 100644 index 0000000..1e7e0f9 --- /dev/null +++ b/sugar/graphics/__init__.py @@ -0,0 +1,18 @@ +"""Graphics/controls for use in Sugar""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. diff --git a/sugar/graphics/alert.py b/sugar/graphics/alert.py new file mode 100644 index 0000000..ef649b2 --- /dev/null +++ b/sugar/graphics/alert.py @@ -0,0 +1,254 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from gettext import gettext as _ + +import gtk +import gobject +import hippo +import math + +from sugar.graphics import style +from sugar.graphics.icon import Icon + + +class Alert(gtk.EventBox, gobject.GObject): + """UI interface for Alerts + + Alerts are used inside the activity window instead of being a + separate popup window. They do not hide canvas content. You can + use add_alert(widget) and remove_alert(widget) inside your activity + to add and remove the alert. The position of the alert is below the + toolbox or top in fullscreen mode. + + Properties: + 'title': the title of the alert, + 'message': the message of the alert, + 'icon': the icon that appears at the far left + See __gproperties__ + """ + + __gtype_name__ = 'SugarAlert' + + __gsignals__ = { + 'response': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])) + } + + __gproperties__ = { + 'title' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'msg' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'icon' : (object, None, None, + gobject.PARAM_WRITABLE) + } + + def __init__(self, **kwargs): + gobject.GObject.__init__(self) + + self.set_visible_window(True) + self._hbox = gtk.HBox() + self._hbox.set_border_width(style.DEFAULT_SPACING) + self._hbox.set_spacing(style.DEFAULT_SPACING) + self.add(self._hbox) + + self._title = None + self._msg = None + self._icon = None + self._buttons = {} + + self._msg_box = gtk.VBox() + self._title_label = gtk.Label() + self._title_label.set_alignment(0, 0.5) + self._msg_box.pack_start(self._title_label, False) + self._title_label.show() + + self._msg_label = gtk.Label() + self._msg_label.set_alignment(0, 0.5) + self._msg_box.pack_start(self._msg_label, False) + self._hbox.pack_start(self._msg_box, False) + self._msg_label.show() + + self._buttons_box = gtk.HButtonBox() + self._buttons_box.set_layout(gtk.BUTTONBOX_END) + self._buttons_box.set_spacing(style.DEFAULT_SPACING) + self._hbox.pack_start(self._buttons_box) + self._buttons_box.show() + + self._msg_box.show() + self._hbox.show() + self.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'title': + if self._title != value: + self._title = value + self._title_label.set_markup("" + self._title + "") + elif pspec.name == 'msg': + if self._msg != value: + self._msg = value + self._msg_label.set_markup(self._msg) + elif pspec.name == 'icon': + if self._icon != value: + self._icon = value + self._hbox.pack_start(self._icon, False) + self._hbox.reorder_child(self._icon, 0) + + def do_get_property(self, pspec): + if pspec.name == 'title': + return self._title + elif pspec.name == 'msg': + return self._msg + + def add_button(self, response_id, label, icon=None, position=-1): + """Add a button to the alert + + response_id: will be emitted with the response signal + a response ID should one of the pre-defined + GTK Response Type Constants or a positive number + label: that will occure right to the buttom + icon: this can be a SugarIcon or a gtk.Image + position: the position of the button in the box (optional) + """ + button = gtk.Button() + self._buttons[response_id] = button + if icon is not None: + button.set_image(icon) + button.set_label(label) + self._buttons_box.pack_start(button) + button.show() + button.connect('clicked', self.__button_clicked_cb, response_id) + if position != -1: + self._buttons_box.reorder_child(button, position) + return button + + def remove_button(self, response_id): + """Remove a button from the alert by the given button id""" + self._buttons_box.remove(self._buttons[id]) + + def _response(self, id): + """Emitting response when we have a result + + A result can be that a user has clicked a button or + a timeout has occured, the id identifies the button + that has been clicked and -1 for a timeout + """ + self.emit('response', id) + + def __button_clicked_cb(self, button, response_id): + self._response(response_id) + + +class ConfirmationAlert(Alert): + """This is a ready-made two button (Cancel,Ok) alert""" + + def __init__(self, **kwargs): + Alert.__init__(self, **kwargs) + + icon = Icon(icon_name='dialog-cancel') + cancel_button = self.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), icon) + icon.show() + + icon = Icon(icon_name='dialog-ok') + ok_button = self.add_button(gtk.RESPONSE_OK, _('Ok'), icon) + icon.show() + + +class _TimeoutIcon(hippo.CanvasText, hippo.CanvasItem): + __gtype_name__ = 'AlertTimeoutIcon' + + def __init__(self, **kwargs): + hippo.CanvasText.__init__(self, **kwargs) + + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self.props.border_left = style.DEFAULT_SPACING + self.props.border_right = style.DEFAULT_SPACING + + def do_paint_background(self, cr, damaged_box): + [width, height] = self.get_allocation() + + x = width * 0.5 + y = height * 0.5 + radius = min(width * 0.5, height * 0.5) + + hippo.cairo_set_source_rgba32(cr, self.props.background_color) + cr.arc(x, y, radius, 0, 2*math.pi) + cr.fill_preserve() + + +class TimeoutAlert(Alert): + """This is a ready-made two button (Cancel,Continue) alert + + It times out with a positive reponse after the given amount of seconds. + """ + + def __init__(self, timeout=5, **kwargs): + Alert.__init__(self, **kwargs) + + self._timeout = timeout + + icon = Icon(icon_name='dialog-cancel') + cancel_button = self.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), icon) + icon.show() + + self._timeout_text = _TimeoutIcon( + text=self._timeout, + color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_WHITE.get_int()) + canvas = hippo.Canvas() + canvas.set_root(self._timeout_text) + canvas.show() + self.add_button(gtk.RESPONSE_OK, _('Continue'), canvas) + + gobject.timeout_add(1000, self.__timeout) + + def __timeout(self): + self._timeout -= 1 + self._timeout_text.props.text = self._timeout + if self._timeout == 0: + self._response(gtk.RESPONSE_OK) + return False + return True + + +class NotifyAlert(Alert): + """Timeout alert with only an "OK" button - just for notifications""" + + def __init__(self, timeout=5, **kwargs): + Alert.__init__(self, **kwargs) + + self._timeout = timeout + + self._timeout_text = _TimeoutIcon( + text=self._timeout, + color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_WHITE.get_int()) + canvas = hippo.Canvas() + canvas.set_root(self._timeout_text) + canvas.show() + self.add_button(gtk.RESPONSE_OK, _('OK'), canvas) + + gobject.timeout_add(1000, self.__timeout) + + def __timeout(self): + self._timeout -= 1 + self._timeout_text.props.text = self._timeout + if self._timeout == 0: + self._response(gtk.RESPONSE_OK) + return False + return True diff --git a/sugar/graphics/animator.py b/sugar/graphics/animator.py new file mode 100644 index 0000000..459851b --- /dev/null +++ b/sugar/graphics/animator.py @@ -0,0 +1,94 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import time + +import gobject + +EASE_OUT_EXPO = 0 +EASE_IN_EXPO = 1 + +class Animator(gobject.GObject): + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + } + + def __init__(self, time, fps=20, easing=EASE_OUT_EXPO): + gobject.GObject.__init__(self) + self._animations = [] + self._time = time + self._interval = 1.0 / fps + self._easing = easing + self._timeout_sid = 0 + + def add(self, animation): + self._animations.append(animation) + + def remove_all(self): + self.stop() + self._animations = [] + + def start(self): + if self._timeout_sid: + self.stop() + + self._start_time = time.time() + self._timeout_sid = gobject.timeout_add( + int(self._interval * 1000), self._next_frame_cb) + + def stop(self): + if self._timeout_sid: + gobject.source_remove(self._timeout_sid) + self._timeout_sid = 0 + self.emit('completed') + + def _next_frame_cb(self): + current_time = min(self._time, time.time() - self._start_time) + current_time = max(current_time, 0.0) + + for animation in self._animations: + animation.do_frame(current_time, self._time, self._easing) + + if current_time == self._time: + self.stop() + return False + else: + return True + +class Animation(object): + def __init__(self, start, end): + self.start = start + self.end = end + + def do_frame(self, time, duration, easing): + start = self.start + change = self.end - self.start + + if time == duration: + # last frame + frame = self.end + else: + if easing == EASE_OUT_EXPO: + frame = change * (-pow(2, -10 * time/duration) + 1) + start; + elif easing == EASE_IN_EXPO: + frame = change * pow(2, 10 * (time / duration - 1)) + start; + + self.next_frame(frame) + + def next_frame(self, frame): + pass diff --git a/sugar/graphics/combobox.py b/sugar/graphics/combobox.py new file mode 100644 index 0000000..5584267 --- /dev/null +++ b/sugar/graphics/combobox.py @@ -0,0 +1,114 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +import sys +import os +import logging + +import gobject +import gtk + +class ComboBox(gtk.ComboBox): + __gtype_name__ = 'SugarComboBox' + + __gproperties__ = { + 'value' : (object, None, None, + gobject.PARAM_READABLE) + } + def __init__(self): + gtk.ComboBox.__init__(self) + + self._text_renderer = None + self._icon_renderer = None + + self._model = gtk.ListStore(gobject.TYPE_PYOBJECT, + gobject.TYPE_STRING, + gtk.gdk.Pixbuf, + gobject.TYPE_BOOLEAN) + self.set_model(self._model) + + self.set_row_separator_func(self._is_separator) + + def do_get_property(self, pspec): + if pspec.name == 'value': + row = self.get_active_item() + if not row: + return None + return row[0] + else: + return gtk.ComboBox.do_get_property(self, pspec) + + def _get_real_name_from_theme(self, name, size): + icon_theme = gtk.icon_theme_get_default() + width, height = gtk.icon_size_lookup(size) + info = icon_theme.lookup_icon(name, width, 0) + if not info: + raise ValueError("Icon '" + name + "' not found.") + fname = info.get_filename() + del info + return fname + + def append_item(self, action_id, text, icon_name=None, file_name=None): + if not self._icon_renderer and (icon_name or file_name): + self._icon_renderer = gtk.CellRendererPixbuf() + + settings = self.get_settings() + w, h = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU) + self._icon_renderer.props.stock_size = w + + self.pack_start(self._icon_renderer, False) + self.add_attribute(self._icon_renderer, 'pixbuf', 2) + + if not self._text_renderer and text: + self._text_renderer = gtk.CellRendererText() + self.pack_end(self._text_renderer, True) + self.add_attribute(self._text_renderer, 'text', 1) + + if icon_name or file_name: + if text: + size = gtk.ICON_SIZE_MENU + else: + size = gtk.ICON_SIZE_LARGE_TOOLBAR + width, height = gtk.icon_size_lookup(size) + + if icon_name: + file_name = self._get_real_name_from_theme(icon_name, size) + + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(file_name, width, height) + else: + pixbuf = None + + self._model.append([action_id, text, pixbuf, False]) + + def append_separator(self): + self._model.append([0, None, None, True]) + + def get_active_item(self): + index = self.get_active() + if index == -1: + index = 0 + + row = self._model.iter_nth_child(None, index) + if not row: + return None + return self._model[row] + + def remove_all(self): + self._model.clear() + + def _is_separator(self, model, row): + action_id, text, icon_name, is_separator = model[row] + return is_separator diff --git a/sugar/graphics/entry.py b/sugar/graphics/entry.py new file mode 100644 index 0000000..95510e5 --- /dev/null +++ b/sugar/graphics/entry.py @@ -0,0 +1,25 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import hippo + +class CanvasEntry(hippo.CanvasEntry): + def set_background(self, color_spec): + color = gtk.gdk.color_parse(color_spec) + self.props.widget.modify_bg(gtk.STATE_INSENSITIVE, color) + self.props.widget.modify_base(gtk.STATE_INSENSITIVE, color) diff --git a/sugar/graphics/icon.py b/sugar/graphics/icon.py new file mode 100644 index 0000000..81a8232 --- /dev/null +++ b/sugar/graphics/icon.py @@ -0,0 +1,550 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import re +import math +import time +import logging + +import gobject +import gtk +import hippo +import cairo + +from sugar.graphics.style import Color +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style +from sugar.graphics.palette import Palette, CanvasInvoker +from sugar.util import LRU + +_BADGE_SIZE = 0.45 + +class _SVGLoader(object): + def __init__(self): + self._cache = LRU(50) + + def load(self, file_name, entities, cache): + if file_name in self._cache: + icon = self._cache[file_name] + else: + icon_file = open(file_name, 'r') + icon = icon_file.read() + icon_file.close() + + if cache: + self._cache[file_name] = icon + + for entity, value in entities.items(): + if isinstance(value, basestring): + xml = '' % (entity, value) + icon = re.sub('' % entity, xml, icon) + else: + logging.error( + 'Icon %s, entity %s is invalid.', file_name, entity) + + import rsvg # XXX this is very slow! why? + return rsvg.Handle(data=icon) + +class _IconInfo(object): + def __init__(self): + self.file_name = None + self.attach_x = 0 + self.attach_y = 0 + +class _BadgeInfo(object): + def __init__(self): + self.attach_x = 0 + self.attach_y = 0 + self.size = 0 + self.icon_padding = 0 + +class _IconBuffer(object): + _surface_cache = LRU(50) + _loader = _SVGLoader() + + def __init__(self): + self.icon_name = None + self.file_name = None + self.fill_color = None + self.stroke_color = None + self.badge_name = None + self.width = None + self.height = None + self.cache = False + self.scale = 1.0 + + def _get_cache_key(self, sensitive): + return (self.icon_name, self.file_name, self.fill_color, + self.stroke_color, self.badge_name, self.width, self.height, + sensitive) + + def _load_svg(self, file_name): + entities = {} + if self.fill_color: + entities['fill_color'] = self.fill_color + if self.stroke_color: + entities['stroke_color'] = self.stroke_color + + return self._loader.load(file_name, entities, self.cache) + + def _get_attach_points(self, info, size_request): + attach_points = info.get_attach_points() + + if attach_points: + attach_x = float(attach_points[0][0]) / size_request + attach_y = float(attach_points[0][1]) / size_request + else: + attach_x = attach_y = 0 + + return attach_x, attach_y + + def _get_icon_info(self): + icon_info = _IconInfo() + + if self.file_name: + icon_info.file_name = self.file_name + elif self.icon_name: + theme = gtk.icon_theme_get_default() + + size = 50 + if self.width != None: + size = self.width + + info = theme.lookup_icon(self.icon_name, size, 0) + if info: + attach_x, attach_y = self._get_attach_points(info, size) + + icon_info.file_name = info.get_filename() + icon_info.attach_x = attach_x + icon_info.attach_y = attach_y + + del info + else: + logging.warning('No icon with the name %s ' + 'was found in the theme.' % self.icon_name) + + return icon_info + + def _draw_badge(self, context, size, sensitive, widget): + theme = gtk.icon_theme_get_default() + badge_info = theme.lookup_icon(self.badge_name, size, 0) + if badge_info: + badge_file_name = badge_info.get_filename() + if badge_file_name.endswith('.svg'): + handle = self._loader.load(badge_file_name, {}, self.cache) + pixbuf = handle.get_pixbuf() + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(badge_file_name) + + if not sensitive: + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(surface, 0, 0) + context.paint() + + def _get_size(self, icon_width, icon_height, padding): + if self.width is not None and self.height is not None: + width = self.width + padding + height = self.height + padding + else: + width = icon_width + padding + height = icon_height + padding + + return width, height + + def _get_badge_info(self, icon_info, icon_width, icon_height): + info = _BadgeInfo() + if self.badge_name is None: + return info + + info.size = int(_BADGE_SIZE * icon_width) + info.attach_x = int(icon_info.attach_x * icon_width - info.size / 2) + info.attach_y = int(icon_info.attach_y * icon_height - info.size / 2) + + if info.attach_x < 0 or info.attach_y < 0: + info.icon_padding = max(-info.attach_x, -info.attach_y) + elif info.attach_x + info.size > icon_width or \ + info.attach_y + info.size > icon_height: + x_padding = info.attach_x + info.size - icon_width + y_padding = info.attach_y + info.size - icon_height + info.icon_padding = max(x_padding, y_padding) + + return info + + def _get_xo_color(self): + if self.stroke_color and self.fill_color: + return XoColor('%s,%s' % (self.stroke_color, self.fill_color)) + else: + return None + + def _set_xo_color(self, xo_color): + if xo_color: + self.stroke_color = xo_color.get_stroke_color() + self.fill_color = xo_color.get_fill_color() + else: + self.stroke_color = None + self.fill_color = None + + def _get_insensitive_pixbuf (self, pixbuf, widget): + if not (widget and widget.style): + return pixbuf + + icon_source = gtk.IconSource() + # Special size meaning "don't touch" + icon_source.set_size(-1) + icon_source.set_pixbuf(pixbuf) + icon_source.set_state(gtk.STATE_INSENSITIVE) + icon_source.set_direction_wildcarded(False) + icon_source.set_size_wildcarded(False) + + # Please note that the pixbuf returned by this function is leaked + # with current stable versions of pygtk. The relevant bug is + # http://bugzilla.gnome.org/show_bug.cgi?id=502871 + # -- 2007-12-14 Benjamin Berg + pixbuf = widget.style.render_icon(icon_source, widget.get_direction(), + gtk.STATE_INSENSITIVE, -1, widget, + "sugar-icon") + + return pixbuf + + def get_surface(self, sensitive=True, widget=None): + cache_key = self._get_cache_key(sensitive) + if cache_key in self._surface_cache: + return self._surface_cache[cache_key] + + icon_info = self._get_icon_info() + if icon_info.file_name is None: + return None + + is_svg = icon_info.file_name.endswith('.svg') + + if is_svg: + handle = self._load_svg(icon_info.file_name) + dimensions = handle.get_dimension_data() + icon_width = int(dimensions[0]) + icon_height = int(dimensions[1]) + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.file_name) + icon_width = pixbuf.get_width() + icon_height = pixbuf.get_height() + + badge_info = self._get_badge_info(icon_info, icon_width, icon_height) + + padding = badge_info.icon_padding + width, height = self._get_size(icon_width, icon_height, padding) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + + context = cairo.Context(surface) + context.scale(float(width) / (icon_width + padding * 2), + float(height) / (icon_height + padding * 2)) + context.save() + + context.translate(padding, padding) + if is_svg: + if sensitive: + handle.render_cairo(context) + else: + pixbuf = handle.get_pixbuf() + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + + pixbuf_surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(pixbuf_surface, 0, 0) + context.paint() + else: + if not sensitive: + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + pixbuf_surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(pixbuf_surface, 0, 0) + context.paint() + + if self.badge_name: + context.restore() + context.translate(badge_info.attach_x, badge_info.attach_y) + self._draw_badge(context, badge_info.size, sensitive, widget) + + self._surface_cache[cache_key] = surface + + return surface + + xo_color = property(_get_xo_color, _set_xo_color) + +class Icon(gtk.Image): + __gtype_name__ = 'SugarIcon' + + __gproperties__ = { + 'xo-color' : (object, None, None, + gobject.PARAM_WRITABLE), + 'fill-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'stroke-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'badge-name' : (str, None, None, None, + gobject.PARAM_READWRITE) + } + + def __init__(self, **kwargs): + self._buffer = _IconBuffer() + + gobject.GObject.__init__(self, **kwargs) + + def _sync_image_properties(self): + if self._buffer.icon_name != self.props.icon_name: + self._buffer.icon_name = self.props.icon_name + + if self._buffer.file_name != self.props.file: + self._buffer.file_name = self.props.file + + width, height = gtk.icon_size_lookup(self.props.icon_size) + if self._buffer.width != width or self._buffer.height != height: + self._buffer.width = width + self._buffer.height = height + + def _icon_size_changed_cb(self, image, pspec): + self._buffer.icon_size = self.props.icon_size + + def _icon_name_changed_cb(self, image, pspec): + self._buffer.icon_name = self.props.icon_name + + def _file_changed_cb(self, image, pspec): + self._buffer.file_name = self.props.file + + def _update_buffer_size(self): + width, height = gtk.icon_size_lookup(self.props.icon_size) + + self._buffer.width = width + self._buffer.height = height + + def do_size_request(self, requisition): + self._sync_image_properties() + surface = self._buffer.get_surface() + if surface: + requisition[0] = surface.get_width() + requisition[1] = surface.get_height() + elif self._buffer.width and self._buffer.height: + requisition[0] = self._buffer.width + requisition[1] = self._buffer.width + else: + requisition[0] = requisition[1] = 0 + + def do_expose_event(self, event): + self._sync_image_properties() + sensitive = (self.state != gtk.STATE_INSENSITIVE) + surface = self._buffer.get_surface(sensitive, self) + if surface is None: + return + + xpad, ypad = self.get_padding() + xalign, yalign = self.get_alignment() + requisition = self.get_child_requisition() + if self.get_direction() != gtk.TEXT_DIR_LTR: + xalign = 1.0 - xalign + + x = math.floor(self.allocation.x + xpad + + (self.allocation.width - requisition[0]) * xalign) + y = math.floor(self.allocation.y + ypad + + (self.allocation.height - requisition[1]) * yalign) + + cr = self.window.cairo_create() + cr.set_source_surface(surface, x, y) + cr.paint() + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + if self._buffer.xo_color != value: + self._buffer.xo_color = value + self.queue_draw() + elif pspec.name == 'fill-color': + if self._buffer.fill_color != value: + self._buffer.fill_color = value + self.queue_draw() + elif pspec.name == 'stroke-color': + if self._buffer.stroke_color != value: + self._buffer.stroke_color = value + self.queue_draw() + elif pspec.name == 'badge-name': + if self._buffer.badge_name != value: + self._buffer.badge_name = value + self.queue_resize() + else: + gtk.Image.do_set_property(self, pspec, value) + + def do_get_property(self, pspec): + if pspec.name == 'fill-color': + return self._buffer.fill_color + elif pspec.name == 'stroke-color': + return self._buffer.stroke_color + elif pspec.name == 'badge-name': + return self._buffer.badge_name + else: + return gtk.Image.do_get_property(self, pspec) + +class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'CanvasIcon' + + __gproperties__ = { + 'file-name' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'icon-name' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'xo-color' : (object, None, None, + gobject.PARAM_WRITABLE), + 'fill-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'stroke-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'size' : (int, None, None, 0, 1024, 0, + gobject.PARAM_READWRITE), + 'scale' : (float, None, None, -1024.0, 1024.0, 1.0, + gobject.PARAM_READWRITE), + 'cache' : (bool, None, None, False, + gobject.PARAM_READWRITE), + 'badge-name' : (str, None, None, None, + gobject.PARAM_READWRITE) + } + + def __init__(self, **kwargs): + self._buffer = _IconBuffer() + + hippo.CanvasBox.__init__(self, **kwargs) + + self._palette = None + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette is not None: + self._palette.destroy() + + def do_set_property(self, pspec, value): + if pspec.name == 'file-name': + if self._buffer.file_name != value: + self._buffer.file_name = value + self.emit_paint_needed(0, 0, -1, -1) + elif pspec.name == 'icon-name': + if self._buffer.icon_name != value: + self._buffer.icon_name = value + self.emit_paint_needed(0, 0, -1, -1) + elif pspec.name == 'xo-color': + if self._buffer.xo_color != value: + self._buffer.xo_color = value + self.emit_paint_needed(0, 0, -1, -1) + elif pspec.name == 'fill-color': + if self._buffer.fill_color != value: + self._buffer.fill_color = value + self.emit_paint_needed(0, 0, -1, -1) + elif pspec.name == 'stroke-color': + if self._buffer.stroke_color != value: + self._buffer.stroke_color = value + self.emit_paint_needed(0, 0, -1, -1) + elif pspec.name == 'size': + if self._buffer.width != value: + self._buffer.width = value + self._buffer.height = value + self.emit_request_changed() + elif pspec.name == 'scale': + logging.warning('CanvasIcon: the scale parameter is currently unsupported') + if self._buffer.scale != value: + self._buffer.scale = value + self.emit_request_changed() + elif pspec.name == 'cache': + self._buffer.cache = value + elif pspec.name == 'badge-name': + if self._buffer.badge_name != value: + self._buffer.badge_name = value + self.emit_paint_needed(0, 0, -1, -1) + + def do_get_property(self, pspec): + if pspec.name == 'size': + return self._buffer.width + elif pspec.name == 'file-name': + return self._buffer.file_name + elif pspec.name == 'icon-name': + return self._buffer.icon_name + elif pspec.name == 'fill-color': + return self._buffer.fill_color + elif pspec.name == 'stroke-color': + return self._buffer.stroke_color + elif pspec.name == 'cache': + return self._buffer.cache + elif pspec.name == 'badge-name': + return self._buffer.badge_name + elif pspec.name == 'scale': + return self._buffer.scale + + def do_paint_below_children(self, cr, damaged_box): + surface = self._buffer.get_surface() + if surface: + width, height = self.get_allocation() + + x = (width - surface.get_width()) / 2 + y = (height - surface.get_height()) / 2 + + cr.set_source_surface(surface, x, y) + cr.paint() + + def do_get_content_width_request(self): + surface = self._buffer.get_surface() + if surface: + size = surface.get_width() + elif self._buffer.width: + size = self._buffer.width + else: + size = 0 + + return size, size + + def do_get_content_height_request(self, for_width): + surface = self._buffer.get_surface() + if surface: + size = surface.get_height() + elif self._buffer.height: + size = self._buffer.height + else: + size = 0 + + return size, size + + def do_button_press_event(self, event): + self.emit_activated() + return True + + def get_palette(self): + return self._palette + + def set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + if not self._palette.props.invoker: + self._palette.props.invoker = CanvasInvoker(self) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + + palette = property(get_palette, set_palette) + +def get_icon_state(base_name, perc): + step = 5 + strength = round(perc / step) * step + icon_theme = gtk.icon_theme_get_default() + + while strength <= 100: + icon_name = '%s-%03d' % (base_name, strength) + if icon_theme.has_icon(icon_name): + return icon_name + + strength = strength + step diff --git a/sugar/graphics/iconentry.py b/sugar/graphics/iconentry.py new file mode 100644 index 0000000..a1fed31 --- /dev/null +++ b/sugar/graphics/iconentry.py @@ -0,0 +1,108 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +from sugar import _sugarext + +from sugar.graphics import style +from sugar.graphics.icon import _SVGLoader +import sugar.profile + +ICON_ENTRY_PRIMARY = _sugarext.ICON_ENTRY_PRIMARY +ICON_ENTRY_SECONDARY = _sugarext.ICON_ENTRY_SECONDARY + +class IconEntry(_sugarext.IconEntry): + + def __init__(self): + _sugarext.IconEntry.__init__(self) + + self._clear_icon = None + self._clear_shown = False + + self.connect('key_press_event', self._keypress_event_cb) + + def set_icon_from_name(self, position, name): + icon_theme = gtk.icon_theme_get_default() + icon_info = icon_theme.lookup_icon(name, + gtk.ICON_SIZE_SMALL_TOOLBAR, + 0) + + if icon_info.get_filename().endswith('.svg'): + loader = _SVGLoader() + color = sugar.profile.get_color() + entities = {'fill_color': style.COLOR_TOOLBAR_GREY.get_svg(), + 'stroke_color': style.COLOR_TOOLBAR_GREY.get_svg()} + handle = loader.load(icon_info.get_filename(), entities, None) + pixbuf = handle.get_pixbuf() + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.get_filename()) + del icon_info + + image = gtk.Image() + image.set_from_pixbuf(pixbuf) + image.show() + + self.set_icon(position, image) + + def set_icon(self, position, image): + if image.get_storage_type() not in [gtk.IMAGE_PIXBUF, gtk.IMAGE_STOCK]: + raise ValueError('Image must have a storage type of pixbuf or ' + + 'stock, not %r.' % image.get_storage_type()) + _sugarext.IconEntry.set_icon(self, position, image) + + def remove_icon(self, position): + _sugarext.IconEntry.set_icon(self, position, None) + + def add_clear_button(self): + if self.props.text != "": + self.show_clear_button() + else: + self.hide_clear_button() + + self.connect('icon-pressed', self._icon_pressed_cb) + self.connect('changed', self._changed_cb) + + def show_clear_button(self): + if not self._clear_shown: + self.set_icon_from_name(ICON_ENTRY_SECONDARY, + 'dialog-cancel') + self._clear_shown = True + + def hide_clear_button(self): + if self._clear_shown: + self.remove_icon(ICON_ENTRY_SECONDARY) + self._clear_shown = False + + def _keypress_event_cb(self, widget, event): + keyval = gtk.gdk.keyval_name(event.keyval) + if keyval == 'Escape': + self.props.text = '' + return True + return False + + def _icon_pressed_cb(self, entru, icon_pos, button): + if icon_pos == ICON_ENTRY_SECONDARY: + self.set_text('') + self.hide_clear_button() + + def _changed_cb(self, icon_entry): + if not self.props.text: + self.hide_clear_button() + else: + self.show_clear_button() + diff --git a/sugar/graphics/menuitem.py b/sugar/graphics/menuitem.py new file mode 100644 index 0000000..908cc1f --- /dev/null +++ b/sugar/graphics/menuitem.py @@ -0,0 +1,33 @@ +# Copyright (C) 2007, Eduardo Silva +# +# 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 gtk +from sugar.graphics.icon import Icon + +import pango + +class MenuItem(gtk.ImageMenuItem): + def __init__(self, text_label=None, icon_name=None, text_maxlen=0): + gtk.ImageMenuItem.__init__(self, text_label) + if icon_name: + icon = Icon(icon_name=icon_name, icon_size=gtk.ICON_SIZE_MENU) + self.set_image(icon) + icon.show() + + if text_maxlen > 0: + self.child.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + self.child.set_max_width_chars(text_maxlen) diff --git a/sugar/graphics/notebook.py b/sugar/graphics/notebook.py new file mode 100644 index 0000000..2d49b1f --- /dev/null +++ b/sugar/graphics/notebook.py @@ -0,0 +1,115 @@ +"""Notebook class + +This class create a gtk.Notebook() widget supporting +a close button in every tab when the 'can-close-tabs' gproperty +is enabled (True) +""" + +# Copyright (C) 2007, Eduardo Silva (edsiper@gmail.com) +# +# 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 gtk +import gobject + +class Notebook(gtk.Notebook): + __gtype_name__ = 'SugarNotebook' + + __gproperties__ = { + 'can-close-tabs': (bool, None, None, False, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY) + } + + def __init__(self, **kwargs): + # Initialise the Widget + # + # Side effects: + # Set the 'can-close-tabs' property using **kwargs + # Set True the scrollable notebook property + + gobject.GObject.__init__(self, **kwargs) + gtk.Notebook.__init__(self) + + self.set_scrollable(True) + self.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'can-close-tabs': + self._can_close_tabs = value + else: + raise AssertionError + + def _add_icon_to_button(self, button): + icon_box = gtk.HBox() + image = gtk.Image() + image.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + gtk.Button.set_relief(button, gtk.RELIEF_NONE) + + settings = gtk.Widget.get_settings(button) + (w,h) = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU) + gtk.Widget.set_size_request(button, w + 4, h + 4) + image.show() + icon_box.pack_start(image, True, False, 0) + button.add(icon_box) + icon_box.show() + + def _create_custom_tab(self, text, child): + event_box = gtk.EventBox() + + tab_box = gtk.HBox(False, 2) + tab_label = gtk.Label(text) + + tab_button = gtk.Button() + tab_button.connect('clicked', self._close_page, child) + + # Add a picture on a button + self._add_icon_to_button(tab_button) + icon_box = gtk.HBox(False, 0) + + event_box.show() + tab_button.show() + tab_label.show() + + tab_box.pack_start(tab_label, True) + tab_box.pack_start(tab_button, True) + + tab_box.show_all() + event_box.add(tab_box) + + return event_box + + def add_page(self, text_label, widget): + # Add a new page to the notebook + if self._can_close_tabs: + eventbox = self._create_custom_tab(text_label, widget) + self.append_page(widget, eventbox) + else: + self.append_page(widget, gtk.Label(text_label)) + + pages = self.get_n_pages() + + # Set the new page + self.set_current_page(pages - 1) + self.show_all() + + return True + + def _close_page(self, button, child): + # Remove a page from the notebook + page = self.page_num(child) + + if page != -1: + self.remove_page(page) diff --git a/sugar/graphics/objectchooser.py b/sugar/graphics/objectchooser.py new file mode 100644 index 0000000..59f1a8a --- /dev/null +++ b/sugar/graphics/objectchooser.py @@ -0,0 +1,119 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging +import time + +import gobject +import gtk +import dbus + +from sugar.datastore import datastore + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' + +class ObjectChooser(object): + def __init__(self, title=None, parent=None, flags=None, buttons=None): + # For backwards compatibility: + # - We ignore title, flags and buttons. + # - 'parent' can be a xid or a gtk.gdk.Window + + if title is not None or flags is not None or buttons is not None: + logging.warning('Invocation of ObjectChooser() has deprecated ' + 'parameters.') + + if parent is None: + parent_xid = 0 + elif hasattr(parent, 'window') and hasattr(parent.window, 'xid'): + parent_xid = parent.window.xid + else: + parent_xid = parent + + self._parent_xid = parent_xid + self._main_loop = None + self._object_id = None + self._bus = None + self._chooser_id = None + self._response_code = gtk.RESPONSE_NONE + + def run(self): + self._object_id = None + + self._main_loop = gobject.MainLoop() + + self._bus = dbus.SessionBus(mainloop=self._main_loop) + self._bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus", + arg0=J_DBUS_SERVICE) + + obj = self._bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) + journal = dbus.Interface(obj, J_DBUS_INTERFACE) + journal.connect_to_signal('ObjectChooserResponse', + self.__chooser_response_cb) + journal.connect_to_signal('ObjectChooserCancelled', + self.__chooser_cancelled_cb) + self._chooser_id = journal.ChooseObject(self._parent_xid) + + gtk.gdk.threads_leave() + try: + self._main_loop.run() + finally: + gtk.gdk.threads_enter() + self._main_loop = None + + return self._response_code + + def get_selected_object(self): + if self._object_id is None: + return None + else: + return datastore.get(self._object_id) + + def destroy(self): + self._cleanup() + + def _cleanup(self): + if self._main_loop is not None: + self._main_loop.quit() + self._main_loop = None + self._bus = None + + def __chooser_response_cb(self, chooser_id, object_id): + if chooser_id != self._chooser_id: + return + logging.debug('ObjectChooser.__chooser_response_cb: %r' % object_id) + self._response_code = gtk.RESPONSE_ACCEPT + self._object_id = object_id + self._cleanup() + + def __chooser_cancelled_cb(self, chooser_id): + if chooser_id != self._chooser_id: + return + logging.debug('ObjectChooser.__chooser_cancelled_cb: %r' % chooser_id) + self._response_code = gtk.RESPONSE_CANCEL + self._cleanup() + + def __name_owner_changed_cb(self, name, old, new): + logging.debug('ObjectChooser.__name_owner_changed_cb') + # Journal service disappeared from the bus + self._response_code = gtk.RESPONSE_CANCEL + self._cleanup() + diff --git a/sugar/graphics/palette.py b/sugar/graphics/palette.py new file mode 100644 index 0000000..85e60ac --- /dev/null +++ b/sugar/graphics/palette.py @@ -0,0 +1,877 @@ +# Copyright (C) 2007, Eduardo Silva +# +# 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 logging + +import gtk +import gobject +import time +import hippo +import pango + +from sugar.graphics import palettegroup +from sugar.graphics import animator +from sugar.graphics import style +from sugar import _sugarext + +# Helper function to find the gap position and size of widget a +def _calculate_gap(a, b): + # Test for each side if the palette and invoker are + # adjacent to each other. + gap = True + + if a.y + a.height == b.y: + gap_side = gtk.POS_BOTTOM + elif a.x + a.width == b.x: + gap_side = gtk.POS_RIGHT + elif a.x == b.x + b.width: + gap_side = gtk.POS_LEFT + elif a.y == b.y + b.height: + gap_side = gtk.POS_TOP + else: + gap = False + + if gap: + if gap_side == gtk.POS_BOTTOM or gap_side == gtk.POS_TOP: + gap_start = min(a.width, max(0, b.x - a.x)) + gap_size = max(0, min(a.width, + (b.x + b.width) - a.x) - gap_start) + elif gap_side == gtk.POS_RIGHT or gap_side == gtk.POS_LEFT: + gap_start = min(a.height, max(0, b.y - a.y)) + gap_size = max(0, min(a.height, + (b.y + b.height) - a.y) - gap_start) + + if gap and gap_size > 0: + return (gap_side, gap_start, gap_size) + else: + return False + +class MouseSpeedDetector(gobject.GObject): + + __gsignals__ = { + 'motion-slow': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'motion-fast': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + _MOTION_SLOW = 1 + _MOTION_FAST = 2 + + def __init__(self, parent, delay, thresh): + """Create MouseSpeedDetector object, + delay in msec + threshold in pixels (per tick of 'delay' msec)""" + + gobject.GObject.__init__(self) + + self._threshold = thresh + self._parent = parent + self._delay = delay + + self._state = None + + self._timeout_hid = None + + def start(self): + self._state = None + self._mouse_pos = self._get_mouse_position() + + self._timeout_hid = gobject.timeout_add(self._delay, self._timer_cb) + + def stop(self): + if self._timeout_hid is not None: + gobject.source_remove(self._timeout_hid) + self._state = None + + def _get_mouse_position(self): + display = gtk.gdk.display_get_default() + screen, x, y, mask = display.get_pointer() + return (x, y) + + def _detect_motion(self): + oldx, oldy = self._mouse_pos + (x, y) = self._get_mouse_position() + self._mouse_pos = (x, y) + + dist2 = (oldx - x)**2 + (oldy - y)**2 + if dist2 > self._threshold**2: + return True + else: + return False + + def _timer_cb(self): + motion = self._detect_motion() + if motion and self._state != self._MOTION_FAST: + self.emit('motion-fast') + self._state = self._MOTION_FAST + elif not motion and self._state != self._MOTION_SLOW: + self.emit('motion-slow') + self._state = self._MOTION_SLOW + + return True + +class Palette(gtk.Window): + PRIMARY = 0 + SECONDARY = 1 + + __gtype_name__ = 'SugarPalette' + + __gproperties__ = { + 'invoker' : (object, None, None, + gobject.PARAM_READWRITE) + } + + __gsignals__ = { + 'popup' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'popdown' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, label, accel_path=None, menu_after_content=False, + text_maxlen=0): + gtk.Window.__init__(self) + + self.set_decorated(False) + self.set_resizable(False) + # Just assume xthickness and ythickness are the same + self.set_border_width(self.style.xthickness) + self.connect('realize', self._realize_cb) + self.connect('destroy', self.__destroy_cb) + + self.palette_state = self.PRIMARY + + self._alignment = None + self._old_alloc = None + self._full_request = [0, 0] + self._cursor_x = 0 + self._cursor_y = 0 + self._invoker = None + self._group_id = None + self._up = False + self._palette_popup_sid = None + + self._popup_anim = animator.Animator(0.3, 10) + self._popup_anim.add(_PopupAnimation(self)) + + self._secondary_anim = animator.Animator(1.0, 10) + self._secondary_anim.add(_SecondaryAnimation(self)) + + self._popdown_anim = animator.Animator(0.6, 10) + self._popdown_anim.add(_PopdownAnimation(self)) + + vbox = gtk.VBox() + + self._label = gtk.Label() + self._label.set_size_request(-1, style.zoom(style.GRID_CELL_SIZE) + - 2*self.get_border_width()) + self._label.set_alignment(0, 0.5) + self._label.set_padding(style.DEFAULT_SPACING, 0) + + if text_maxlen > 0: + self._label.set_max_width_chars(text_maxlen) + self._label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + + vbox.pack_start(self._label, False) + + self._secondary_box = gtk.VBox() + vbox.pack_start(self._secondary_box) + + self._separator = gtk.HSeparator() + self._secondary_box.pack_start(self._separator) + + self._menu_content_separator = gtk.HSeparator() + + if menu_after_content: + self._add_content() + self._secondary_box.pack_start(self._menu_content_separator) + self._add_menu() + else: + self._add_menu() + self._secondary_box.pack_start(self._menu_content_separator) + self._add_content() + + self.action_bar = PaletteActionBar() + self._secondary_box.pack_start(self.action_bar) + self.action_bar.show() + + self.add(vbox) + vbox.show() + + # The menu is not shown here until an item is added + self.menu = _Menu(self) + + self.connect('enter-notify-event', + self._enter_notify_event_cb) + self.connect('leave-notify-event', + self._leave_notify_event_cb) + + self.set_primary_text(label, accel_path) + self.set_group_id('default') + + self._mouse_detector = MouseSpeedDetector(self, 200, 5) + self._mouse_detector.connect('motion-slow', self._mouse_slow_cb) + + def __destroy_cb(self, palette): + self.set_group_id(None) + + if self._palette_popup_sid is not None: + _palette_observer.disconnect(self._palette_popup_sid) + + def _add_menu(self): + self._menu_box = gtk.VBox() + self._secondary_box.pack_start(self._menu_box) + self._menu_box.show() + + def _add_content(self): + # The content is not shown until a widget is added + self._content = gtk.VBox() + self._content.set_border_width(style.DEFAULT_SPACING) + self._secondary_box.pack_start(self._content) + + def do_style_set(self, previous_style): + # Prevent a warning from pygtk + if previous_style is not None: + gtk.Window.do_style_set(self, previous_style) + self.set_border_width(self.style.xthickness) + + def is_up(self): + return self._up + + def get_rect(self): + win_x, win_y = self.window.get_origin() + rectangle = self.get_allocation() + + x = win_x + rectangle.x + y = win_y + rectangle.y + width = rectangle.width + height = rectangle.height + + return gtk.gdk.Rectangle(x, y, width, height) + + def set_primary_text(self, label, accel_path=None): + if label is not None: + self._label.set_markup(""+label+"") + self._label.show() + + def set_content(self, widget): + if len(self._content.get_children()) > 0: + self._content.remove(self._content.get_children()[0]) + + if widget is not None: + self._content.add(widget) + self._content.show() + else: + self._content.hide() + + self._update_accept_focus() + self._update_separators() + + def set_group_id(self, group_id): + if self._group_id: + group = palettegroup.get_group(self._group_id) + group.remove(self) + if group_id: + self._group_id = group_id + group = palettegroup.get_group(group_id) + group.add(self) + + def do_set_property(self, pspec, value): + if pspec.name == 'invoker': + if self._invoker is not None: + self._invoker.disconnect(self._enter_invoker_hid) + self._invoker.disconnect(self._leave_invoker_hid) + + self._invoker = value + if value is not None: + self._enter_invoker_hid = self._invoker.connect( + 'mouse-enter', self._invoker_mouse_enter_cb) + self._leave_invoker_hid = self._invoker.connect( + 'mouse-leave', self._invoker_mouse_leave_cb) + else: + raise AssertionError + + def do_get_property(self, pspec): + if pspec.name == 'invoker': + return self._invoker + else: + raise AssertionError + + def do_size_request(self, requisition): + gtk.Window.do_size_request(self, requisition) + + requisition.width = max(requisition.width, self._full_request[0]) + + # Minimum width + requisition.width = max(requisition.width, + style.zoom(style.GRID_CELL_SIZE*2)) + + def do_size_allocate(self, allocation): + gtk.Window.do_size_allocate(self, allocation) + + if self._old_alloc is None or \ + self._old_alloc.x != allocation.x or \ + self._old_alloc.y != allocation.y or \ + self._old_alloc.width != allocation.width or \ + self._old_alloc.height != allocation.height: + self.queue_draw() + + # We need to store old allocation because when size_allocate + # is called widget.allocation is already updated. + # gtk.Window resizing is different from normal containers: + # the X window is resized, widget.allocation is updated from + # the configure request handler and finally size_allocate is called. + self._old_alloc = allocation + + def do_expose_event(self, event): + # We want to draw a border with a beautiful gap + if self._invoker is not None and self._invoker.has_rectangle_gap(): + invoker = self._invoker.get_rect() + palette = self.get_rect() + + gap = _calculate_gap(palette, invoker) + else: + gap = False + + if gap: + self.style.paint_box_gap(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, + self.allocation.width, + self.allocation.height, + gap[0], gap[1], gap[2]) + else: + self.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, + self.allocation.width, + self.allocation.height) + + # Fall trough to the container expose handler. + # (Leaving out the window expose handler which redraws everything) + gtk.Bin.do_expose_event(self, event) + + def _update_separators(self): + visible = len(self.menu.get_children()) > 0 or \ + len(self._content.get_children()) > 0 + self._separator.props.visible = visible + + visible = len(self.menu.get_children()) > 0 and \ + len(self._content.get_children()) > 0 + self._menu_content_separator.props.visible = visible + + def _update_accept_focus(self): + accept_focus = len(self._content.get_children()) + if self.window: + self.window.set_accept_focus(accept_focus) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self._update_accept_focus() + + def _update_full_request(self): + state = self.palette_state + + self._set_state(self.SECONDARY) + self._full_request = self.size_request() + + self._set_state(state) + + def _update_position(self): + invoker = self._invoker + if invoker is None or self._alignment is None: + logging.error('Cannot update the palette position.') + return + + rect = self.size_request() + position = invoker.get_position_for_alignment(self._alignment, rect) + if position is None: + position = invoker.get_position(rect) + + self.move(position.x, position.y) + + def _show(self): + if self._up: + return + + self._palette_popup_sid = _palette_observer.connect( + 'popup', self._palette_observer_popup_cb) + + if self._invoker is not None: + self._update_full_request() + self._alignment = self._invoker.get_alignment(self._full_request) + self._update_position() + + self.menu.set_active(True) + self.show() + + self._invoker.notify_popup() + + self._up = True + _palette_observer.emit('popup', self) + self.emit('popup') + + def _hide(self): + self._secondary_anim.stop() + + if not self._palette_popup_sid is None: + _palette_observer.disconnect(self._palette_popup_sid) + self._palette_popup_sid = None + + self.menu.set_active(False) + self.hide() + + if self._invoker: + self._invoker.notify_popdown() + + self._up = False + self.emit('popdown') + + def popup(self, immediate=False): + self._popdown_anim.stop() + + if not immediate: + self._popup_anim.start() + else: + self._show() + + self._secondary_anim.start() + + def popdown(self, immediate=False): + self._popup_anim.stop() + + self._mouse_detector.stop() + + if not immediate: + self._popdown_anim.start() + else: + self._hide() + + def _set_state(self, state): + if self.palette_state == state: + return + + if state == self.PRIMARY: + self.menu.unembed() + self._secondary_box.hide() + elif state == self.SECONDARY: + self.menu.embed(self._menu_box) + self._secondary_box.show() + + self.palette_state = state + + def _invoker_mouse_enter_cb(self, invoker): + self._mouse_detector.start() + + def _mouse_slow_cb(self, widget): + self._mouse_detector.stop() + self._palette_do_popup() + + def _palette_do_popup(self): + immediate = False + + if self.is_up(): + self._popdown_anim.stop() + return + + if self._group_id: + group = palettegroup.get_group(self._group_id) + if group and group.is_up(): + self._set_state(self.PRIMARY) + + immediate = True + group.popdown() + + self.popup(immediate=immediate) + + def _invoker_mouse_leave_cb(self, invoker): + self._mouse_detector.stop() + self.popdown() + + def _enter_notify_event_cb(self, widget, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._popdown_anim.stop() + self._secondary_anim.start() + + def _leave_notify_event_cb(self, widget, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.popdown() + + def _palette_observer_popup_cb(self, observer, palette): + if self != palette: + self._hide() + +class PaletteActionBar(gtk.HButtonBox): + def add_action(label, icon_name=None): + button = Button(label) + + if icon_name: + icon = Icon(icon_name) + button.set_image(icon) + icon.show() + + self.pack_start(button) + button.show() + +class _Menu(_sugarext.Menu): + __gtype_name__ = 'SugarPaletteMenu' + + def __init__(self, palette): + _sugarext.Menu.__init__(self) + self._palette = palette + + def do_insert(self, item, position): + _sugarext.Menu.do_insert(self, item, position) + self._palette._update_separators() + self.show() + + def do_expose_event(self, event): + # Ignore the Menu expose, just do the MenuShell expose to prevent any + # border from being drawn here. A border is drawn by the palette object + # around everything. + gtk.MenuShell.do_expose_event(self, event) + + def do_grab_notify(self, was_grabbed): + # Ignore grab_notify as the menu would close otherwise + pass + + def do_deactivate(self): + self._palette._hide() + +class _PopupAnimation(animator.Animation): + def __init__(self, palette): + animator.Animation.__init__(self, 0.0, 1.0) + self._palette = palette + + def next_frame(self, current): + if current == 1.0: + self._palette._set_state(Palette.PRIMARY) + self._palette._show() + +class _SecondaryAnimation(animator.Animation): + def __init__(self, palette): + animator.Animation.__init__(self, 0.0, 1.0) + self._palette = palette + + def next_frame(self, current): + if current == 1.0: + self._palette._set_state(Palette.SECONDARY) + self._palette._update_position() + +class _PopdownAnimation(animator.Animation): + def __init__(self, palette): + animator.Animation.__init__(self, 0.0, 1.0) + self._palette = palette + + def next_frame(self, current): + if current == 1.0: + self._palette._hide() + +class Invoker(gobject.GObject): + __gtype_name__ = 'SugarPaletteInvoker' + + __gsignals__ = { + 'mouse-enter': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'mouse-leave': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'focus-out': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + ANCHORED = 0 + AT_CURSOR = 1 + + BOTTOM = [(0.0, 0.0, 0.0, 1.0), + (-1.0, 0.0, 1.0, 1.0)] + RIGHT = [(0.0, 0.0, 1.0, 0.0), + (0.0, -1.0, 1.0, 1.0)] + TOP = [(0.0, -1.0, 0.0, 0.0), + (-1.0, -1.0, 1.0, 0.0)] + LEFT = [(-1.0, 0.0, 0.0, 0.0), + (-1.0, -1.0, 0.0, 1.0)] + + def __init__(self): + gobject.GObject.__init__(self) + + self._screen_area = gtk.gdk.Rectangle(0, 0, gtk.gdk.screen_width(), + gtk.gdk.screen_height()) + self._position_hint = self.ANCHORED + self._cursor_x = -1 + self._cursor_y = -1 + + def _get_position_for_alignment(self, alignment, palette_dim): + palette_halign = alignment[0] + palette_valign = alignment[1] + invoker_halign = alignment[2] + invoker_valign = alignment[3] + + if self._cursor_x == -1 or self._cursor_y == -1: + display = gtk.gdk.display_get_default() + screen, x, y, mask = display.get_pointer() + self._cursor_x = x + self._cursor_y = y + + if self._position_hint is self.ANCHORED: + rect = self.get_rect() + else: + dist = style.PALETTE_CURSOR_DISTANCE + rect = gtk.gdk.Rectangle(self._cursor_x - dist, + self._cursor_y - dist, + dist * 2, dist * 2) + + palette_width, palette_height = palette_dim + + x = rect.x + rect.width * invoker_halign + \ + palette_width * palette_halign + + y = rect.y + rect.height * invoker_valign + \ + palette_height * palette_valign + + return gtk.gdk.Rectangle(int(x), int(y), + palette_width, palette_height) + + def _in_screen(self, rect): + return rect.x >= self._screen_area.x and \ + rect.y >= self._screen_area.y and \ + rect.x + rect.width <= self._screen_area.width and \ + rect.y + rect.height <= self._screen_area.height + + def _get_area_in_screen(self, rect): + """Return area of rectangle visible in the screen""" + + x1 = max(rect.x, self._screen_area.x) + y1 = max(rect.y, self._screen_area.y) + x2 = min(rect.x + rect.width, + self._screen_area.x + self._screen_area.width) + y2 = min(rect.y + rect.height, + self._screen_area.y + self._screen_area.height) + + return (x2 - x1) * (y2 - y1) + + def _get_alignments(self): + if self._position_hint is self.AT_CURSOR: + return [(0.0, 0.0, 1.0, 1.0), + (0.0, -1.0, 1.0, 0.0), + (-1.0, -1.0, 0.0, 0.0), + (-1.0, 0.0, 0.0, 1.0)] + else: + return self.BOTTOM + self.RIGHT + self.TOP + self.LEFT + + def get_position_for_alignment(self, alignment, palette_dim): + rect = self._get_position_for_alignment(alignment, palette_dim) + if self._in_screen(rect): + return rect + else: + return None + + def get_position(self, palette_dim): + alignment = self.get_alignment(palette_dim) + return self._get_position_for_alignment(alignment, palette_dim) + + def get_alignment(self, palette_dim): + best_alignment = None + best_area = -1 + for alignment in self._get_alignments(): + pos = self._get_position_for_alignment(alignment, palette_dim) + if self._in_screen(pos): + return alignment + + area = self._get_area_in_screen(pos) + if area > best_area: + best_alignment = alignment + best_area = area + + # Palette horiz/vert alignment + ph = best_alignment[0] + pv = best_alignment[1] + + # Invoker horiz/vert alignment + ih = best_alignment[2] + iv = best_alignment[3] + + rect = self.get_rect() + screen_area = self._screen_area + + if best_alignment in self.LEFT or best_alignment in self.RIGHT: + dtop = rect.y - screen_area.y + dbottom = screen_area.y + screen_area.height - rect.y - rect.width + + iv = 0 + + # Set palette_valign to align to screen on the top + if dtop > dbottom: + pv = -float(dtop) / palette_dim[1] + + # Set palette_valign to align to screen on the bottom + else: + pv = -float(palette_dim[1] - dbottom - rect.height) \ + / palette_dim[1] + + else: + dleft = rect.x - screen_area.x + dright = screen_area.x + screen_area.width - rect.x - rect.width + + ih = 0 + + # Set palette_halign to align to screen on left + if dleft > dright: + ph = -float(dleft) / palette_dim[0] + + # Set palette_halign to align to screen on right + else: + ph = -float(palette_dim[0] - dright - rect.width) \ + / palette_dim[0] + + return (ph, pv, ih, iv) + + def has_rectangle_gap(self): + return False + + def draw_rectangle(self, event, palette): + pass + + def notify_popup(self): + pass + + def notify_popdown(self): + self._cursor_x = -1 + self._cursor_y = -1 + +class WidgetInvoker(Invoker): + def __init__(self, widget): + Invoker.__init__(self) + self._widget = widget + + widget.connect('enter-notify-event', self._enter_notify_event_cb) + widget.connect('leave-notify-event', self._leave_notify_event_cb) + + def get_rect(self): + allocation = self._widget.get_allocation() + if self._widget.window is not None: + x, y = self._widget.window.get_origin() + else: + logging.warning( + "Trying to position palette with invoker that's not realized.") + x = 0 + y = 0 + + if self._widget.flags() & gtk.NO_WINDOW: + x += allocation.x + y += allocation.y + + width = allocation.width + height = allocation.height + + return gtk.gdk.Rectangle(x, y, width, height) + + def has_rectangle_gap(self): + return True + + def draw_rectangle(self, event, palette): + style = self._widget.style + gap = _calculate_gap(self.get_rect(), palette.get_rect()) + if gap: + style.paint_box_gap(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self._widget, + "palette-invoker", + self._widget.allocation.x, + self._widget.allocation.y, + self._widget.allocation.width, + self._widget.allocation.height, + gap[0], gap[1], gap[2]) + else: + style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self._widget, + "palette-invoker", + self._widget.allocation.x, + self._widget.allocation.y, + self._widget.allocation.width, + self._widget.allocation.height) + + def _enter_notify_event_cb(self, widget, event): + self.emit('mouse-enter') + + def _leave_notify_event_cb(self, widget, event): + self.emit('mouse-leave') + + def get_toplevel(self): + return self._widget.get_toplevel() + + def notify_popup(self): + Invoker.notify_popup(self) + self._widget.queue_draw() + + def notify_popdown(self): + Invoker.notify_popdown(self) + self._widget.queue_draw() + +class CanvasInvoker(Invoker): + def __init__(self, item): + Invoker.__init__(self) + + self._item = item + self._position_hint = self.AT_CURSOR + + item.connect('motion-notify-event', + self._motion_notify_event_cb) + + def get_default_position(self): + return self.AT_CURSOR + + def get_rect(self): + context = self._item.get_context() + if context: + x, y = context.translate_to_screen(self._item) + width, height = self._item.get_allocation() + return gtk.gdk.Rectangle(x, y, width, height) + else: + return gtk.gdk.Rectangle() + + def _motion_notify_event_cb(self, button, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + context = self._item.get_context() + self.emit('mouse-enter') + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + self.emit('mouse-leave') + + return False + + def get_toplevel(self): + return hippo.get_canvas_for_item(self._item).get_toplevel() + +class ToolInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget.child) + + def _get_alignments(self): + parent = self._widget.get_parent() + if parent is None: + return WidgetInvoker.get_alignments() + + if parent.get_orientation() is gtk.ORIENTATION_HORIZONTAL: + return self.BOTTOM + self.TOP + else: + return self.LEFT + self.RIGHT + +class _PaletteObserver(gobject.GObject): + __gtype_name__ = 'SugarPaletteObserver' + + __gsignals__ = { + 'popup': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([object])) + } + + def __init__(self): + gobject.GObject.__init__(self) + +_palette_observer = _PaletteObserver() diff --git a/sugar/graphics/palettegroup.py b/sugar/graphics/palettegroup.py new file mode 100644 index 0000000..bdae76b --- /dev/null +++ b/sugar/graphics/palettegroup.py @@ -0,0 +1,91 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gobject + +_groups = {} + +def get_group(group_id): + if _groups.has_key(group_id): + group = _groups[group_id] + else: + group = Group() + _groups[group_id] = group + + return group + +class Group(gobject.GObject): + __gsignals__ = { + 'popup' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'popdown' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + def __init__(self): + gobject.GObject.__init__(self) + self._up = False + self._palettes = [] + self._sig_ids = {} + + def is_up(self): + return self._up + + def get_state(self): + for palette in self._palettes: + if palette.is_up(): + return palette.palette_state + + return None + + def add(self, palette): + self._palettes.append(palette) + + self._sig_ids[palette] = [] + + sid = palette.connect('popup', self._palette_popup_cb) + self._sig_ids[palette].append(sid) + + sid = palette.connect('popdown', self._palette_popdown_cb) + self._sig_ids[palette].append(sid) + + def remove(self, palette): + sig_ids = self._sig_ids[palette] + for sid in sig_ids: + palette.disconnect(sid) + + self._palettes.remove(palette) + del self._sig_ids[palette] + + def popdown(self): + for palette in self._palettes: + if palette.is_up(): + palette.popdown(immediate=True) + + def _palette_popup_cb(self, palette): + if not self._up: + self.emit('popup') + self._up = True + + def _palette_popdown_cb(self, palette): + down = True + for palette in self._palettes: + if palette.is_up(): + down = False + + if down: + self._up = False + self.emit('popdown') diff --git a/sugar/graphics/panel.py b/sugar/graphics/panel.py new file mode 100644 index 0000000..bf3ed24 --- /dev/null +++ b/sugar/graphics/panel.py @@ -0,0 +1,23 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +class Panel(gtk.VBox): + __gtype_name__ = 'SugarPanel' + def __init__(self): + gtk.VBox.__init__(self) diff --git a/sugar/graphics/radiotoolbutton.py b/sugar/graphics/radiotoolbutton.py new file mode 100644 index 0000000..cb4ae25 --- /dev/null +++ b/sugar/graphics/radiotoolbutton.py @@ -0,0 +1,67 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker + +class RadioToolButton(gtk.RadioToolButton): + __gtype_name__ = "SugarRadioToolButton" + + def __init__(self, named_icon=None, group=None, xo_color=None): + gtk.RadioToolButton.__init__(self, group=group) + self._palette = None + self._xo_color = xo_color + self.set_named_icon(named_icon) + + def set_named_icon(self, named_icon): + icon = Icon(icon_name=named_icon, + xo_color=self._xo_color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self.set_icon_widget(icon) + icon.show() + + def get_palette(self): + return self._palette + + def set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + self._palette.props.invoker = ToolInvoker(self) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + + def do_expose_event(self, event): + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) + elif self.child.state == gtk.STATE_PRELIGHT: + self.child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + self.child, "toolbutton-prelight", + self.allocation.x, + self.allocation.y, + self.allocation.width, + self.allocation.height) + + gtk.RadioToolButton.do_expose_event(self, event) + + palette = property(get_palette, set_palette) diff --git a/sugar/graphics/roundbox.py b/sugar/graphics/roundbox.py new file mode 100644 index 0000000..51b9e7d --- /dev/null +++ b/sugar/graphics/roundbox.py @@ -0,0 +1,66 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import math + +import hippo + +from sugar.graphics import style + +class CanvasRoundBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarRoundBox' + + _BORDER_DEFAULT = style.LINE_WIDTH + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + # TODO: we should calculate this value depending on the height of the box. + self._radius = style.zoom(10) + + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self.props.border = self._BORDER_DEFAULT + self.props.border_left = self._radius + self.props.border_right = self._radius + self.props.border_color = style.COLOR_BLACK.get_int() + + def do_paint_background(self, cr, damaged_box): + [width, height] = self.get_allocation() + + x = self._BORDER_DEFAULT / 2 + y = self._BORDER_DEFAULT / 2 + width -= self._BORDER_DEFAULT + height -= self._BORDER_DEFAULT + + cr.move_to(x + self._radius, y); + cr.arc(x + width - self._radius, y + self._radius, + self._radius, math.pi * 1.5, math.pi * 2); + cr.arc(x + width - self._radius, x + height - self._radius, + self._radius, 0, math.pi * 0.5); + cr.arc(x + self._radius, y + height - self._radius, + self._radius, math.pi * 0.5, math.pi); + cr.arc(x + self._radius, y + self._radius, self._radius, + math.pi, math.pi * 1.5); + + hippo.cairo_set_source_rgba32(cr, self.props.background_color) + cr.fill_preserve(); + + # TODO: we should be more consistent here with the border properties. + if self.props.border_color: + hippo.cairo_set_source_rgba32(cr, self.props.border_color) + cr.set_line_width(self.props.border_top) + cr.stroke() diff --git a/sugar/graphics/style.py b/sugar/graphics/style.py new file mode 100644 index 0000000..1f97adc --- /dev/null +++ b/sugar/graphics/style.py @@ -0,0 +1,147 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +""" +All the constants are expressed in pixels. They are defined for the XO screen +and are usually adapted to different resolution by applying a zoom factor. The +factor for traditional 96 dpi screen is currently 0.72 which is the inverse +of the one we are using to adapt web pages to the XO screen. It should be +considered a reference value rather then a scale constant which has to be +automatically applied and always respected. +""" + +import os + +import gtk +import pango + +_XO_DPI = 200.0 + +_FOCUS_LINE_WIDTH = 2 +_TAB_CURVATURE = 1 + +def _get_screen_dpi(): + xft_dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') + return float(xft_dpi / 1024) + +def _compute_zoom_factor(): + if _get_screen_dpi() == 96.0: + if not os.environ.has_key('SUGAR_XO_STYLE') or \ + not os.environ['SUGAR_XO_STYLE'] == 'yes': + return 0.72 + + return 1.0 + +def _compute_font_height(font): + widget = gtk.Label('') + + context = widget.get_pango_context() + pango_font = context.load_font(font.get_pango_desc()) + metrics = pango_font.get_metrics() + + return pango.PIXELS(metrics.get_ascent() + metrics.get_descent()) + +class Font(object): + def __init__(self, desc): + self._desc = desc + + def __str__(self): + return self._desc + + def get_pango_desc(self): + return pango.FontDescription(self._desc) + +class Color(object): + def __init__(self, color, alpha=1.0): + self._r, self._g, self._b = self._html_to_rgb(color) + self._a = alpha + + def get_rgba(self): + return (self._r, self._g, self._b, self._a) + + def get_int(self): + return int(self._a * 255) + (int(self._b * 255) << 8) + \ + (int(self._g * 255) << 16) + (int(self._r * 255) << 24) + + def get_gdk_color(self): + return gtk.gdk.Color(int(self._r * 65535), int(self._g * 65535), + int(self._b * 65535)) + + def get_html(self): + return '#%02x%02x%02x' % (self._r * 255, self._g * 255, self._b * 255) + + def _html_to_rgb(self, html_color): + """ #RRGGBB -> (r, g, b) tuple (in float format) """ + + html_color = html_color.strip() + if html_color[0] == '#': + html_color = html_color[1:] + if len(html_color) != 6: + raise ValueError, "input #%s is not in #RRGGBB format" % html_color + + r, g, b = html_color[:2], html_color[2:4], html_color[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + r, g, b = (r / 255.0, g / 255.0, b / 255.0) + + return (r, g, b) + + def get_svg(self): + if self._a == 0.0: + return 'none' + else: + return self.get_html() + +def zoom(units): + return int(ZOOM_FACTOR * units) + +ZOOM_FACTOR = _compute_zoom_factor() + +DEFAULT_SPACING = zoom(15) +DEFAULT_PADDING = zoom(6) +GRID_CELL_SIZE = zoom(75) +LINE_WIDTH = zoom(2) + +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) + +FONT_SIZE = zoom(7 * _XO_DPI / _get_screen_dpi()) +FONT_NORMAL = Font('Bitstream Vera Sans %d' % FONT_SIZE) +FONT_BOLD = Font('Bitstream Vera Sans bold %d' % FONT_SIZE) +FONT_NORMAL_H = _compute_font_height(FONT_NORMAL) +FONT_BOLD_H = _compute_font_height(FONT_BOLD) + +TOOLBOX_SEPARATOR_HEIGHT = zoom(9) +TOOLBOX_HORIZONTAL_PADDING = zoom(75) +TOOLBOX_TAB_VBORDER = int((zoom(36) - FONT_NORMAL_H - _FOCUS_LINE_WIDTH) / 2) +TOOLBOX_TAB_HBORDER = zoom(15) - _FOCUS_LINE_WIDTH - _TAB_CURVATURE +TOOLBOX_TAB_LABEL_WIDTH = zoom(150 - 15 * 2) + +COLOR_BLACK = Color('#000000') +COLOR_WHITE = Color('#FFFFFF') +COLOR_TRANSPARENT = Color('#FFFFFF', alpha=0.0) +COLOR_PANEL_GREY = Color('#C0C0C0') +COLOR_SELECTION_GREY = Color('#A6A6A6') +COLOR_TOOLBAR_GREY = Color('#404040') +COLOR_BUTTON_GREY = Color('#808080') +COLOR_INACTIVE_FILL = Color('#9D9FA1') +COLOR_INACTIVE_STROKE = Color('#757575') +COLOR_TEXT_FIELD_GREY = Color('#E5E5E5') + +PALETTE_CURSOR_DISTANCE = zoom(10) diff --git a/sugar/graphics/toggletoolbutton.py b/sugar/graphics/toggletoolbutton.py new file mode 100644 index 0000000..3d05cc0 --- /dev/null +++ b/sugar/graphics/toggletoolbutton.py @@ -0,0 +1,63 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker + +class ToggleToolButton(gtk.ToggleToolButton): + __gtype_name__ = "SugarToggleToolButton" + + def __init__(self, named_icon=None): + gtk.ToggleToolButton.__init__(self) + self._palette = None + self.set_named_icon(named_icon) + + def set_named_icon(self, named_icon): + icon = Icon(icon_name=named_icon) + self.set_icon_widget(icon) + icon.show() + + def get_palette(self): + return self._palette + + def set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + self._palette.props.invoker = ToolInvoker(self) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + + def do_expose_event(self, event): + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) + elif self.child.state == gtk.STATE_PRELIGHT: + self.child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + self.child, "toolbutton-prelight", + self.allocation.x, + self.allocation.y, + self.allocation.width, + self.allocation.height) + + gtk.ToggleToolButton.do_expose_event(self, event) + + palette = property(get_palette, set_palette) diff --git a/sugar/graphics/toolbox.py b/sugar/graphics/toolbox.py new file mode 100644 index 0000000..4171d00 --- /dev/null +++ b/sugar/graphics/toolbox.py @@ -0,0 +1,97 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gobject +import hippo + +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import style + +class Toolbox(gtk.VBox): + __gtype_name__ = 'SugarToolbox' + + __gsignals__ = { + 'current-toolbar-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([int])) + } + + def __init__(self): + gtk.VBox.__init__(self) + + self._notebook = gtk.Notebook() + self._notebook.set_tab_pos(gtk.POS_BOTTOM) + self._notebook.set_show_border(False) + self._notebook.set_show_tabs(False) + self._notebook.props.tab_vborder = style.TOOLBOX_TAB_VBORDER + self._notebook.props.tab_hborder = style.TOOLBOX_TAB_HBORDER + self.pack_start(self._notebook) + self._notebook.show() + + # FIXME improve gtk.Notebook and do this in the theme + self._separator = hippo.Canvas() + box = hippo.CanvasBox( + border_color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_PANEL_GREY.get_int(), + box_height=style.TOOLBOX_SEPARATOR_HEIGHT, + border_bottom=style.LINE_WIDTH) + self._separator.set_root(box) + self.pack_start(self._separator, False) + + self._notebook.connect('notify::page', self._notify_page_cb) + + def _notify_page_cb(self, notebook, pspec): + self.emit('current-toolbar-changed', notebook.props.page) + + def add_toolbar(self, name, toolbar): + label = gtk.Label(name) + label.set_size_request(style.TOOLBOX_TAB_LABEL_WIDTH, -1) + label.set_alignment(0.0, 0.5) + + event_box = gtk.EventBox() + + alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0) + alignment.set_padding(0, 0, style.TOOLBOX_HORIZONTAL_PADDING, + style.TOOLBOX_HORIZONTAL_PADDING) + + alignment.add(toolbar) + event_box.add(alignment) + alignment.show() + event_box.show() + + self._notebook.append_page(event_box, label) + + if self._notebook.get_n_pages() > 1: + self._notebook.set_show_tabs(True) + self._separator.show() + + def remove_toolbar(self, index): + self._notebook.remove_page(index) + + if self._notebook.get_n_pages() < 2: + self._notebook.set_show_tabs(False) + self._separator.hide() + + def set_current_toolbar(self, index): + self._notebook.set_current_page(index) + + def get_current_toolbar(self): + return self._notebook.get_current_page() + + current_toolbar = property(get_current_toolbar, set_current_toolbar) + diff --git a/sugar/graphics/toolbutton.py b/sugar/graphics/toolbutton.py new file mode 100644 index 0000000..26acc83 --- /dev/null +++ b/sugar/graphics/toolbutton.py @@ -0,0 +1,74 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gobject +import time + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker + +class ToolButton(gtk.ToolButton): + __gtype_name__ = "SugarToolButton" + + def __init__(self, icon_name=None): + gtk.ToolButton.__init__(self) + self._palette = None + if icon_name: + self.set_icon(icon_name) + self.connect('clicked', self._button_clicked_cb) + + def set_icon(self, icon_name): + icon = Icon(icon_name=icon_name) + self.set_icon_widget(icon) + icon.show() + + def get_palette(self): + return self._palette + + def set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + self._palette.props.invoker = ToolInvoker(self) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + + # Set label, shows up when toolbar overflows + self.set_label(text) + + def do_expose_event(self, event): + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) + elif self.child.state == gtk.STATE_PRELIGHT: + self.child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + self.child, "toolbutton-prelight", + self.allocation.x, + self.allocation.y, + self.allocation.width, + self.allocation.height) + + gtk.ToolButton.do_expose_event(self, event) + + def _button_clicked_cb(self, widget): + if self._palette: + self._palette.popdown(True) + + palette = property(get_palette, set_palette) diff --git a/sugar/graphics/toolcombobox.py b/sugar/graphics/toolcombobox.py new file mode 100644 index 0000000..460dcee --- /dev/null +++ b/sugar/graphics/toolcombobox.py @@ -0,0 +1,59 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gobject + +from sugar.graphics.combobox import ComboBox +from sugar.graphics import style + +class ToolComboBox(gtk.ToolItem): + __gproperties__ = { + 'label-text' : (str, None, None, None, + gobject.PARAM_WRITABLE), + } + + def __init__(self, combo=None, **kwargs): + self.label = None + self._label_text = '' + + gobject.GObject.__init__(self, **kwargs) + + self.set_border_width(style.DEFAULT_PADDING) + + hbox = gtk.HBox(False, style.DEFAULT_SPACING) + + self.label = gtk.Label(self._label_text) + hbox.pack_start(self.label, False) + self.label.show() + + if combo: + self.combo = combo + else: + self.combo = ComboBox() + + hbox.pack_start(self.combo) + self.combo.show() + + self.add(hbox) + hbox.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'label-text': + self._label_text = value + if self.label: + self.label.set_text(self._label_text) diff --git a/sugar/graphics/tray.py b/sugar/graphics/tray.py new file mode 100644 index 0000000..da40501 --- /dev/null +++ b/sugar/graphics/tray.py @@ -0,0 +1,299 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import Palette, ToolInvoker +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon + +_PREVIOUS_PAGE = 0 +_NEXT_PAGE = 1 + +class _TrayViewport(gtk.Viewport): + __gproperties__ = { + 'scrollable' : (bool, None, None, False, + gobject.PARAM_READABLE), + 'can-scroll-prev' : (bool, None, None, False, + gobject.PARAM_READABLE), + 'can-scroll-next' : (bool, None, None, False, + gobject.PARAM_READABLE), + } + + def __init__(self, orientation): + self.orientation = orientation + self._scrollable = False + self._can_scroll_next = False + self._can_scroll_prev = False + + gobject.GObject.__init__(self) + + self.set_shadow_type(gtk.SHADOW_NONE) + + self.traybar = gtk.Toolbar() + self.traybar.set_orientation(orientation) + self.traybar.set_show_arrow(False) + self.add(self.traybar) + self.traybar.show() + + self.connect('size_allocate', self._size_allocate_cb) + + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + else: + adj = self.get_vadjustment() + adj.connect('changed', self._adjustment_changed_cb) + adj.connect('value-changed', self._adjustment_changed_cb) + + def scroll(self, direction): + if direction == _PREVIOUS_PAGE: + self._scroll_previous() + elif direction == _NEXT_PAGE: + self._scroll_next() + + def _scroll_next(self): + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + new_value = adj.value + self.allocation.width + adj.value = min(new_value, adj.upper - self.allocation.width) + else: + adj = self.get_vadjustment() + new_value = adj.value + self.allocation.height + adj.value = min(new_value, adj.upper - self.allocation.height) + + def _scroll_previous(self): + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + new_value = adj.value - self.allocation.width + adj.value = max(adj.lower, new_value) + else: + adj = self.get_vadjustment() + new_value = adj.value - self.allocation.height + adj.value = max(adj.lower, new_value) + + def do_size_request(self, requisition): + child_requisition = self.child.size_request() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + requisition[0] = 0 + requisition[1] = child_requisition[1] + else: + requisition[0] = child_requisition[0] + requisition[1] = 0 + + def do_get_property(self, pspec): + if pspec.name == 'scrollable': + return self._scrollable + elif pspec.name == 'can-scroll-next': + return self._can_scroll_next + elif pspec.name == 'can-scroll-prev': + return self._can_scroll_prev + + def _size_allocate_cb(self, viewport, allocation): + bar_requisition = self.traybar.get_child_requisition() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + scrollable = bar_requisition[0] > allocation.width + else: + scrollable = bar_requisition[1] > allocation.height + + if scrollable != self._scrollable: + self._scrollable = scrollable + self.notify('scrollable') + + def _adjustment_changed_cb(self, adjustment): + if adjustment.value <= adjustment.lower: + can_scroll_prev = False + else: + can_scroll_prev = True + + if adjustment.value + adjustment.page_size >= adjustment.upper: + can_scroll_next = False + else: + can_scroll_next = True + + if can_scroll_prev != self._can_scroll_prev: + self._can_scroll_prev = can_scroll_prev + self.notify('can-scroll-prev') + + if can_scroll_next != self._can_scroll_next: + self._can_scroll_next = can_scroll_next + self.notify('can-scroll-next') + + +class _TrayScrollButton(ToolButton): + def __init__(self, icon_name, scroll_direction): + ToolButton.__init__(self) + self._viewport = None + + self._scroll_direction = scroll_direction + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + self.icon = Icon(icon_name = icon_name, + icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR) + # The alignment is a hack to work around gtk.ToolButton code + # that sets the icon_size when the icon_widget is a gtk.Image + alignment = gtk.Alignment(0.5, 0.5) + alignment.add(self.icon) + self.set_icon_widget(alignment) + alignment.show_all() + + self.connect('clicked', self._clicked_cb) + + def set_viewport(self, viewport): + self._viewport = viewport + self._viewport.connect('notify::scrollable', + self._viewport_scrollable_changed_cb) + + if self._scroll_direction == _PREVIOUS_PAGE: + self._viewport.connect('notify::can-scroll-prev', + self._viewport_can_scroll_dir_changed_cb) + self.set_sensitive(self._viewport.props.can_scroll_prev) + else: + self._viewport.connect('notify::can-scroll-next', + self._viewport_can_scroll_dir_changed_cb) + self.set_sensitive(self._viewport.props.can_scroll_next) + + + def _viewport_scrollable_changed_cb(self, viewport, pspec): + self.props.visible = self._viewport.props.scrollable + + def _viewport_can_scroll_dir_changed_cb(self, viewport, pspec): + if self._scroll_direction == _PREVIOUS_PAGE: + sensitive = self._viewport.props.can_scroll_prev + else: + sensitive = self._viewport.props.can_scroll_next + + self.set_sensitive(sensitive) + + def _clicked_cb(self, button): + self._viewport.scroll(self._scroll_direction) + + viewport = property(fset=set_viewport) + +class HTray(gtk.HBox): + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + self.set_direction(gtk.TEXT_DIR_LTR) + + scroll_left = _TrayScrollButton('go-left', _PREVIOUS_PAGE) + self.pack_start(scroll_left, False) + + self._viewport = _TrayViewport(gtk.ORIENTATION_HORIZONTAL) + self.pack_start(self._viewport) + self._viewport.show() + + scroll_right = _TrayScrollButton('go-right', _NEXT_PAGE) + self.pack_start(scroll_right, False) + + scroll_left.viewport = self._viewport + scroll_right.viewport = self._viewport + + def get_children(self): + return self._viewport.traybar.get_children() + + def add_item(self, item, index=-1): + self._viewport.traybar.insert(item, index) + + def remove_item(self, item): + self._viewport.traybar.remove(item) + + def get_item_index(self, item): + return self._viewport.traybar.get_item_index(item) + +class VTray(gtk.VBox): + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + + # FIXME we need a go-up icon + scroll_left = _TrayScrollButton('go-left', _PREVIOUS_PAGE) + self.pack_start(scroll_left, False) + + self._viewport = _TrayViewport(gtk.ORIENTATION_VERTICAL) + self.pack_start(self._viewport) + self._viewport.show() + + # FIXME we need a go-down icon + scroll_right = _TrayScrollButton('go-right', _NEXT_PAGE) + self.pack_start(scroll_right, False) + + scroll_left.viewport = self._viewport + scroll_right.viewport = self._viewport + + def get_children(self): + return self._viewport.traybar.get_children() + + def add_item(self, item, index=-1): + self._viewport.traybar.insert(item, index) + + def remove_item(self, item): + self._viewport.traybar.remove(item) + + def get_item_index(self, item): + return self._viewport.traybar.get_item_index(item) + +class TrayButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, **kwargs) + +class _IconWidget(gtk.EventBox): + __gtype_name__ = "SugarTrayIconWidget" + + def __init__(self, icon_name=None, xo_color=None): + gtk.EventBox.__init__(self) + + self._palette = None + + self.set_app_paintable(True) + + icon = Icon(icon_name=icon_name, xo_color=xo_color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self.add(icon) + icon.show() + + def do_expose_event(self, event): + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) + + gtk.EventBox.do_expose_event(self, event) + + def set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + self._palette.props.invoker = ToolInvoker(self) + +class TrayIcon(gtk.ToolItem): + __gtype_name__ = "SugarTrayIcon" + + def __init__(self, icon_name=None, xo_color=None): + gtk.ToolItem.__init__(self) + + self._icon_widget = _IconWidget(icon_name, xo_color) + self.add(self._icon_widget) + self._icon_widget.show() + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + def set_palette(self, palette): + self._icon_widget.set_palette(palette) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + diff --git a/sugar/graphics/window.py b/sugar/graphics/window.py new file mode 100644 index 0000000..3189400 --- /dev/null +++ b/sugar/graphics/window.py @@ -0,0 +1,220 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gobject +import gtk +import logging + +from sugar.graphics.icon import Icon + +class UnfullscreenButton(gtk.Window): + + def __init__(self): + gtk.Window.__init__(self) + + self.set_decorated(False) + self.set_resizable(False) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + + self.set_border_width(0) + + self.props.accept_focus = False + + #Setup estimate of width, height + w, h = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR) + self._width = w + self._height = h + + self.connect('size-request', self._size_request_cb) + + screen = self.get_screen() + screen.connect('size-changed', self._screen_size_changed_cb) + + self._button = gtk.Button() + self._button.set_relief(gtk.RELIEF_NONE) + + self._icon = Icon(icon_name='view-return', + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self._icon.show() + self._button.add(self._icon) + + self._button.show() + self.add(self._button) + + def connect_button_press(self, cb): + self._button.connect('button-press-event', cb) + + def _reposition(self): + x = gtk.gdk.screen_width() - self._width + self.move(x, 0) + + def _size_request_cb(self, widget, req): + self._width = req.width + self._height = req.height + self._reposition() + + def _screen_size_changed_cb(self, screen): + self._reposition() + +class Window(gtk.Window): + + __gproperties__ = { + 'enable-fullscreen-mode': (bool, None, None, True, + gobject.PARAM_READWRITE), + } + + def __init__(self, **args): + self._enable_fullscreen_mode = True + + gtk.Window.__init__(self, **args) + + self.connect('realize', self.__window_realize_cb) + self.connect('window-state-event', self.__window_state_event_cb) + self.connect('key-press-event', self.__key_press_cb) + + self.toolbox = None + self._alerts = [] + self.canvas = None + self.tray = None + + self._vbox = gtk.VBox() + self._hbox = gtk.HBox() + self._vbox.pack_start(self._hbox) + self._hbox.show() + + self._event_box = gtk.EventBox() + self._hbox.pack_start(self._event_box) + self._event_box.show() + + self.add(self._vbox) + self._vbox.show() + + self._is_fullscreen = False + self._unfullscreen_button = UnfullscreenButton() + self._unfullscreen_button.set_transient_for(self) + self._unfullscreen_button.connect_button_press( + self.__unfullscreen_button_pressed) + + def do_get_property(self, prop): + if prop.name == 'enable-fullscreen-mode': + return self._enable_fullscreen_mode + else: + return gtk.Window.do_get_property(self, prop) + + def do_set_property(self, prop, val): + if prop.name == 'enable-fullscreen-mode': + self._enable_fullscreen_mode = val + else: + gtk.Window.do_set_property(self, prop, val) + + def set_canvas(self, canvas): + if self.canvas: + self._event_box.remove(self.canvas) + + if canvas: + self._event_box.add(canvas) + + self.canvas = canvas + + def set_toolbox(self, toolbox): + if self.toolbox: + self._vbox.remove(self.toolbox) + + self._vbox.pack_start(toolbox, False) + self._vbox.reorder_child(toolbox, 0) + + self.toolbox = toolbox + + def set_tray(self, tray, position): + if self.tray: + box = self.tray.get_parent() + box.remove(self.tray) + + if position == gtk.POS_LEFT: + self._hbox.pack_start(tray, False) + elif position == gtk.POS_RIGHT: + self._hbox.pack_end(tray, False) + elif position == gtk.POS_BOTTOM: + self._vbox.pack_end(tray, False) + + self.tray = tray + + def add_alert(self, alert): + self._alerts.append(alert) + if len(self._alerts) == 1: + self._vbox.pack_start(alert, False) + if self.toolbox is not None: + self._vbox.reorder_child(alert, 1) + else: + self._vbox.reorder_child(alert, 0) + + def remove_alert(self, alert): + if alert in self._alerts: + self._alerts.remove(alert) + # if the alert is the visible one on top of the queue + if alert.get_parent() is not None: + self._vbox.remove(alert) + if len(self._alerts) >= 1: + self._vbox.pack_start(self._alerts[0], False) + if self.toolbox is not None: + self._vbox.reorder_child(self._alerts[0], 1) + else: + self._vbox.reorder_child(self._alert[0], 0) + + def __window_realize_cb(self, window): + group = gtk.Window() + group.realize() + window.window.set_group(group.window) + + def __window_state_event_cb(self, window, event): + if not (event.changed_mask & gtk.gdk.WINDOW_STATE_FULLSCREEN): + return False + + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + if self.toolbox is not None: + self.toolbox.hide() + if self.tray is not None: + self.tray.hide() + + self._is_fullscreen = True + if self.props.enable_fullscreen_mode: + self._unfullscreen_button.show() + + else: + if self.toolbox is not None: + self.toolbox.show() + if self.tray is not None: + self.tray.show() + + self._is_fullscreen = False + if self.props.enable_fullscreen_mode: + self._unfullscreen_button.hide() + + def __key_press_cb(self, widget, event): + key = gtk.gdk.keyval_name(event.keyval) + if event.state & gtk.gdk.MOD1_MASK: + if key == 'space': + self.tray.props.visible = not self.tray.props.visible + return True + elif key == 'Escape' and self._is_fullscreen and \ + self.props.enable_fullscreen_mode: + self.unfullscreen() + return True + return False + + def __unfullscreen_button_pressed(self, widget, event): + self.unfullscreen() diff --git a/sugar/graphics/xocolor.py b/sugar/graphics/xocolor.py new file mode 100644 index 0000000..d37eab1 --- /dev/null +++ b/sugar/graphics/xocolor.py @@ -0,0 +1,255 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import random + +_colors = [ +['#B20008', '#FF2B34'], \ +['#FF2B34', '#B20008'], \ +['#E6000A', '#FF2B34'], \ +['#FF2B34', '#E6000A'], \ +['#FFADCE', '#FF2B34'], \ +['#9A5200', '#FF2B34'], \ +['#FF2B34', '#9A5200'], \ +['#FF8F00', '#FF2B34'], \ +['#FF2B34', '#FF8F00'], \ +['#FFC169', '#FF2B34'], \ +['#807500', '#FF2B34'], \ +['#FF2B34', '#807500'], \ +['#BE9E00', '#FF2B34'], \ +['#FF2B34', '#BE9E00'], \ +['#F8E800', '#FF2B34'], \ +['#008009', '#FF2B34'], \ +['#FF2B34', '#008009'], \ +['#00B20D', '#FF2B34'], \ +['#FF2B34', '#00B20D'], \ +['#8BFF7A', '#FF2B34'], \ +['#00588C', '#FF2B34'], \ +['#FF2B34', '#00588C'], \ +['#005FE4', '#FF2B34'], \ +['#FF2B34', '#005FE4'], \ +['#BCCDFF', '#FF2B34'], \ +['#5E008C', '#FF2B34'], \ +['#FF2B34', '#5E008C'], \ +['#7F00BF', '#FF2B34'], \ +['#FF2B34', '#7F00BF'], \ +['#D1A3FF', '#FF2B34'], \ +['#9A5200', '#FF8F00'], \ +['#FF8F00', '#9A5200'], \ +['#C97E00', '#FF8F00'], \ +['#FF8F00', '#C97E00'], \ +['#FFC169', '#FF8F00'], \ +['#807500', '#FF8F00'], \ +['#FF8F00', '#807500'], \ +['#BE9E00', '#FF8F00'], \ +['#FF8F00', '#BE9E00'], \ +['#F8E800', '#FF8F00'], \ +['#008009', '#FF8F00'], \ +['#FF8F00', '#008009'], \ +['#00B20D', '#FF8F00'], \ +['#FF8F00', '#00B20D'], \ +['#8BFF7A', '#FF8F00'], \ +['#00588C', '#FF8F00'], \ +['#FF8F00', '#00588C'], \ +['#005FE4', '#FF8F00'], \ +['#FF8F00', '#005FE4'], \ +['#BCCDFF', '#FF8F00'], \ +['#5E008C', '#FF8F00'], \ +['#FF8F00', '#5E008C'], \ +['#A700FF', '#FF8F00'], \ +['#FF8F00', '#A700FF'], \ +['#D1A3FF', '#FF8F00'], \ +['#B20008', '#FF8F00'], \ +['#FF8F00', '#B20008'], \ +['#FF2B34', '#FF8F00'], \ +['#FF8F00', '#FF2B34'], \ +['#FFADCE', '#FF8F00'], \ +['#807500', '#F8E800'], \ +['#F8E800', '#807500'], \ +['#BE9E00', '#F8E800'], \ +['#F8E800', '#BE9E00'], \ +['#FFFA00', '#EDDE00'], \ +['#008009', '#F8E800'], \ +['#F8E800', '#008009'], \ +['#00EA11', '#F8E800'], \ +['#F8E800', '#00EA11'], \ +['#8BFF7A', '#F8E800'], \ +['#00588C', '#F8E800'], \ +['#F8E800', '#00588C'], \ +['#00A0FF', '#F8E800'], \ +['#F8E800', '#00A0FF'], \ +['#BCCEFF', '#F8E800'], \ +['#5E008C', '#F8E800'], \ +['#F8E800', '#5E008C'], \ +['#AC32FF', '#F8E800'], \ +['#F8E800', '#AC32FF'], \ +['#D1A3FF', '#F8E800'], \ +['#B20008', '#F8E800'], \ +['#F8E800', '#B20008'], \ +['#FF2B34', '#F8E800'], \ +['#F8E800', '#FF2B34'], \ +['#FFADCE', '#F8E800'], \ +['#9A5200', '#F8E800'], \ +['#F8E800', '#9A5200'], \ +['#FF8F00', '#F8E800'], \ +['#F8E800', '#FF8F00'], \ +['#FFC169', '#F8E800'], \ +['#008009', '#00EA11'], \ +['#00EA11', '#008009'], \ +['#00B20D', '#00EA11'], \ +['#00EA11', '#00B20D'], \ +['#8BFF7A', '#00EA11'], \ +['#00588C', '#00EA11'], \ +['#00EA11', '#00588C'], \ +['#005FE4', '#00EA11'], \ +['#00EA11', '#005FE4'], \ +['#BCCDFF', '#00EA11'], \ +['#5E008C', '#00EA11'], \ +['#00EA11', '#5E008C'], \ +['#7F00BF', '#00EA11'], \ +['#00EA11', '#7F00BF'], \ +['#D1A3FF', '#00EA11'], \ +['#B20008', '#00EA11'], \ +['#00EA11', '#B20008'], \ +['#FF2B34', '#00EA11'], \ +['#00EA11', '#FF2B34'], \ +['#FFADCE', '#00EA11'], \ +['#9A5200', '#00EA11'], \ +['#00EA11', '#9A5200'], \ +['#FF8F00', '#00EA11'], \ +['#00EA11', '#FF8F00'], \ +['#FFC169', '#00EA11'], \ +['#807500', '#00EA11'], \ +['#00EA11', '#807500'], \ +['#BE9E00', '#00EA11'], \ +['#00EA11', '#BE9E00'], \ +['#F8E800', '#00EA11'], \ +['#00588C', '#00A0FF'], \ +['#00A0FF', '#00588C'], \ +['#005FE4', '#00A0FF'], \ +['#00A0FF', '#005FE4'], \ +['#BCCDFF', '#00A0FF'], \ +['#5E008C', '#00A0FF'], \ +['#00A0FF', '#5E008C'], \ +['#9900E6', '#00A0FF'], \ +['#00A0FF', '#9900E6'], \ +['#D1A3FF', '#00A0FF'], \ +['#B20008', '#00A0FF'], \ +['#00A0FF', '#B20008'], \ +['#FF2B34', '#00A0FF'], \ +['#00A0FF', '#FF2B34'], \ +['#FFADCE', '#00A0FF'], \ +['#9A5200', '#00A0FF'], \ +['#00A0FF', '#9A5200'], \ +['#FF8F00', '#00A0FF'], \ +['#00A0FF', '#FF8F00'], \ +['#FFC169', '#00A0FF'], \ +['#807500', '#00A0FF'], \ +['#00A0FF', '#807500'], \ +['#BE9E00', '#00A0FF'], \ +['#00A0FF', '#BE9E00'], \ +['#F8E800', '#00A0FF'], \ +['#008009', '#00A0FF'], \ +['#00A0FF', '#008009'], \ +['#00B20D', '#00A0FF'], \ +['#00A0FF', '#00B20D'], \ +['#8BFF7A', '#00A0FF'], \ +['#5E008C', '#AC32FF'], \ +['#AC32FF', '#5E008C'], \ +['#7F00BF', '#AC32FF'], \ +['#AC32FF', '#7F00BF'], \ +['#D1A3FF', '#AC32FF'], \ +['#B20008', '#AC32FF'], \ +['#AC32FF', '#B20008'], \ +['#FF2B34', '#AC32FF'], \ +['#AC32FF', '#FF2B34'], \ +['#FFADCE', '#AC32FF'], \ +['#9A5200', '#AC32FF'], \ +['#AC32FF', '#9A5200'], \ +['#FF8F00', '#AC32FF'], \ +['#AC32FF', '#FF8F00'], \ +['#FFC169', '#AC32FF'], \ +['#807500', '#AC32FF'], \ +['#AC32FF', '#807500'], \ +['#BE9E00', '#AC32FF'], \ +['#AC32FF', '#BE9E00'], \ +['#F8E800', '#AC32FF'], \ +['#008009', '#AC32FF'], \ +['#AC32FF', '#008009'], \ +['#00B20D', '#AC32FF'], \ +['#AC32FF', '#00B20D'], \ +['#8BFF7A', '#AC32FF'], \ +['#00588C', '#AC32FF'], \ +['#AC32FF', '#00588C'], \ +['#005FE4', '#AC32FF'], \ +['#AC32FF', '#005FE4'], \ +['#BCCDFF', '#AC32FF'], \ +] + +def _parse_string(color_string): + if color_string == 'white': + return ['#ffffff', '#414141'] + elif color_string == 'insensitive': + return ['#ffffff', '#e2e2e2'] + + splitted = color_string.split(',') + if len(splitted) == 2: + return [splitted[0], splitted[1]] + else: + return None + +def is_valid(color_string): + return (_parse_string(color_string) != None) + +class XoColor: + def __init__(self, color_string=None): + if color_string == None or not is_valid(color_string): + n = int(random.random() * (len(_colors) - 1)) + [self._stroke, self._fill] = _colors[n] + else: + [self._stroke, self._fill] = _parse_string(color_string) + + def __cmp__(self, other): + if isinstance(other, XoColor): + if self._stroke == other._stroke and self._fill == other._fill: + return 0 + return -1 + + def get_stroke_color(self): + return self._stroke + + def get_fill_color(self): + return self._fill + + def to_string(self): + return '%s,%s' % (self._stroke, self._fill) + +if __name__ == "__main__": + import sys + import re + + f = open(sys.argv[1], 'r') + + print '_colors = [' + + for line in f.readlines(): + match = re.match(r'fill: ([A-Z0-9]*) stroke: ([A-Z0-9]*)', line) + print "['#%s', '#%s'], \\" % (match.group(2), match.group(1)) + + print ']' + + f.close() diff --git a/sugar/network.py b/sugar/network.py new file mode 100644 index 0000000..49d4882 --- /dev/null +++ b/sugar/network.py @@ -0,0 +1,575 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# pylint: disable-msg = W0221 + +import socket +import os +import threading +import traceback +import xmlrpclib +import sys +import httplib +import urllib +import fcntl +import tempfile + +import gobject +import SimpleXMLRPCServer +import SimpleHTTPServer +import SocketServer + + +__authinfos = {} + +def _add_authinfo(authinfo): + __authinfos[threading.currentThread()] = authinfo + +def get_authinfo(): + return __authinfos.get(threading.currentThread()) + +def _del_authinfo(): + del __authinfos[threading.currentThread()] + + +class GlibTCPServer(SocketServer.TCPServer): + """GlibTCPServer + + Integrate socket accept into glib mainloop. + """ + + allow_reuse_address = True + request_queue_size = 20 + + def __init__(self, server_address, RequestHandlerClass): + SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass) + self.socket.setblocking(0) # Set nonblocking + + # Watch the listener socket for data + gobject.io_add_watch(self.socket, gobject.IO_IN, self._handle_accept) + + def _handle_accept(self, source, condition): + """Process incoming data on the server's socket by doing an accept() + via handle_request().""" + if not (condition & gobject.IO_IN): + return True + self.handle_request() + return True + + def close_request(self, request): + """Called to clean up an individual request.""" + # let the request be closed by the request handler when its done + pass + + +class ChunkedGlibHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + """RequestHandler class that integrates with Glib mainloop. It writes + the specified file to the client in chunks, returning control to the + mainloop between chunks. + """ + + CHUNK_SIZE = 4096 + + def __init__(self, request, client_address, server): + self._file = None + self._srcid = 0 + SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, request, client_address, server) + + def log_request(self, code='-', size='-'): + pass + + def do_GET(self): + """Serve a GET request.""" + self._file = self.send_head() + if self._file: + self._srcid = gobject.io_add_watch(self.wfile, gobject.IO_OUT | gobject.IO_ERR, self._send_next_chunk) + else: + self._file.close() + self._cleanup() + + def _send_next_chunk(self, source, condition): + if condition & gobject.IO_ERR: + self._cleanup() + return False + if not (condition & gobject.IO_OUT): + self._cleanup() + return False + data = self._file.read(self.CHUNK_SIZE) + count = os.write(self.wfile.fileno(), data) + if count != len(data) or len(data) != self.CHUNK_SIZE: + self._cleanup() + return False + return True + + def _cleanup(self): + if self._file: + self._file.close() + self._file = None + if self._srcid > 0: + gobject.source_remove(self._srcid) + self._srcid = 0 + if not self.wfile.closed: + self.wfile.flush() + self.wfile.close() + self.rfile.close() + + def finish(self): + """Close the sockets when we're done, not before""" + pass + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + ** [dcbw] modified to send Content-disposition filename too + """ + path = self.translate_path(self.path) + if not path or not os.path.exists(path): + self.send_error(404, "File not found") + return None + + f = None + if os.path.isdir(path): + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(os.fstat(f.fileno())[6])) + self.send_header("Content-Disposition", 'attachment; filename="%s"' % os.path.basename(path)) + self.end_headers() + return f + +class GlibURLDownloader(gobject.GObject): + """Grabs a URL in chunks, returning to the mainloop after each chunk""" + + __gsignals__ = { + 'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + CHUNK_SIZE = 4096 + + def __init__(self, url, destdir=None): + self._url = url + if not destdir: + destdir = tempfile.gettempdir() + self._destdir = destdir + self._srcid = 0 + self._fname = None + self._outf = None + self._written = 0 + gobject.GObject.__init__(self) + + def start(self, destfile=None, destfd=None): + self._info = urllib.urlopen(self._url) + self._outf = None + self._fname = None + if destfd and not destfile: + raise ValueError("Must provide destination file too when specifying file descriptor") + if destfile: + self._suggested_fname = os.path.basename(destfile) + self._fname = os.path.abspath(os.path.expanduser(destfile)) + if destfd: + # Use the user-supplied destination file descriptor + self._outf = destfd + else: + self._outf = os.open(self._fname, os.O_RDWR | os.O_TRUNC | os.O_CREAT, 0644) + else: + self._suggested_fname = self._get_filename_from_headers(self._info.headers) + garbage, path = urllib.splittype(self._url) + garbage, path = urllib.splithost(path or "") + path, garbage = urllib.splitquery(path or "") + path, garbage = urllib.splitattr(path or "") + suffix = os.path.splitext(path)[1] + (self._outf, self._fname) = tempfile.mkstemp(suffix=suffix, dir=self._destdir) + + fcntl.fcntl(self._info.fp.fileno(), fcntl.F_SETFD, os.O_NDELAY) + self._srcid = gobject.io_add_watch(self._info.fp.fileno(), + gobject.IO_IN | gobject.IO_ERR, + self._read_next_chunk) + + def cancel(self): + if self._srcid == 0: + raise RuntimeError("Download already canceled or stopped") + self.cleanup(remove=True) + + def _get_filename_from_headers(self, headers): + if not headers.has_key("Content-Disposition"): + return None + + ftag = "filename=" + data = headers["Content-Disposition"] + fidx = data.find(ftag) + if fidx < 0: + return None + fname = data[fidx+len(ftag):] + if fname[0] == '"' or fname[0] == "'": + fname = fname[1:] + if fname[len(fname)-1] == '"' or fname[len(fname)-1] == "'": + fname = fname[:len(fname)-1] + return fname + + def _read_next_chunk(self, source, condition): + if condition & gobject.IO_ERR: + self.cleanup(remove=True) + self.emit("error", "Error downloading file.") + return False + elif not (condition & gobject.IO_IN): + # shouldn't get here, but... + return True + + try: + data = self._info.fp.read(self.CHUNK_SIZE) + count = os.write(self._outf, data) + self._written += len(data) + + # error writing data to file? + if count < len(data): + self.cleanup(remove=True) + self.emit("error", "Error writing to download file.") + return False + + self.emit("progress", self._written) + + # done? + if len(data) < self.CHUNK_SIZE: + self.cleanup() + self.emit("finished", self._fname, self._suggested_fname) + return False + except Exception, err: + self.cleanup(remove=True) + self.emit("error", "Error downloading file: %s" % err) + return False + return True + + def cleanup(self, remove=False): + if self._srcid > 0: + gobject.source_remove(self._srcid) + self._srcid = 0 + del self._info + self._info = None + os.close(self._outf) + if remove: + os.remove(self._fname) + self._outf = None + + +class GlibXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + """ GlibXMLRPCRequestHandler + + The stock SimpleXMLRPCRequestHandler and server don't allow any way to pass + the client's address and/or SSL certificate into the function that actually + _processes_ the request. So we have to store it in a thread-indexed dict. + """ + + def do_POST(self): + _add_authinfo(self.client_address) + try: + SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.do_POST(self) + except socket.timeout: + pass + except socket.error, e: + print "Error (%s): socket error - '%s'" % (self.client_address, e) + except: + print "Error while processing POST:" + traceback.print_exc() + _del_authinfo() + +class GlibXMLRPCServer(GlibTCPServer, SimpleXMLRPCServer.SimpleXMLRPCDispatcher): + """GlibXMLRPCServer + + Use nonblocking sockets and handle the accept via glib rather than + blocking on accept(). + """ + + def __init__(self, addr, requestHandler=GlibXMLRPCRequestHandler, + logRequests=0, allow_none=False): + self.logRequests = logRequests + if sys.version_info[:3] >= (2, 5, 0): + SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding="utf-8") + else: + SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self) + GlibTCPServer.__init__(self, addr, requestHandler) + + def _marshaled_dispatch(self, data, dispatch_method = None): + """Dispatches an XML-RPC method from marshalled (XML) data. + + XML-RPC methods are dispatched from the marshalled (XML) data + using the _dispatch method and the result is returned as + marshalled data. For backwards compatibility, a dispatch + function can be provided as an argument (see comment in + SimpleXMLRPCRequestHandler.do_POST) but overriding the + existing method through subclassing is the prefered means + of changing method dispatch behavior. + """ + + params, method = xmlrpclib.loads(data) + + # generate response + try: + if dispatch_method is not None: + response = dispatch_method(method, params) + else: + response = self._dispatch(method, params) + # wrap response in a singleton tuple + response = (response,) + response = xmlrpclib.dumps(response, methodresponse=1) + except xmlrpclib.Fault, fault: + response = xmlrpclib.dumps(fault) + except: + print "Exception while processing request:" + traceback.print_exc() + + # report exception back to server + response = xmlrpclib.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + + return response + + +class GlibHTTP(httplib.HTTP): + """Subclass HTTP so we can return it's connection class' socket.""" + def connect(self, host=None, port=None): + httplib.HTTP.connect(self, host, port) + self._conn.sock.setblocking(0) + +class GlibXMLRPCTransport(xmlrpclib.Transport): + """Integrate the request with the glib mainloop rather than blocking.""" + ## + # Connect to server. + # + # @param host Target host. + # @return A connection handle. + + def __init__(self, use_datetime=0): + if sys.version_info[:3] >= (2, 5, 0): + xmlrpclib.Transport.__init__(self, use_datetime) + + def make_connection(self, host): + """Use our own connection object so we can get its socket.""" + # create a HTTP connection object from a host descriptor + host, extra_headers, x509 = self.get_host_info(host) + return GlibHTTP(host) + + ## + # Send a complete request, and parse the response. + # + # @param host Target host. + # @param handler Target PRC handler. + # @param request_body XML-RPC request body. + # @param verbose Debugging flag. + # @return Parsed response. + + def start_request(self, host, handler, request_body, verbose=0, reply_handler=None, error_handler=None, user_data=None): + """Do the first half of the request by sending data to the remote + server. The bottom half bits get run when the remote server's response + actually comes back.""" + # issue XML-RPC request + + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + # Schedule a GIOWatch so we don't block waiting for the response + gobject.io_add_watch(h._conn.sock, gobject.IO_IN, self._finish_request, + h, host, handler, verbose, reply_handler, error_handler, user_data) + + def _finish_request(self, source, condition, h, host, handler, verbose, reply_handler=None, error_handler=None, user_data=None): + """Parse and return response when the remote server actually returns it.""" + if not (condition & gobject.IO_IN): + return True + + try: + errcode, errmsg, headers = h.getreply() + except socket.error, err: + if err[0] != 104: + raise socket.error(err) + else: + if error_handler: + gobject.idle_add(error_handler, err, user_data) + return False + + if errcode != 200: + raise xmlrpclib.ProtocolError(host + handler, errcode, errmsg, headers) + self.verbose = verbose + response = self._parse_response(h.getfile(), h._conn.sock) + if reply_handler: + # Coerce to a list so we can append user data + response = response[0] + if not isinstance(response, list): + response = [response] + response.append(user_data) + gobject.idle_add(reply_handler, *response) + return False + +class _Method: + """Right, so python people thought it would be funny to make this + class private to xmlrpclib.py...""" + # some magic to bind an XML-RPC method to an RPC server. + # supports "nested" methods (e.g. examples.getStateName) + def __init__(self, send, name): + self.__send = send + self.__name = name + def __getattr__(self, name): + return _Method(self.__send, "%s.%s" % (self.__name, name)) + def __call__(self, *args, **kwargs): + return self.__send(self.__name, *args, **kwargs) + + +class GlibServerProxy(xmlrpclib.ServerProxy): + """Subclass xmlrpclib.ServerProxy so we can run the XML-RPC request + in two parts, integrated with the glib mainloop, such that we don't + block anywhere. + + Using this object is somewhat special; it requires more arguments to each + XML-RPC request call than the normal xmlrpclib.ServerProxy object: + + client = GlibServerProxy("http://127.0.0.1:8888") + user_data = "bar" + xmlrpc_arg1 = "test" + xmlrpc_arg2 = "foo" + client.test(xmlrpc_test_cb, user_data, xmlrpc_arg1, xmlrpc_arg2) + + Here, 'xmlrpc_test_cb' is the callback function, which has the following + signature: + + def xmlrpc_test_cb(result_status, response, user_data=None): + ... + """ + def __init__(self, uri, encoding=None, verbose=0, allow_none=0): + self._transport = GlibXMLRPCTransport() + self._encoding = encoding + self._verbose = verbose + self._allow_none = allow_none + xmlrpclib.ServerProxy.__init__(self, uri, self._transport, encoding, verbose, allow_none) + + # get the url + import urllib + urltype, uri = urllib.splittype(uri) + if urltype not in ("http", "https"): + raise IOError, "unsupported XML-RPC protocol" + self._host, self._handler = urllib.splithost(uri) + if not self._handler: + self._handler = "/RPC2" + + def __request(self, methodname, *args, **kwargs): + """Call the method on the remote server. We just start the request here + and the transport itself takes care of scheduling the response callback + when the remote server returns the response. We don't want to block anywhere.""" + + request = xmlrpclib.dumps(args, methodname, encoding=self._encoding, + allow_none=self._allow_none) + + reply_hdl = kwargs.get("reply_handler") + err_hdl = kwargs.get("error_handler") + udata = kwargs.get("user_data") + try: + response = self._transport.start_request( + self._host, + self._handler, + request, + verbose=self._verbose, + reply_handler=reply_hdl, + error_handler=err_hdl, + user_data=udata + ) + except socket.error, exc: + if err_hdl: + gobject.idle_add(err_hdl, exc, udata) + + def __getattr__(self, name): + # magic method dispatcher + return _Method(self.__request, name) + + +class Test(object): + def test(self, arg1, arg2): + print "Request got %s, %s" % (arg1, arg2) + return "success", "bork" + +def xmlrpc_success_cb(response, resp2, loop): + print "Response was %s %s" % (response, resp2) + loop.quit() + +def xmlrpc_error_cb(err, loop): + print "Error: %s" % err + loop.quit() + +def xmlrpc_test(loop): + client = GlibServerProxy("http://127.0.0.1:8888") + client.test("bar", "baz", + reply_handler=xmlrpc_success_cb, + error_handler=xmlrpc_error_cb, + user_data=loop) + +def start_xmlrpc(): + server = GlibXMLRPCServer(("", 8888)) + inst = Test() + server.register_instance(inst) + gobject.idle_add(xmlrpc_test, loop) + +class TestReqHandler(ChunkedGlibHTTPRequestHandler): + def translate_path(self, path): + return "/tmp/foo" + +def start_http(): + server = GlibTCPServer(("", 8890), TestReqHandler) + +def main(): + loop = gobject.MainLoop() +# start_xmlrpc() + start_http() + try: + loop.run() + except KeyboardInterrupt: + print 'Ctrl+C pressed, exiting...' + print "Done." + +if __name__ == "__main__": + main() + + diff --git a/sugar/presence/Makefile.am b/sugar/presence/Makefile.am new file mode 100644 index 0000000..cb52a41 --- /dev/null +++ b/sugar/presence/Makefile.am @@ -0,0 +1,8 @@ +sugardir = $(pythondir)/sugar/presence +sugar_PYTHON = \ + __init__.py \ + activity.py \ + buddy.py \ + tubeconn.py \ + presenceservice.py + diff --git a/sugar/presence/__init__.py b/sugar/presence/__init__.py new file mode 100644 index 0000000..3834ab2 --- /dev/null +++ b/sugar/presence/__init__.py @@ -0,0 +1,24 @@ +"""Client-code's interface to the PresenceService + +Provides a simplified API for accessing the dbus service +which coordinates native network presence and sharing +information. This includes both "buddies" and "shared +activities". +""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. diff --git a/sugar/presence/activity.py b/sugar/presence/activity.py new file mode 100644 index 0000000..b0110a0 --- /dev/null +++ b/sugar/presence/activity.py @@ -0,0 +1,407 @@ +"""UI interface to an activity in the presence service""" +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import gobject +import telepathy + +_logger = logging.getLogger('sugar.presence.activity') + +class Activity(gobject.GObject): + """UI interface for an Activity in the presence service + + Activities in the presence service represent your and other user's + shared activities. + + Properties: + id + color + name + type + joined + """ + __gsignals__ = { + 'buddy-joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-left': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'new-channel': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + __gproperties__ = { + 'id' : (str, None, None, None, gobject.PARAM_READABLE), + 'name' : (str, None, None, None, gobject.PARAM_READWRITE), + 'tags' : (str, None, None, None, gobject.PARAM_READWRITE), + 'color' : (str, None, None, None, gobject.PARAM_READWRITE), + 'type' : (str, None, None, None, gobject.PARAM_READABLE), + 'private' : (bool, None, None, True, gobject.PARAM_READWRITE), + 'joined' : (bool, None, None, False, gobject.PARAM_READABLE), + } + + _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" + _ACTIVITY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Activity" + + def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialse the activity interface, connecting to service""" + gobject.GObject.__init__(self) + self._object_path = object_path + self._ps_new_object = new_obj_cb + self._ps_del_object = del_obj_cb + bobj = bus.get_object(self._PRESENCE_SERVICE, object_path) + self._activity = dbus.Interface(bobj, self._ACTIVITY_DBUS_INTERFACE) + self._activity.connect_to_signal('BuddyHandleJoined', + self._buddy_handle_joined_cb) + self._activity.connect_to_signal('BuddyLeft', + self._buddy_left_cb) + self._activity.connect_to_signal('NewChannel', self._new_channel_cb) + self._activity.connect_to_signal('PropertiesChanged', + self._properties_changed_cb, + utf8_strings=True) + # FIXME: this *would* just use a normal proxy call, but I want the + # pending call object so I can block on it, and normal proxy methods + # don't return those as of dbus-python 0.82.1; so do it the hard way + self._get_properties_call = bus.call_async(self._PRESENCE_SERVICE, + object_path, self._ACTIVITY_DBUS_INTERFACE, 'GetProperties', + '', (), self._get_properties_reply_cb, + self._get_properties_error_cb, utf8_strings=True) + + self._id = None + self._color = None + self._name = None + self._type = None + self._tags = None + self._private = True + self._joined = False + # Cache for get_buddy_by_handle, maps handles to buddy object paths + self._handle_to_buddy_path = {} + self._buddy_path_to_handle = {} + + # Set up by set_up_tubes() + self.telepathy_conn = None + self.telepathy_tubes_chan = None + self.telepathy_text_chan = None + self._telepathy_room = None + + def __repr__(self): + return ('' % (self._object_path, id(self))) + + def _get_properties_reply_cb(self, new_props): + self._get_properties_call = None + _logger.debug('%r: initial GetProperties returned', self) + self._properties_changed_cb(new_props) + + def _get_properties_error_cb(self, e): + self._get_properties_call = None + # FIXME: do something with the error + _logger.warning('%r: Error doing initial GetProperties: %s', self, e) + + def _properties_changed_cb(self, new_props): + _logger.debug('%r: Activity properties changed to %r', self, new_props) + val = new_props.get('name', self._name) + if isinstance(val, str) and val != self._name: + self._name = val + self.notify('name') + val = new_props.get('tags', self._tags) + if isinstance(val, str) and val != self._tags: + self._tags = val + self.notify('tags') + val = new_props.get('color', self._color) + if isinstance(val, str) and val != self._color: + self._color = val + self.notify('color') + val = bool(new_props.get('private', self._private)) + if val != self._private: + self._private = val + self.notify('private') + val = new_props.get('id', self._id) + if isinstance(val, str) and self._id is None: + self._id = val + self.notify('id') + val = new_props.get('type', self._type) + if isinstance(val, str) and self._type is None: + self._type = val + self.notify('type') + + def object_path(self): + """Get our dbus object path""" + return self._object_path + + def do_get_property(self, pspec): + """Retrieve a particular property from our property dictionary""" + + if pspec.name == "joined": + return self._joined + + if self._get_properties_call is not None: + _logger.debug('%r: Blocking on GetProperties() because someone ' + 'wants property %s', self, pspec.name) + self._get_properties_call.block() + + if pspec.name == "id": + return self._id + elif pspec.name == "name": + return self._name + elif pspec.name == "color": + return self._color + elif pspec.name == "type": + return self._type + elif pspec.name == "tags": + return self._tags + elif pspec.name == "private": + return self._private + + # FIXME: need an asynchronous API to set these properties, particularly + # 'private' + def do_set_property(self, pspec, val): + """Set a particular property in our property dictionary""" + if pspec.name == "name": + self._activity.SetProperties({'name': val}) + self._name = val + elif pspec.name == "color": + self._activity.SetProperties({'color': val}) + self._color = val + elif pspec.name == "tags": + self._activity.SetProperties({'tags': val}) + self._tags = val + elif pspec.name == "private": + self._activity.SetProperties({'private': val}) + self._private = val + + def set_private(self, val, reply_handler, error_handler): + self._activity.SetProperties({'private': bool(val)}, + reply_handler=reply_handler, + error_handler=error_handler) + + def _emit_buddy_joined_signal(self, object_path): + """Generate buddy-joined GObject signal with presence Buddy object""" + self.emit('buddy-joined', self._ps_new_object(object_path)) + return False + + def _buddy_handle_joined_cb(self, object_path, handle): + _logger.debug('%r: buddy %s joined with handle %u', self, object_path, + handle) + gobject.idle_add(self._emit_buddy_joined_signal, object_path) + self._handle_to_buddy_path[handle] = object_path + self._buddy_path_to_handle[object_path] = handle + + def _emit_buddy_left_signal(self, object_path): + """Generate buddy-left GObject signal with presence Buddy object + + XXX note use of _ps_new_object instead of _ps_del_object here + """ + self.emit('buddy-left', self._ps_new_object(object_path)) + return False + + def _buddy_left_cb(self, object_path): + _logger.debug('%r: buddy %s left', self, object_path) + gobject.idle_add(self._emit_buddy_left_signal, object_path) + handle = self._buddy_path_to_handle.pop(object_path, None) + if handle: + self._handle_to_buddy_path.pop(handle, None) + + def _emit_new_channel_signal(self, object_path): + """Generate new-channel GObject signal with channel object path + + New telepathy-python communications channel has been opened + """ + self.emit('new-channel', object_path) + return False + + def _new_channel_cb(self, object_path): + _logger.debug('%r: new channel created at %s', self, object_path) + gobject.idle_add(self._emit_new_channel_signal, object_path) + + def get_joined_buddies(self): + """Retrieve the set of Buddy objects attached to this activity + + returns list of presence Buddy objects + """ + resp = self._activity.GetJoinedBuddies() + buddies = [] + for item in resp: + buddies.append(self._ps_new_object(item)) + return buddies + + def get_buddy_by_handle(self, handle): + """Retrieve the Buddy object given a telepathy handle. + + buddy object paths are cached in self._handle_to_buddy_path, + so we can get the buddy without calling PS. + """ + object_path = self._handle_to_buddy_path.get(handle, None) + if object_path: + buddy = self._ps_new_object(object_path) + return buddy + return None + + def invite(self, buddy, message, response_cb): + """Invite the given buddy to join this activity. + + The callback will be called with one parameter: None on success, + or an exception on failure. + """ + op = buddy.object_path() + _logger.debug('%r: inviting %s', self, op) + self._activity.Invite(op, message, + reply_handler=lambda: response_cb(None), + error_handler=response_cb) + + # Joining and sharing (FIXME: sharing is actually done elsewhere) + + def set_up_tubes(self, reply_handler, error_handler): + + cpaths = [] + + def tubes_chan_ready(chan): + _logger.debug('%r: Tubes channel %r is ready', self, chan) + self.telepathy_tubes_chan = chan + + _logger.debug('%r: finished setting up tubes', self) + reply_handler() + + def got_tubes_chan(path): + _logger.debug('%r: got Tubes channel at %s', self, path) + telepathy.client.Channel(self.telepathy_conn.service_name, + path, ready_handler=tubes_chan_ready, + error_handler=error_handler) + + def text_chan_ready(chan): + _logger.debug('%r: Text channel %r is ready', self, chan) + self.telepathy_text_chan = chan + + self.telepathy_conn.RequestChannel(telepathy.CHANNEL_TYPE_TUBES, + telepathy.HANDLE_TYPE_ROOM, + self._telepathy_room_handle, + True, + reply_handler=got_tubes_chan, + error_handler=error_handler) + + def got_text_chan(path): + _logger.debug('%r: got Text channel at %s', self, path) + telepathy.client.Channel(self.telepathy_conn.service_name, + path, ready_handler=text_chan_ready, + error_handler=error_handler) + + def conn_ready(conn): + _logger.debug('%r: Connection %r is ready', self, conn) + self.telepathy_conn = conn + + # For the moment we'll do this synchronously. + # If the PS gained a GetRoom method, we could + # do this async too + + for channel_path in cpaths: + channel = telepathy.client.Channel(conn.service_name, + channel_path) + handle_type, handle = channel.GetHandle() + if handle_type == telepathy.HANDLE_TYPE_ROOM: + room = handle + break + + if room is None: + error_handler(AssertionError("Presence Service didn't create " + "a chatroom")) + else: + self._telepathy_room_handle = room + + conn.RequestChannel(telepathy.CHANNEL_TYPE_TEXT, + telepathy.HANDLE_TYPE_ROOM, + room, True, + reply_handler=got_text_chan, + error_handler=error_handler) + + def got_channels(bus_name, conn_path, channel_paths): + _logger.debug('%r: Connection on %s at %s, channel paths: %r', + self, bus_name, conn_path, channel_paths) + + # can't use assignment for this due to Python scoping + cpaths.extend(channel_paths) + + telepathy.client.Connection(bus_name, conn_path, + ready_handler=conn_ready, + error_handler=error_handler) + + self._activity.GetChannels(reply_handler=got_channels, + error_handler=error_handler) + + def _join_cb(self): + _logger.debug('%r: Join finished', self) + self._joined = True + self.emit("joined", True, None) + + def _join_error_cb(self, err): + _logger.debug('%r: Join failed because: %s', self, err) + self.emit("joined", False, str(err)) + + def join(self): + """Join this activity. + + Emits 'joined' and otherwise does nothing if we're already joined. + """ + if self._joined: + self.emit("joined", True, None) + return + + _logger.debug('%r: joining', self) + + def joined(): + self.set_up_tubes(reply_handler=self._join_cb, + error_handler=self._join_error_cb) + + self._activity.Join(reply_handler=joined, + error_handler=self._join_error_cb) + + # GetChannels() wrapper + + def get_channels(self): + """Retrieve communications channel descriptions for the activity + + Returns a tuple containing: + - the D-Bus well-known service name of the connection + (FIXME: this is redundant; in Telepathy it can be derived + from that of the connection) + - the D-Bus object path of the connection + - a list of D-Bus object paths representing the channels + associated with this activity + """ + (bus_name, connection, channels) = self._activity.GetChannels() + _logger.debug('%r: bus name is %s, connection is %s, channels are %r', + self, bus_name, connection, channels) + return bus_name, connection, channels + + # Leaving + + def _leave_cb(self): + """Callback for async action of leaving shared activity.""" + self.emit("joined", False, "left activity") + + def _leave_error_cb(self, err): + """Callback for error in async leaving of shared activity.""" + _logger.debug('Failed to leave activity: %s', err) + + def leave(self): + """Leave this shared activity""" + _logger.debug('%r: leaving', self) + self._joined = False + self._activity.Leave(reply_handler=self._leave_cb, + error_handler=self._leave_error_cb) diff --git a/sugar/presence/buddy.py b/sugar/presence/buddy.py new file mode 100644 index 0000000..1030cfc --- /dev/null +++ b/sugar/presence/buddy.py @@ -0,0 +1,236 @@ +"""UI interface to a buddy in the presence service""" +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gobject +import gtk +import dbus + + +class Buddy(gobject.GObject): + """UI interface for a Buddy in the presence service + + Each buddy interface tracks a set of activities and properties + that can be queried to provide UI controls for manipulating + the presence interface. + + Properties Dictionary: + 'key': public key, + 'nick': nickname , + 'color': color (XXX what format), + 'current-activity': (XXX dbus path?), + 'owner': (XXX dbus path?), + 'icon': (XXX pixel data for an icon?) + See __gproperties__ + """ + __gsignals__ = { + 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])), + 'joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'property-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + __gproperties__ = { + 'key' : (str, None, None, None, gobject.PARAM_READABLE), + 'icon' : (str, None, None, None, gobject.PARAM_READABLE), + 'nick' : (str, None, None, None, gobject.PARAM_READABLE), + 'color' : (str, None, None, None, gobject.PARAM_READABLE), + 'current-activity' : (object, None, None, gobject.PARAM_READABLE), + 'owner' : (bool, None, None, False, gobject.PARAM_READABLE), + 'ip4-address' : (str, None, None, None, gobject.PARAM_READABLE) + } + + _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" + _BUDDY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Buddy" + + def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialise the reference to the buddy + + bus -- dbus bus object + new_obj_cb -- callback to call when this buddy joins an activity + del_obj_cb -- callback to call when this buddy leaves an activity + object_path -- path to the buddy object + """ + gobject.GObject.__init__(self) + self._object_path = object_path + self._ps_new_object = new_obj_cb + self._ps_del_object = del_obj_cb + self._properties = {} + self._activities = {} + + bobj = bus.get_object(self._PRESENCE_SERVICE, object_path) + self._buddy = dbus.Interface(bobj, self._BUDDY_DBUS_INTERFACE) + + self._icon_changed_signal = self._buddy.connect_to_signal( + 'IconChanged', self._icon_changed_cb, byte_arrays=True) + self._joined_activity_signal = self._buddy.connect_to_signal( + 'JoinedActivity', self._joined_activity_cb) + self._left_activity_signal = self._buddy.connect_to_signal( + 'LeftActivity', self._left_activity_cb) + self._property_changed_signal = self._buddy.connect_to_signal( + 'PropertyChanged', self._property_changed_cb) + + self._properties = self._get_properties_helper() + + activities = self._buddy.GetJoinedActivities() + for op in activities: + self._activities[op] = self._ps_new_object(op) + self._icon = None + + def destroy(self): + self._icon_changed_signal.remove() + self._joined_activity_signal.remove() + self._left_activity_signal.remove() + self._property_changed_signal.remove() + + def _get_properties_helper(self): + """Retrieve the Buddy's property dictionary from the service object + """ + props = self._buddy.GetProperties(byte_arrays=True) + if not props: + return {} + return props + + def do_get_property(self, pspec): + """Retrieve a particular property from our property dictionary + + pspec -- XXX some sort of GTK specifier object with attributes + including 'name', 'active' and 'icon-name' + """ + if pspec.name == "key": + return self._properties["key"] + elif pspec.name == "nick": + return self._properties["nick"] + elif pspec.name == "color": + return self._properties["color"] + elif pspec.name == "current-activity": + if not self._properties.has_key("current-activity"): + return None + curact = self._properties["current-activity"] + if not len(curact): + return None + for activity in self._activities.values(): + if activity.props.id == curact: + return activity + return None + elif pspec.name == "owner": + return self._properties["owner"] + elif pspec.name == "icon": + if not self._icon: + self._icon = str(self._buddy.GetIcon(byte_arrays=True)) + return self._icon + elif pspec.name == "ip4-address": + # IPv4 address will go away quite soon + if not self._properties.has_key("ip4-address"): + return None + return self._properties["ip4-address"] + + def object_path(self): + """Retrieve our dbus object path""" + return self._object_path + + def _emit_icon_changed_signal(self, bytes): + """Emit GObject signal when icon has changed""" + self._icon = str(bytes) + self.emit('icon-changed') + return False + + def _icon_changed_cb(self, icon_data): + """Handle dbus signal by emitting a GObject signal""" + gobject.idle_add(self._emit_icon_changed_signal, icon_data) + + def _emit_joined_activity_signal(self, object_path): + """Emit activity joined signal with Activity object""" + self.emit('joined-activity', self._ps_new_object(object_path)) + return False + + def _joined_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Stores the activity in activities dictionary as well + """ + if not self._activities.has_key(object_path): + self._activities[object_path] = self._ps_new_object(object_path) + gobject.idle_add(self._emit_joined_activity_signal, object_path) + + def _emit_left_activity_signal(self, object_path): + """Emit activity left signal with Activity object + + XXX this calls self._ps_new_object instead of self._ps_del_object, + which would seem to be the incorrect callback? + """ + self.emit('left-activity', self._ps_new_object(object_path)) + return False + + def _left_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Also removes from the activities dictionary + """ + if self._activities.has_key(object_path): + del self._activities[object_path] + gobject.idle_add(self._emit_left_activity_signal, object_path) + + def _handle_property_changed_signal(self, prop_list): + """Emit property-changed signal with property dictionary + + Generates a property-changed signal with the results of + _get_properties_helper() + """ + self._properties = self._get_properties_helper() + # FIXME: don't leak unexposed property names + self.emit('property-changed', prop_list) + return False + + def _property_changed_cb(self, prop_list): + """Handle dbus signal by emitting a GObject signal""" + gobject.idle_add(self._handle_property_changed_signal, prop_list) + + def get_icon_pixbuf(self): + """Retrieve Buddy's icon as a GTK pixel buffer + + XXX Why aren't the icons coming in as SVG? + """ + if self.props.icon and len(self.props.icon): + pbl = gtk.gdk.PixbufLoader() + pbl.write(self.props.icon) + pbl.close() + return pbl.get_pixbuf() + else: + return None + + def get_joined_activities(self): + """Retrieve the set of all activities which this buddy has joined + + Uses the GetJoinedActivities method on the service + object to produce object paths, wraps each in an + Activity object. + + returns list of presence Activity objects + """ + try: + resp = self._buddy.GetJoinedActivities() + except dbus.exceptions.DBusException: + return [] + acts = [] + for item in resp: + acts.append(self._ps_new_object(item)) + return acts diff --git a/sugar/presence/presenceservice.py b/sugar/presence/presenceservice.py new file mode 100644 index 0000000..cb47a3a --- /dev/null +++ b/sugar/presence/presenceservice.py @@ -0,0 +1,577 @@ +"""UI class to access system-level presence object""" +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import dbus.exceptions +import dbus.glib +import gobject + +from sugar.presence.buddy import Buddy +from sugar.presence.activity import Activity + + +DBUS_SERVICE = "org.laptop.Sugar.Presence" +DBUS_INTERFACE = "org.laptop.Sugar.Presence" +DBUS_PATH = "/org/laptop/Sugar/Presence" + +_logger = logging.getLogger('sugar.presence.presenceservice') + + +class PresenceService(gobject.GObject): + """UI-side interface to the dbus presence service + + This class provides UI programmers with simplified access + to the dbus service of the same name. It allows for observing + various events from the presence service as GObject events, + as well as some basic introspection queries. + """ + __gsignals__ = { + 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + # parameters: (activity: Activity, inviter: Buddy, message: unicode) + 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, None, ([object]*3)), + 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])) + } + + _PS_BUDDY_OP = DBUS_PATH + "/Buddies/" + _PS_ACTIVITY_OP = DBUS_PATH + "/Activities/" + + + def __init__(self, allow_offline_iface=True): + """Initialise the service and attempt to connect to events + """ + gobject.GObject.__init__(self) + self._objcache = {} + + # Get a connection to the session bus + self._bus = dbus.SessionBus() + self._bus.add_signal_receiver(self._name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + # attempt to load the interface to the service... + self._allow_offline_iface = allow_offline_iface + self._get_ps() + + def _name_owner_changed_cb(self, name, old, new): + if name != DBUS_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # PS went away, clear out PS dbus service wrapper + self._ps_ = None + elif (not old and not len(old)) and (new and len(new)): + # PS started up + self._get_ps() + + _ps_ = None + def _get_ps(self): + """Retrieve dbus interface to PresenceService + + Also registers for updates from various dbus events on the + interface. + + If unable to retrieve the interface, we will temporarily + return an _OfflineInterface object to allow the calling + code to continue functioning as though it had accessed a + real presence service. + + If successful, caches the presence service interface + for use by other methods and returns that interface + """ + if not self._ps_: + try: + # NOTE: We need to follow_name_owner_changes here + # because we can not connect to a signal unless + # we follow the changes or we start the service + # before we connect. Starting the service here + # causes a major bottleneck during startup + ps = dbus.Interface( + self._bus.get_object(DBUS_SERVICE, + DBUS_PATH, + follow_name_owner_changes=True), + DBUS_INTERFACE + ) + except dbus.exceptions.DBusException, err: + _logger.error( + """Failure retrieving %r interface from the D-BUS service %r %r: %s""", + DBUS_INTERFACE, DBUS_SERVICE, DBUS_PATH, err + ) + if self._allow_offline_iface: + return _OfflineInterface() + raise RuntimeError("Failed to connect to the presence service.") + else: + self._ps_ = ps + ps.connect_to_signal('BuddyAppeared', self._buddy_appeared_cb) + ps.connect_to_signal('BuddyDisappeared', self._buddy_disappeared_cb) + ps.connect_to_signal('ActivityAppeared', self._activity_appeared_cb) + ps.connect_to_signal('ActivityDisappeared', self._activity_disappeared_cb) + ps.connect_to_signal('ActivityInvitation', self._activity_invitation_cb) + ps.connect_to_signal('PrivateInvitation', self._private_invitation_cb) + return self._ps_ + + _ps = property( + _get_ps, None, None, + """DBUS interface to the PresenceService (services/presence/presenceservice)""" + ) + + def _new_object(self, object_path): + """Turn new object path into (cached) Buddy/Activity instance + + object_path -- full dbus path of the new object, must be + prefixed with either of _PS_BUDDY_OP or _PS_ACTIVITY_OP + + Note that this method is called throughout the class whenever + the representation of the object is required, it is not only + called when the object is first discovered. The point is to only have + _one_ Python object for any D-Bus object represented by an object path, + effectively wrapping the D-Bus object in a single Python GObject. + + returns presence Buddy or Activity representation + """ + obj = None + try: + obj = self._objcache[object_path] + _logger.debug('Reused proxy %r', obj) + except KeyError: + if object_path.startswith(self._PS_BUDDY_OP): + obj = Buddy(self._bus, self._new_object, + self._del_object, object_path) + elif object_path.startswith(self._PS_ACTIVITY_OP): + obj = Activity(self._bus, self._new_object, + self._del_object, object_path) + try: + # Pre-fill the activity's ID + foo = obj.props.id + except dbus.exceptions.DBusException, err: + pass + else: + raise RuntimeError("Unknown object type") + self._objcache[object_path] = obj + _logger.debug('Created proxy %r', obj) + return obj + + def _have_object(self, object_path): + return object_path in self._objcache.keys() + + def _del_object(self, object_path): + """Fully remove an object from the object cache when it's no longer needed. + """ + del self._objcache[object_path] + + def _emit_buddy_appeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" + self.emit('buddy-appeared', self._new_object(object_path)) + return False + + def _buddy_appeared_cb(self, op): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_buddy_appeared_signal, op) + + def _emit_buddy_disappeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" + # Don't try to create a new object here if needed; it will probably + # fail anyway because the object has already been destroyed in the PS + if self._have_object(object_path): + obj = self._objcache[object_path] + self.emit('buddy-disappeared', obj) + + # We cannot maintain the object in the cache because that would keep + # a lot of objects from being collected. That includes UI objects + # due to signals using strong references. + # If we want to cache some despite the memory usage increase, + # we could use a LRU cache limited to some value. + del self._objcache[object_path] + obj.destroy() + + return False + + def _buddy_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_buddy_disappeared_signal, object_path) + + def _emit_activity_invitation_signal(self, activity_path, buddy_path, + message): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-invitation', self._new_object(activity_path), + self._new_object(buddy_path), unicode(message)) + return False + + def _activity_invitation_cb(self, activity_path, buddy_path, message): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_invitation_signal, activity_path, + buddy_path, message) + + def _emit_private_invitation_signal(self, bus_name, connection, channel): + """Emit GObject event with bus_name, connection and channel""" + self.emit('private-invitation', bus_name, connection, channel) + return False + + def _private_invitation_cb(self, bus_name, connection, channel): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_service_disappeared_signal, bus_name, + connection, channel) + + def _emit_activity_appeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-appeared', self._new_object(object_path)) + return False + + def _activity_appeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_appeared_signal, object_path) + + def _emit_activity_disappeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-disappeared', self._new_object(object_path)) + return False + + def _activity_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_disappeared_signal, object_path) + + def get(self, object_path): + """Return the Buddy or Activity object corresponding to the given + D-Bus object path. + """ + return self._new_object(object_path) + + def get_activities(self): + """Retrieve set of all activities from service + + returns list of Activity objects for all object paths + the service reports exist (using GetActivities) + """ + try: + resp = self._ps.GetActivities() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve activity list from presence service: %s""" + % err + ) + return [] + else: + acts = [] + for item in resp: + acts.append(self._new_object(item)) + return acts + + def _get_activities_cb(self, reply_handler, resp): + acts = [] + for item in resp: + acts.append(self._new_object(item)) + + reply_handler(acts) + + def _get_activities_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + _logger.warn( + """Unable to retrieve activity-list from presence service: %s""" + % e + ) + + def get_activities_async(self, reply_handler=None, error_handler=None): + """Retrieve set of all activities from service asyncronously + """ + + if not reply_handler: + logging.error('Function get_activities_async called without a reply handler. Can not run.') + return + + self._ps.GetActivities( + reply_handler=lambda resp:self._get_activities_cb(reply_handler, resp), + error_handler=lambda e:self._get_activities_error_cb(error_handler, e)) + + + def get_activity(self, activity_id, warn_if_none=True): + """Retrieve single Activity object for the given unique id + + activity_id -- unique ID for the activity + + returns single Activity object or None if the activity + is not found using GetActivityById on the service + """ + try: + act_op = self._ps.GetActivityById(activity_id) + except dbus.exceptions.DBusException, err: + if warn_if_none: + _logger.warn("Unable to retrieve activity handle for %r from " + "presence service: %s", activity_id, err) + return None + return self._new_object(act_op) + + def get_buddies(self): + """Retrieve set of all buddies from service + + returns list of Buddy objects for all object paths + the service reports exist (using GetBuddies) + """ + try: + resp = self._ps.GetBuddies() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve buddy-list from presence service: %s""" + % err + ) + return [] + else: + buddies = [] + for item in resp: + buddies.append(self._new_object(item)) + return buddies + + def _get_buddies_cb(self, reply_handler, resp): + buddies = [] + for item in resp: + buddies.append(self._new_object(item)) + + reply_handler(buddies) + + def _get_buddies_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + _logger.warn( + """Unable to retrieve buddy-list from presence service: %s""" + % e + ) + + def get_buddies_async(self, reply_handler=None, error_handler=None): + """Retrieve set of all buddies from service asyncronously + """ + + if not reply_handler: + logging.error('Function get_buddies_async called without a reply handler. Can not run.') + return + + self._ps.GetBuddies( + reply_handler=lambda resp:self._get_buddies_cb(reply_handler, resp), + error_handler=lambda e:self._get_buddies_error_cb(error_handler, e)) + + def get_buddy(self, key): + """Retrieve single Buddy object for the given public key + + key -- buddy's public encryption key + + returns single Buddy object or None if the activity + is not found using GetBuddyByPublicKey on the + service + """ + try: + buddy_op = self._ps.GetBuddyByPublicKey(dbus.ByteArray(key)) + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve buddy handle for %r from presence service: %s""" + % key, err + ) + return None + return self._new_object(buddy_op) + + def get_buddy_by_telepathy_handle(self, tp_conn_name, tp_conn_path, + handle): + """Retrieve single Buddy object for the given public key + + :Parameters: + `tp_conn_name` : str + The well-known bus name of a Telepathy connection + `tp_conn_path` : dbus.ObjectPath + The object path of the Telepathy connection + `handle` : int or long + The handle of a Telepathy contact on that connection, + of type HANDLE_TYPE_CONTACT. This may not be a + channel-specific handle. + :Returns: the Buddy object, or None if the buddy is not found + """ + try: + buddy_op = self._ps.GetBuddyByTelepathyHandle(tp_conn_name, + tp_conn_path, + handle) + except dbus.exceptions.DBusException, err: + _logger.warn('Unable to retrieve buddy handle for handle %u at ' + 'conn %s:%s from presence service: %s', + handle, tp_conn_name, tp_conn_path, err) + return None + return self._new_object(buddy_op) + + def get_owner(self): + """Retrieves the laptop "owner" Buddy object.""" + try: + owner_op = self._ps.GetOwner() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve local user/owner from presence service: %s""" + % err + ) + raise RuntimeError("Could not get owner object from presence service.") + return self._new_object(owner_op) + + def _share_activity_cb(self, activity, op): + """Finish sharing the activity + """ + psact = self._new_object(op) + psact._joined = True + _logger.debug('%r: Just shared, setting up tubes', activity) + psact.set_up_tubes(reply_handler=lambda: + self.emit("activity-shared", True, psact, None), + error_handler=lambda e: + self._share_activity_error_cb(activity, e)) + + def _share_activity_error_cb(self, activity, err): + """Notify with GObject event of unsuccessful sharing of activity""" + _logger.debug("Error sharing activity %s: %s" % (activity.get_id(), err)) + self.emit("activity-shared", False, None, err) + + def share_activity(self, activity, properties={}, private=True): + """Ask presence service to ask the activity to share itself publicly. + + Uses the AdvertiseActivity method on the service to ask for the + sharing of the given activity. Arranges to emit activity-shared + event with: + + (success, Activity, err) + + on success/failure. + + returns None + """ + actid = activity.get_id() + + # Ensure the activity is not already shared/joined + for obj in self._objcache.values(): + if not isinstance(object, Activity): + continue + if obj.props.id == actid or obj.props.joined: + raise RuntimeError("Activity %s is already shared." % + actid) + + atype = activity.get_bundle_id() + name = activity.props.title + properties['private'] = bool(private) + self._ps.ShareActivity(actid, atype, name, properties, + reply_handler=lambda op: \ + self._share_activity_cb(activity, op), + error_handler=lambda e: \ + self._share_activity_error_cb(activity, e)) + + def get_preferred_connection(self): + """Gets the preferred telepathy connection object that an activity + should use when talking directly to telepathy + + returns the bus name and the object path of the Telepathy connection""" + + try: + bus_name, object_path = self._ps.GetPreferredConnection() + except dbus.exceptions.DBusException: + return None + + return bus_name, object_path + +class _OfflineInterface( object ): + """Offline-presence-service interface + + Used to mimic the behaviour of a real PresenceService sufficiently + to avoid crashing client code that expects the given interface. + + XXX we could likely return a "MockOwner" object reasonably + easily, but would it be worth it? + """ + def raiseException( self, *args, **named ): + """Raise dbus.exceptions.DBusException""" + raise dbus.exceptions.DBusException( + """PresenceService Interface not available""" + ) + GetActivities = raiseException + GetActivityById = raiseException + GetBuddies = raiseException + GetBuddyByPublicKey = raiseException + GetOwner = raiseException + GetPreferredConnection = raiseException + def ShareActivity( + self, actid, atype, name, properties, + reply_handler, error_handler, + ): + """Pretend to share and fail...""" + exc = IOError( + """Unable to share activity as PresenceService is not currenly available""" + ) + return error_handler( exc ) + +class _MockPresenceService(gobject.GObject): + """Test fixture allowing testing of items that use PresenceService + + See PresenceService for usage and purpose + """ + __gsignals__ = { + 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + def get_activities(self): + return [] + + def get_activity(self, activity_id): + return None + + def get_buddies(self): + return [] + + def get_buddy(self, key): + return None + + def get_owner(self): + return None + + def share_activity(self, activity, properties={}): + return None + +_ps = None +def get_instance(allow_offline_iface=False): + """Retrieve this process' view of the PresenceService""" + global _ps + if not _ps: + _ps = PresenceService(allow_offline_iface) + return _ps + diff --git a/sugar/presence/test_presence.txt b/sugar/presence/test_presence.txt new file mode 100644 index 0000000..d0736a9 --- /dev/null +++ b/sugar/presence/test_presence.txt @@ -0,0 +1,26 @@ +This is a test of presence. + +To test this service we will start up a mock dbus library: + + >>> from sugar.testing import mockdbus + >>> import dbus + >>> pres_service = mockdbus.MockService( + ... 'org.laptop.Presence', '/org/laptop/Presence', name='pres') + >>> pres_service.install() + >>> pres_interface = dbus.Interface(pres_service, 'org.laptop.Presence') + +Then we import the library (second, to make sure it connects to our +mocked system, though the lazy instantiation in get_instance() should +handle it): + + >>> from sugar.presence import PresenceService + >>> ps = PresenceService.get_instance() + >>> pres_interface.make_response('getServices', []) + >>> ps.get_services() + Called pres.org.laptop.Presence:getServices() + [] + >>> pres_interface.make_response('getBuddies', []) + >>> ps.get_buddies() + Called pres.org.laptop.Presence:getBuddies() + [] + diff --git a/sugar/presence/tubeconn.py b/sugar/presence/tubeconn.py new file mode 100644 index 0000000..b487391 --- /dev/null +++ b/sugar/presence/tubeconn.py @@ -0,0 +1,107 @@ +# This should eventually land in telepathy-python, so has the same license: + +# Copyright (C) 2007 Collabora Ltd. +# +# This program 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.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +__all__ = ('TubeConnection',) +__docformat__ = 'reStructuredText' + + +import logging + +from dbus.connection import Connection + + +logger = logging.getLogger('telepathy.tubeconn') + + +class TubeConnection(Connection): + + def __new__(cls, conn, tubes_iface, tube_id, address=None, + group_iface=None, mainloop=None): + if address is None: + address = tubes_iface.GetDBusTubeAddress(tube_id) + self = super(TubeConnection, cls).__new__(cls, address, + mainloop=mainloop) + + self._tubes_iface = tubes_iface + self.tube_id = tube_id + self.participants = {} + self.bus_name_to_handle = {} + self._mapping_watches = [] + + if group_iface is None: + method = conn.GetSelfHandle + else: + method = group_iface.GetSelfHandle + method(reply_handler=self._on_get_self_handle_reply, + error_handler=self._on_get_self_handle_error) + + return self + + def _on_get_self_handle_reply(self, handle): + self.self_handle = handle + match = self._tubes_iface.connect_to_signal('DBusNamesChanged', + self._on_dbus_names_changed) + self._tubes_iface.GetDBusNames(self.tube_id, + reply_handler=self._on_get_dbus_names_reply, + error_handler=self._on_get_dbus_names_error) + self._dbus_names_changed_match = match + + def _on_get_self_handle_error(self, e): + logging.basicConfig() + logger.error('GetSelfHandle failed: %s', e) + + def close(self): + self._dbus_names_changed_match.remove() + self._on_dbus_names_changed(self.tube_id, (), self.participants.keys()) + super(TubeConnection, self).close() + + def _on_get_dbus_names_reply(self, names): + self._on_dbus_names_changed(self.tube_id, names, ()) + + def _on_get_dbus_names_error(self, e): + logging.basicConfig() + logger.error('GetDBusNames failed: %s', e) + + def _on_dbus_names_changed(self, tube_id, added, removed): + if tube_id == self.tube_id: + for handle, bus_name in added: + if handle == self.self_handle: + # I've just joined - set my unique name + self.set_unique_name(bus_name) + self.participants[handle] = bus_name + self.bus_name_to_handle[bus_name] = handle + + # call the callback while the removed people are still in + # participants, so their bus names are available + for callback in self._mapping_watches: + callback(added, removed) + + for handle in removed: + bus_name = self.participants.pop(handle, None) + self.bus_name_to_handle.pop(bus_name, None) + + def watch_participants(self, callback): + self._mapping_watches.append(callback) + if self.participants: + # GetDBusNames already returned: fake a participant add event + # immediately + added = [] + for k, v in self.participants.iteritems(): + added.append((k, v)) + callback(added, []) diff --git a/sugar/profile.py b/sugar/profile.py new file mode 100644 index 0000000..11991dd --- /dev/null +++ b/sugar/profile.py @@ -0,0 +1,207 @@ +"""User settings/configuration loading""" +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import logging +from ConfigParser import ConfigParser + +from sugar import env +from sugar import util +from sugar.graphics.xocolor import XoColor + +DEFAULT_JABBER_SERVER = 'olpc.collabora.co.uk' +DEFAULT_VOLUME = 81 + +_profile = None + +def _set_key(cp, section, key, value): + if not cp.has_section(section): + cp.add_section(section) + cp.set(section, key, value) + +class Profile(object): + """Local user's current options/profile information + + User settings are stored in an INI-style configuration + file. This object uses the ConfigParser module to load + the settings. (We only very rarely set keys, so we don't + keep the ConfigParser around between calls.) + + The profile is also responsible for loading the user's + public and private ssh keys from disk. + + Attributes: + + name -- child's name + color -- XoColor for the child's icon + server -- school server with which the child is + associated + server_registered -- whether the child has registered + with the school server or not + backup1 -- temporary backup info key for Trial-2 + + pubkey -- public ssh key + privkey_hash -- SHA has of the child's public key + """ + def __init__(self, path): + self.nick_name = None + self.color = None + self.jabber_server = DEFAULT_JABBER_SERVER + self.jabber_registered = False + self.backup1 = None + self.sound_volume = DEFAULT_VOLUME + + self._pubkey = None + self._privkey_hash = None + self._config_path = path + + self._load_config() + + def is_valid(self): + return self.nick_name is not None and \ + self.color is not None and \ + self.pubkey is not None and \ + self.privkey_hash is not None + + def is_registered(self): + return self.backup1 is not None + + def save(self): + cp = ConfigParser() + cp.read([self._config_path]) + + if self.nick_name: + _set_key(cp, 'Buddy', 'NickName', self.nick_name.encode('utf8')) + if self.color: + _set_key(cp, 'Buddy', 'Color', self.color.to_string()) + if self.backup1: + _set_key(cp, 'Server', 'Backup1', self.backup1) + if self.jabber_server: + _set_key(cp, 'Jabber', 'Server', self.jabber_server) + + _set_key(cp, 'Jabber', 'Registered', self.jabber_registered) + + _set_key(cp, 'Sound', 'Volume', self.sound_volume) + + f = open(self._config_path, 'w') + cp.write(f) + f.close() + + def _load_config(self): + cp = ConfigParser() + cp.read([self._config_path]) + + if cp.has_option('Buddy', 'NickName'): + name = cp.get('Buddy', 'NickName') + # decode nickname from ascii-safe chars to unicode + self.nick_name = name.decode("utf-8") + if cp.has_option('Buddy', 'Color'): + self.color = XoColor(cp.get('Buddy', 'Color')) + if cp.has_option('Jabber', 'Server'): + self.jabber_server = cp.get('Jabber', 'Server') + if cp.has_option('Jabber', 'Registered'): + registered = cp.get('Jabber', 'Registered') + if registered.lower() == "true": + self.jabber_registered = True + if cp.has_option('Server', 'Backup1'): + self.backup1 = cp.get('Server', 'Backup1') + if cp.has_option('Sound', 'Volume'): + self.sound_volume = float(cp.get('Sound', 'Volume')) + + del cp + + def _load_pubkey(self): + key_path = os.path.join(env.get_profile_path(), 'owner.key.pub') + try: + f = open(key_path, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading public key: %s" % e) + return None + + magic = "ssh-dss " + for l in lines: + l = l.strip() + if not l.startswith(magic): + continue + return l[len(magic):] + else: + logging.error("Error parsing public key.") + return None + + def _get_pubkey(self): + # load on-demand. + if not self._pubkey: + self._pubkey = self._load_pubkey() + return self._pubkey + + def _hash_private_key(self): + key_path = os.path.join(env.get_profile_path(), 'owner.key') + try: + f = open(key_path, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading private key: %s" % e) + return None + + key = "" + for l in lines: + l = l.strip() + if l.startswith("-----BEGIN DSA PRIVATE KEY-----"): + continue + if l.startswith("-----END DSA PRIVATE KEY-----"): + continue + key += l + if not len(key): + logging.error("Error parsing public key.") + return None + + # hash it + key_hash = util._sha_data(key) + return util.printable_hash(key_hash) + + def _get_privkey_hash(self): + # load on-demand. + if not self._privkey_hash: + self._privkey_hash = self._hash_private_key() + return self._privkey_hash + + privkey_hash = property(_get_privkey_hash) + pubkey = property(_get_pubkey) + +def get_profile(): + global _profile + + if not _profile: + path = os.path.join(env.get_profile_path(), 'config') + _profile = Profile(path) + + return _profile + +# Convenience methods for frequently used properties + +def get_nick_name(): + return get_profile().nick_name + +def get_color(): + return get_profile().color + +def get_pubkey(): + return get_profile().pubkey diff --git a/sugar/sexy-icon-entry.c b/sugar/sexy-icon-entry.c new file mode 100644 index 0000000..ca35209 --- /dev/null +++ b/sugar/sexy-icon-entry.c @@ -0,0 +1,984 @@ +/* + * @file libsexy/sexy-icon-entry.c Entry widget + * + * @Copyright (C) 2004-2006 Christian Hammond. + * + * 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. + */ +#include +#include +#include + +#define ICON_MARGIN 2 +#define MAX_ICONS 2 + +#define IS_VALID_ICON_ENTRY_POSITION(pos) \ + ((pos) == SEXY_ICON_ENTRY_PRIMARY || \ + (pos) == SEXY_ICON_ENTRY_SECONDARY) + +typedef struct +{ + GtkImage *icon; + gboolean highlight; + gboolean hovered; + GdkWindow *window; + +} SexyIconInfo; + +struct _SexyIconEntryPriv +{ + SexyIconInfo icons[MAX_ICONS]; + + gulong icon_released_id; +}; + +enum +{ + ICON_PRESSED, + ICON_RELEASED, + LAST_SIGNAL +}; + +static void sexy_icon_entry_class_init(SexyIconEntryClass *klass); +static void sexy_icon_entry_editable_init(GtkEditableClass *iface); +static void sexy_icon_entry_init(SexyIconEntry *entry); +static void sexy_icon_entry_finalize(GObject *obj); +static void sexy_icon_entry_destroy(GtkObject *obj); +static void sexy_icon_entry_map(GtkWidget *widget); +static void sexy_icon_entry_unmap(GtkWidget *widget); +static void sexy_icon_entry_realize(GtkWidget *widget); +static void sexy_icon_entry_unrealize(GtkWidget *widget); +static void sexy_icon_entry_size_request(GtkWidget *widget, + GtkRequisition *requisition); +static void sexy_icon_entry_size_allocate(GtkWidget *widget, + GtkAllocation *allocation); +static gint sexy_icon_entry_expose(GtkWidget *widget, GdkEventExpose *event); +static gint sexy_icon_entry_enter_notify(GtkWidget *widget, + GdkEventCrossing *event); +static gint sexy_icon_entry_leave_notify(GtkWidget *widget, + GdkEventCrossing *event); +static gint sexy_icon_entry_button_press(GtkWidget *widget, + GdkEventButton *event); +static gint sexy_icon_entry_button_release(GtkWidget *widget, + GdkEventButton *event); + +static GtkEntryClass *parent_class = NULL; +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE_EXTENDED(SexyIconEntry, sexy_icon_entry, GTK_TYPE_ENTRY, + 0, + G_IMPLEMENT_INTERFACE(GTK_TYPE_EDITABLE, + sexy_icon_entry_editable_init)); + +static void +sexy_icon_entry_class_init(SexyIconEntryClass *klass) +{ + GObjectClass *gobject_class; + GtkObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkEntryClass *entry_class; + + parent_class = g_type_class_peek_parent(klass); + + gobject_class = G_OBJECT_CLASS(klass); + object_class = GTK_OBJECT_CLASS(klass); + widget_class = GTK_WIDGET_CLASS(klass); + entry_class = GTK_ENTRY_CLASS(klass); + + gobject_class->finalize = sexy_icon_entry_finalize; + + object_class->destroy = sexy_icon_entry_destroy; + + widget_class->map = sexy_icon_entry_map; + widget_class->unmap = sexy_icon_entry_unmap; + widget_class->realize = sexy_icon_entry_realize; + widget_class->unrealize = sexy_icon_entry_unrealize; + widget_class->size_request = sexy_icon_entry_size_request; + widget_class->size_allocate = sexy_icon_entry_size_allocate; + widget_class->expose_event = sexy_icon_entry_expose; + widget_class->enter_notify_event = sexy_icon_entry_enter_notify; + widget_class->leave_notify_event = sexy_icon_entry_leave_notify; + widget_class->button_press_event = sexy_icon_entry_button_press; + widget_class->button_release_event = sexy_icon_entry_button_release; + + /** + * SexyIconEntry::icon-pressed: + * @entry: The entry on which the signal is emitted. + * @icon_pos: The position of the clicked icon. + * @button: The mouse button clicked. + * + * The ::icon-pressed signal is emitted when an icon is clicked. + */ + signals[ICON_PRESSED] = + g_signal_new("icon_pressed", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(SexyIconEntryClass, icon_pressed), + NULL, NULL, + gtk_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); + + /** + * SexyIconEntry::icon-released: + * @entry: The entry on which the signal is emitted. + * @icon_pos: The position of the clicked icon. + * @button: The mouse button clicked. + * + * The ::icon-released signal is emitted on the button release from a + * mouse click. + */ + signals[ICON_RELEASED] = + g_signal_new("icon_released", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(SexyIconEntryClass, icon_released), + NULL, NULL, + gtk_marshal_VOID__INT_INT, + G_TYPE_NONE, 2, + G_TYPE_INT, + G_TYPE_INT); +} + +static void +sexy_icon_entry_editable_init(GtkEditableClass *iface) +{ +}; + +static void +sexy_icon_entry_init(SexyIconEntry *entry) +{ + entry->priv = g_new0(SexyIconEntryPriv, 1); +} + +static void +sexy_icon_entry_finalize(GObject *obj) +{ + SexyIconEntry *entry; + + g_return_if_fail(obj != NULL); + g_return_if_fail(SEXY_IS_ICON_ENTRY(obj)); + + entry = SEXY_ICON_ENTRY(obj); + + g_free(entry->priv); + + if (G_OBJECT_CLASS(parent_class)->finalize) + G_OBJECT_CLASS(parent_class)->finalize(obj); +} + +static void +sexy_icon_entry_destroy(GtkObject *obj) +{ + SexyIconEntry *entry; + + entry = SEXY_ICON_ENTRY(obj); + + sexy_icon_entry_set_icon(entry, SEXY_ICON_ENTRY_PRIMARY, NULL); + sexy_icon_entry_set_icon(entry, SEXY_ICON_ENTRY_SECONDARY, NULL); + + if (GTK_OBJECT_CLASS(parent_class)->destroy) + GTK_OBJECT_CLASS(parent_class)->destroy(obj); +} + +static void +sexy_icon_entry_map(GtkWidget *widget) +{ + if (GTK_WIDGET_REALIZED(widget) && !GTK_WIDGET_MAPPED(widget)) + { + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + GTK_WIDGET_CLASS(parent_class)->map(widget); + + for (i = 0; i < MAX_ICONS; i++) + { + if (entry->priv->icons[i].icon != NULL) + gdk_window_show(entry->priv->icons[i].window); + } + } +} + +static void +sexy_icon_entry_unmap(GtkWidget *widget) +{ + if (GTK_WIDGET_MAPPED(widget)) + { + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + for (i = 0; i < MAX_ICONS; i++) + { + if (entry->priv->icons[i].icon != NULL) + gdk_window_hide(entry->priv->icons[i].window); + } + + GTK_WIDGET_CLASS(parent_class)->unmap(widget); + } +} + +static gint +get_icon_width(SexyIconEntry *entry, SexyIconEntryPosition icon_pos) +{ + GtkRequisition requisition; + gint menu_icon_width; + gint width; + SexyIconInfo *icon_info = &entry->priv->icons[icon_pos]; + + if (icon_info->icon == NULL) + return 0; + + gtk_widget_size_request(GTK_WIDGET(icon_info->icon), &requisition); + gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &menu_icon_width, NULL); + + width = MAX(requisition.width, menu_icon_width); + + return width; +} + +static void +get_borders(SexyIconEntry *entry, gint *xborder, gint *yborder) +{ + GtkWidget *widget = GTK_WIDGET(entry); + gint focus_width; + gboolean interior_focus; + + gtk_widget_style_get(widget, + "interior-focus", &interior_focus, + "focus-line-width", &focus_width, + NULL); + + if (gtk_entry_get_has_frame(GTK_ENTRY(entry))) + { + *xborder = widget->style->xthickness; + *yborder = widget->style->ythickness; + } + else + { + *xborder = 0; + *yborder = 0; + } + + if (!interior_focus) + { + *xborder += focus_width; + *yborder += focus_width; + } +} + +static void +get_text_area_size(SexyIconEntry *entry, GtkAllocation *alloc) +{ + GtkWidget *widget = GTK_WIDGET(entry); + GtkRequisition requisition; + gint xborder, yborder; + + gtk_widget_get_child_requisition(widget, &requisition); + get_borders(entry, &xborder, &yborder); + + alloc->x = xborder; + alloc->y = yborder; + alloc->width = widget->allocation.width - xborder * 2; + alloc->height = requisition.height - yborder * 2; +} + +static void +get_icon_allocation(SexyIconEntry *icon_entry, + gboolean left, + GtkAllocation *widget_alloc, + GtkAllocation *text_area_alloc, + GtkAllocation *allocation, + SexyIconEntryPosition *icon_pos) +{ + gboolean rtl; + + rtl = (gtk_widget_get_direction(GTK_WIDGET(icon_entry)) == + GTK_TEXT_DIR_RTL); + + if (left) + *icon_pos = (rtl ? SEXY_ICON_ENTRY_SECONDARY : SEXY_ICON_ENTRY_PRIMARY); + else + *icon_pos = (rtl ? SEXY_ICON_ENTRY_PRIMARY : SEXY_ICON_ENTRY_SECONDARY); + + allocation->y = text_area_alloc->y; + allocation->width = get_icon_width(icon_entry, *icon_pos); + allocation->height = text_area_alloc->height; + + if (left) + allocation->x = text_area_alloc->x + ICON_MARGIN; + else + { + allocation->x = text_area_alloc->x + text_area_alloc->width - + allocation->width - ICON_MARGIN; + } +} + +static void +sexy_icon_entry_realize(GtkWidget *widget) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + GdkWindowAttr attributes; + gint attributes_mask; + int i; + + GTK_WIDGET_CLASS(parent_class)->realize(widget); + + attributes.x = 0; + attributes.y = 0; + attributes.width = 1; + attributes.height = 1; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual(widget); + attributes.colormap = gtk_widget_get_colormap(widget); + attributes.event_mask = gtk_widget_get_events(widget); + attributes.event_mask |= + (GDK_EXPOSURE_MASK + | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; + + for (i = 0; i < MAX_ICONS; i++) + { + SexyIconInfo *icon_info; + + icon_info = &entry->priv->icons[i]; + icon_info->window = gdk_window_new(widget->window, &attributes, + attributes_mask); + gdk_window_set_user_data(icon_info->window, widget); + + gdk_window_set_background(icon_info->window, + &widget->style->base[GTK_WIDGET_STATE(widget)]); + } + + gtk_widget_queue_resize(widget); +} + +static void +sexy_icon_entry_unrealize(GtkWidget *widget) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + GTK_WIDGET_CLASS(parent_class)->unrealize(widget); + + for (i = 0; i < MAX_ICONS; i++) + { + SexyIconInfo *icon_info = &entry->priv->icons[i]; + + gdk_window_destroy(icon_info->window); + icon_info->window = NULL; + } +} + +static void +sexy_icon_entry_size_request(GtkWidget *widget, GtkRequisition *requisition) +{ + GtkEntry *gtkentry; + SexyIconEntry *entry; + gint icon_widths = 0; + int i; + + gtkentry = GTK_ENTRY(widget); + entry = SEXY_ICON_ENTRY(widget); + + for (i = 0; i < MAX_ICONS; i++) + { + int icon_width = get_icon_width(entry, i); + + if (icon_width > 0) + icon_widths += icon_width + ICON_MARGIN; + } + + GTK_WIDGET_CLASS(parent_class)->size_request(widget, requisition); + + if (icon_widths > requisition->width) + requisition->width += icon_widths; +} + +static void +place_windows(SexyIconEntry *icon_entry, GtkAllocation *widget_alloc) +{ + SexyIconEntryPosition left_icon_pos; + SexyIconEntryPosition right_icon_pos; + GtkAllocation left_icon_alloc; + GtkAllocation right_icon_alloc; + GtkAllocation text_area_alloc; + + get_text_area_size(icon_entry, &text_area_alloc); + get_icon_allocation(icon_entry, TRUE, widget_alloc, &text_area_alloc, + &left_icon_alloc, &left_icon_pos); + get_icon_allocation(icon_entry, FALSE, widget_alloc, &text_area_alloc, + &right_icon_alloc, &right_icon_pos); + + if (left_icon_alloc.width > 0) + { + text_area_alloc.x = left_icon_alloc.x + left_icon_alloc.width + + ICON_MARGIN; + } + + if (right_icon_alloc.width > 0) + text_area_alloc.width -= right_icon_alloc.width + ICON_MARGIN; + + text_area_alloc.width -= text_area_alloc.x; + + gdk_window_move_resize(icon_entry->priv->icons[left_icon_pos].window, + left_icon_alloc.x, left_icon_alloc.y, + left_icon_alloc.width, left_icon_alloc.height); + + gdk_window_move_resize(icon_entry->priv->icons[right_icon_pos].window, + right_icon_alloc.x, right_icon_alloc.y, + right_icon_alloc.width, right_icon_alloc.height); + + gdk_window_move_resize(GTK_ENTRY(icon_entry)->text_area, + text_area_alloc.x, text_area_alloc.y, + text_area_alloc.width, text_area_alloc.height); +} + +static void +sexy_icon_entry_size_allocate(GtkWidget *widget, GtkAllocation *allocation) +{ + g_return_if_fail(SEXY_IS_ICON_ENTRY(widget)); + g_return_if_fail(allocation != NULL); + + widget->allocation = *allocation; + + GTK_WIDGET_CLASS(parent_class)->size_allocate(widget, allocation); + + if (GTK_WIDGET_REALIZED(widget)) + place_windows(SEXY_ICON_ENTRY(widget), allocation); +} + +static GdkPixbuf * +get_pixbuf_from_icon(SexyIconEntry *entry, SexyIconEntryPosition icon_pos) +{ + GdkPixbuf *pixbuf = NULL; + gchar *stock_id; + SexyIconInfo *icon_info = &entry->priv->icons[icon_pos]; + GtkIconSize size; + + switch (gtk_image_get_storage_type(GTK_IMAGE(icon_info->icon))) + { + case GTK_IMAGE_PIXBUF: + pixbuf = gtk_image_get_pixbuf(GTK_IMAGE(icon_info->icon)); + g_object_ref(pixbuf); + break; + + case GTK_IMAGE_STOCK: + gtk_image_get_stock(GTK_IMAGE(icon_info->icon), &stock_id, &size); + pixbuf = gtk_widget_render_icon(GTK_WIDGET(entry), + stock_id, size, NULL); + break; + + default: + return NULL; + } + + return pixbuf; +} + +/* Kudos to the gnome-panel guys. */ +static void +colorshift_pixbuf(GdkPixbuf *dest, GdkPixbuf *src, int shift) +{ + gint i, j; + gint width, height, has_alpha, src_rowstride, dest_rowstride; + guchar *target_pixels; + guchar *original_pixels; + guchar *pix_src; + guchar *pix_dest; + int val; + guchar r, g, b; + + has_alpha = gdk_pixbuf_get_has_alpha(src); + width = gdk_pixbuf_get_width(src); + height = gdk_pixbuf_get_height(src); + src_rowstride = gdk_pixbuf_get_rowstride(src); + dest_rowstride = gdk_pixbuf_get_rowstride(dest); + original_pixels = gdk_pixbuf_get_pixels(src); + target_pixels = gdk_pixbuf_get_pixels(dest); + + for (i = 0; i < height; i++) + { + pix_dest = target_pixels + i * dest_rowstride; + pix_src = original_pixels + i * src_rowstride; + + for (j = 0; j < width; j++) + { + r = *(pix_src++); + g = *(pix_src++); + b = *(pix_src++); + + val = r + shift; + *(pix_dest++) = CLAMP(val, 0, 255); + + val = g + shift; + *(pix_dest++) = CLAMP(val, 0, 255); + + val = b + shift; + *(pix_dest++) = CLAMP(val, 0, 255); + + if (has_alpha) + *(pix_dest++) = *(pix_src++); + } + } +} + +static void +draw_icon(GtkWidget *widget, SexyIconEntryPosition icon_pos) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + SexyIconInfo *icon_info = &entry->priv->icons[icon_pos]; + GdkPixbuf *pixbuf; + gint x, y, width, height; + + if (icon_info->icon == NULL || !GTK_WIDGET_REALIZED(widget)) + return; + + if ((pixbuf = get_pixbuf_from_icon(entry, icon_pos)) == NULL) + return; + + gdk_drawable_get_size(icon_info->window, &width, &height); + + if (width == 1 || height == 1) + { + /* + * size_allocate hasn't been called yet. These are the default values. + */ + return; + } + + if (gdk_pixbuf_get_height(pixbuf) > height) + { + GdkPixbuf *temp_pixbuf; + int scale; + + scale = height - (2 * ICON_MARGIN); + + temp_pixbuf = gdk_pixbuf_scale_simple(pixbuf, scale, scale, + GDK_INTERP_BILINEAR); + + g_object_unref(pixbuf); + + pixbuf = temp_pixbuf; + } + + x = (width - gdk_pixbuf_get_width(pixbuf)) / 2; + y = (height - gdk_pixbuf_get_height(pixbuf)) / 2; + + if (icon_info->hovered) + { + GdkPixbuf *temp_pixbuf; + + temp_pixbuf = gdk_pixbuf_copy(pixbuf); + + colorshift_pixbuf(temp_pixbuf, pixbuf, 30); + + g_object_unref(pixbuf); + + pixbuf = temp_pixbuf; + } + + gdk_draw_pixbuf(icon_info->window, widget->style->black_gc, pixbuf, + 0, 0, x, y, -1, -1, + GDK_RGB_DITHER_NORMAL, 0, 0); + + g_object_unref(pixbuf); +} + +static gint +sexy_icon_entry_expose(GtkWidget *widget, GdkEventExpose *event) +{ + SexyIconEntry *entry; + + g_return_val_if_fail(SEXY_IS_ICON_ENTRY(widget), FALSE); + g_return_val_if_fail(event != NULL, FALSE); + + entry = SEXY_ICON_ENTRY(widget); + + if (GTK_WIDGET_DRAWABLE(widget)) + { + gboolean found = FALSE; + int i; + + for (i = 0; i < MAX_ICONS && !found; i++) + { + SexyIconInfo *icon_info = &entry->priv->icons[i]; + + if (event->window == icon_info->window) + { + gint width; + GtkAllocation text_area_alloc; + + get_text_area_size(entry, &text_area_alloc); + gdk_drawable_get_size(icon_info->window, &width, NULL); + + gtk_paint_flat_box(widget->style, icon_info->window, + GTK_WIDGET_STATE(widget), GTK_SHADOW_NONE, + NULL, widget, "entry_bg", + 0, 0, width, text_area_alloc.height); + + draw_icon(widget, i); + + found = TRUE; + } + } + + if (!found) + GTK_WIDGET_CLASS(parent_class)->expose_event(widget, event); + } + + return FALSE; +} + +static void +update_icon(GObject *obj, GParamSpec *param, SexyIconEntry *entry) +{ + if (param != NULL) + { + const char *name = g_param_spec_get_name(param); + + if (strcmp(name, "pixbuf") && strcmp(name, "stock") && + strcmp(name, "image") && strcmp(name, "pixmap") && + strcmp(name, "icon_set") && strcmp(name, "pixbuf_animation")) + { + return; + } + } + + gtk_widget_queue_resize(GTK_WIDGET(entry)); +} + +static gint +sexy_icon_entry_enter_notify(GtkWidget *widget, GdkEventCrossing *event) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + for (i = 0; i < MAX_ICONS; i++) + { + if (event->window == entry->priv->icons[i].window) + { + if (sexy_icon_entry_get_icon_highlight(entry, i)) + { + entry->priv->icons[i].hovered = TRUE; + + update_icon(NULL, NULL, entry); + + break; + } + } + } + + return FALSE; +} + +static gint +sexy_icon_entry_leave_notify(GtkWidget *widget, GdkEventCrossing *event) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + for (i = 0; i < MAX_ICONS; i++) + { + if (event->window == entry->priv->icons[i].window) + { + if (sexy_icon_entry_get_icon_highlight(entry, i)) + { + entry->priv->icons[i].hovered = FALSE; + + update_icon(NULL, NULL, entry); + + break; + } + } + } + + return FALSE; +} + +static gint +sexy_icon_entry_button_press(GtkWidget *widget, GdkEventButton *event) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + for (i = 0; i < MAX_ICONS; i++) + { + if (event->window == entry->priv->icons[i].window) + { + if (event->button == 1 && + sexy_icon_entry_get_icon_highlight(entry, i)) + { + entry->priv->icons[i].hovered = FALSE; + + update_icon(NULL, NULL, entry); + } + + g_signal_emit(entry, signals[ICON_PRESSED], 0, i, event->button); + + return TRUE; + } + } + + if (GTK_WIDGET_CLASS(parent_class)->button_press_event) + return GTK_WIDGET_CLASS(parent_class)->button_press_event(widget, + event); + + return FALSE; +} + +static gint +sexy_icon_entry_button_release(GtkWidget *widget, GdkEventButton *event) +{ + SexyIconEntry *entry = SEXY_ICON_ENTRY(widget); + int i; + + for (i = 0; i < MAX_ICONS; i++) + { + GdkWindow *icon_window = entry->priv->icons[i].window; + + if (event->window == icon_window) + { + int width, height; + gdk_drawable_get_size(icon_window, &width, &height); + + if (event->button == 1 && + sexy_icon_entry_get_icon_highlight(entry, i) && + event->x >= 0 && event->y >= 0 && + event->x <= width && event->y <= height) + { + entry->priv->icons[i].hovered = TRUE; + + update_icon(NULL, NULL, entry); + } + + g_signal_emit(entry, signals[ICON_RELEASED], 0, i, event->button); + + return TRUE; + } + } + + if (GTK_WIDGET_CLASS(parent_class)->button_release_event) + return GTK_WIDGET_CLASS(parent_class)->button_release_event(widget, + event); + + return FALSE; +} + +/** + * sexy_icon_entry_new + * + * Creates a new SexyIconEntry widget. + * + * Returns a new #SexyIconEntry. + */ +GtkWidget * +sexy_icon_entry_new(void) +{ + return GTK_WIDGET(g_object_new(SEXY_TYPE_ICON_ENTRY, NULL)); +} + +/** + * sexy_icon_entry_set_icon + * @entry: A #SexyIconEntry. + * @position: Icon position. + * @icon: A #GtkImage to set as the icon. + * + * Sets the icon shown in the entry + */ +void +sexy_icon_entry_set_icon(SexyIconEntry *entry, SexyIconEntryPosition icon_pos, + GtkImage *icon) +{ + SexyIconInfo *icon_info; + + g_return_if_fail(entry != NULL); + g_return_if_fail(SEXY_IS_ICON_ENTRY(entry)); + g_return_if_fail(IS_VALID_ICON_ENTRY_POSITION(icon_pos)); + g_return_if_fail(icon == NULL || GTK_IS_IMAGE(icon)); + + icon_info = &entry->priv->icons[icon_pos]; + + if (icon == icon_info->icon) + return; + + if (icon_pos == SEXY_ICON_ENTRY_SECONDARY && + entry->priv->icon_released_id != 0) + { + g_signal_handler_disconnect(entry, entry->priv->icon_released_id); + entry->priv->icon_released_id = 0; + } + + if (icon == NULL) + { + if (icon_info->icon != NULL) + { + gtk_widget_destroy(GTK_WIDGET(icon_info->icon)); + icon_info->icon = NULL; + + /* + * Explicitly check, as the pointer may become invalidated + * during destruction. + */ + if (icon_info->window != NULL && GDK_IS_WINDOW(icon_info->window)) + gdk_window_hide(icon_info->window); + } + } + else + { + if (icon_info->window != NULL && icon_info->icon == NULL) + gdk_window_show(icon_info->window); + + g_signal_connect(G_OBJECT(icon), "notify", + G_CALLBACK(update_icon), entry); + + icon_info->icon = icon; + g_object_ref(icon); + } + + update_icon(NULL, NULL, entry); +} + +/** + * sexy_icon_entry_set_icon_highlight + * @entry: A #SexyIconEntry; + * @position: Icon position. + * @highlight: TRUE if the icon should highlight on mouse-over + * + * Determines whether the icon will highlight on mouse-over. + */ +void +sexy_icon_entry_set_icon_highlight(SexyIconEntry *entry, + SexyIconEntryPosition icon_pos, + gboolean highlight) +{ + SexyIconInfo *icon_info; + + g_return_if_fail(entry != NULL); + g_return_if_fail(SEXY_IS_ICON_ENTRY(entry)); + g_return_if_fail(IS_VALID_ICON_ENTRY_POSITION(icon_pos)); + + icon_info = &entry->priv->icons[icon_pos]; + + if (icon_info->highlight == highlight) + return; + + icon_info->highlight = highlight; +} + +/** + * sexy_icon_entry_get_icon + * @entry: A #SexyIconEntry. + * @position: Icon position. + * + * Retrieves the image used for the icon + * + * Returns: A #GtkImage. + */ +GtkImage * +sexy_icon_entry_get_icon(const SexyIconEntry *entry, + SexyIconEntryPosition icon_pos) +{ + g_return_val_if_fail(entry != NULL, NULL); + g_return_val_if_fail(SEXY_IS_ICON_ENTRY(entry), NULL); + g_return_val_if_fail(IS_VALID_ICON_ENTRY_POSITION(icon_pos), NULL); + + return entry->priv->icons[icon_pos].icon; +} + +/** + * sexy_icon_entry_get_icon_highlight + * @entry: A #SexyIconEntry. + * @position: Icon position. + * + * Retrieves whether entry will highlight the icon on mouseover. + * + * Returns: TRUE if icon highlights. + */ +gboolean +sexy_icon_entry_get_icon_highlight(const SexyIconEntry *entry, + SexyIconEntryPosition icon_pos) +{ + g_return_val_if_fail(entry != NULL, FALSE); + g_return_val_if_fail(SEXY_IS_ICON_ENTRY(entry), FALSE); + g_return_val_if_fail(IS_VALID_ICON_ENTRY_POSITION(icon_pos), FALSE); + + return entry->priv->icons[icon_pos].highlight; +} + +static void +clear_button_clicked_cb(SexyIconEntry *icon_entry, + SexyIconEntryPosition icon_pos, + int button) +{ + if (icon_pos != SEXY_ICON_ENTRY_SECONDARY || button != 1) + return; + + gtk_entry_set_text(GTK_ENTRY(icon_entry), ""); +} + +/** + * sexy_icon_entry_add_clear_button + * @icon_entry: A #SexyIconEntry. + * + * A convenience function to add a clear button to the end of the entry. + * This is useful for search boxes. + */ +void +sexy_icon_entry_add_clear_button(SexyIconEntry *icon_entry) +{ + GtkWidget *icon; + + g_return_if_fail(icon_entry != NULL); + g_return_if_fail(SEXY_IS_ICON_ENTRY(icon_entry)); + + icon = gtk_image_new_from_stock(GTK_STOCK_CLEAR, GTK_ICON_SIZE_MENU); + gtk_widget_show(icon); + sexy_icon_entry_set_icon(SEXY_ICON_ENTRY(icon_entry), + SEXY_ICON_ENTRY_SECONDARY, + GTK_IMAGE(icon)); + sexy_icon_entry_set_icon_highlight(SEXY_ICON_ENTRY(icon_entry), + SEXY_ICON_ENTRY_SECONDARY, TRUE); + + if (icon_entry->priv->icon_released_id != 0) + { + g_signal_handler_disconnect(icon_entry, + icon_entry->priv->icon_released_id); + } + + icon_entry->priv->icon_released_id = + g_signal_connect(G_OBJECT(icon_entry), "icon_released", + G_CALLBACK(clear_button_clicked_cb), NULL); +} + +GType +sexy_icon_entry_position_get_type (void) +{ + static GType etype = 0; + if (etype == 0) { + static const GEnumValue values[] = { + { SEXY_ICON_ENTRY_PRIMARY, "SEXY_ICON_ENTRY_PRIMARY", "primary" }, + { SEXY_ICON_ENTRY_SECONDARY, "SEXY_ICON_ENTRY_SECONDARY", "secondary" }, + { 0, NULL, NULL } + }; + etype = g_enum_register_static ("SexyIconEntryPosition", values); + } + return etype; +} + diff --git a/sugar/sexy-icon-entry.h b/sugar/sexy-icon-entry.h new file mode 100644 index 0000000..eb83fed --- /dev/null +++ b/sugar/sexy-icon-entry.h @@ -0,0 +1,104 @@ +/* + * @file libsexy/sexy-icon-entry.h Entry widget + * + * @Copyright (C) 2004-2006 Christian Hammond. + * + * 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. + */ +#ifndef _SEXY_ICON_ENTRY_H_ +#define _SEXY_ICON_ENTRY_H_ + +typedef struct _SexyIconEntry SexyIconEntry; +typedef struct _SexyIconEntryClass SexyIconEntryClass; +typedef struct _SexyIconEntryPriv SexyIconEntryPriv; + +#include +#include + +#define SEXY_TYPE_ICON_ENTRY (sexy_icon_entry_get_type()) +#define SEXY_ICON_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), SEXY_TYPE_ICON_ENTRY, SexyIconEntry)) +#define SEXY_ICON_ENTRY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), SEXY_TYPE_ICON_ENTRY, SexyIconEntryClass)) +#define SEXY_IS_ICON_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), SEXY_TYPE_ICON_ENTRY)) +#define SEXY_IS_ICON_ENTRY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), SEXY_TYPE_ICON_ENTRY)) +#define SEXY_ICON_ENTRY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), SEXY_TYPE_ICON_ENTRY, SexyIconEntryClass)) + +typedef enum +{ + SEXY_ICON_ENTRY_PRIMARY, + SEXY_ICON_ENTRY_SECONDARY + +} SexyIconEntryPosition; + +GType sexy_icon_entry_position_get_type(void); +#define SEXY_TYPE_ICON_ENTRY_POSITION (sexy_icon_entry_position_get_type()) + +struct _SexyIconEntry +{ + GtkEntry parent_object; + + SexyIconEntryPriv *priv; + + void (*gtk_reserved1)(void); + void (*gtk_reserved2)(void); + void (*gtk_reserved3)(void); + void (*gtk_reserved4)(void); +}; + +struct _SexyIconEntryClass +{ + GtkEntryClass parent_class; + + /* Signals */ + void (*icon_pressed)(SexyIconEntry *entry, SexyIconEntryPosition icon_pos, + int button); + void (*icon_released)(SexyIconEntry *entry, SexyIconEntryPosition icon_pos, + int button); + + void (*gtk_reserved1)(void); + void (*gtk_reserved2)(void); + void (*gtk_reserved3)(void); + void (*gtk_reserved4)(void); +}; + +G_BEGIN_DECLS + +GType sexy_icon_entry_get_type(void); + +GtkWidget *sexy_icon_entry_new(void); + +void sexy_icon_entry_set_icon(SexyIconEntry *entry, + SexyIconEntryPosition position, + GtkImage *icon); + +void sexy_icon_entry_set_icon_highlight(SexyIconEntry *entry, + SexyIconEntryPosition position, + gboolean highlight); + +GtkImage *sexy_icon_entry_get_icon(const SexyIconEntry *entry, + SexyIconEntryPosition position); + +gboolean sexy_icon_entry_get_icon_highlight(const SexyIconEntry *entry, + SexyIconEntryPosition position); +void sexy_icon_entry_add_clear_button(SexyIconEntry *icon_entry); + +G_END_DECLS + +#endif /* _SEXY_ICON_ENTRY_H_ */ diff --git a/sugar/sugar-address-entry.c b/sugar/sugar-address-entry.c new file mode 100644 index 0000000..7b6d525 --- /dev/null +++ b/sugar/sugar-address-entry.c @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2006-2007 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include +#include + +#include "sugar-address-entry.h" + +enum { + PROP_0, + PROP_PROGRESS, + PROP_ADDRESS, + PROP_TITLE +}; + +typedef enum { + CURSOR_STANDARD, + CURSOR_DND +} CursorType; + +static void _gtk_entry_effective_inner_border (GtkEntry *entry, + GtkBorder *border); +static void get_text_area_size (GtkEntry *entry, + gint *x, + gint *y, + gint *width, + gint *height); + +G_DEFINE_TYPE(SugarAddressEntry, sugar_address_entry, GTK_TYPE_ENTRY) + +static GQuark quark_inner_border = 0; +static const GtkBorder default_inner_border = { 2, 2, 2, 2 }; + +static void +draw_insertion_cursor (GtkEntry *entry, + GdkRectangle *cursor_location, + gboolean is_primary, + PangoDirection direction, + gboolean draw_arrow) +{ + GtkWidget *widget = GTK_WIDGET (entry); + GtkTextDirection text_dir; + + if (direction == PANGO_DIRECTION_LTR) + text_dir = GTK_TEXT_DIR_LTR; + else + text_dir = GTK_TEXT_DIR_RTL; + + gtk_draw_insertion_cursor (widget, entry->text_area, NULL, + cursor_location, + is_primary, text_dir, draw_arrow); +} + +static void +gtk_entry_get_pixel_ranges (GtkEntry *entry, + gint **ranges, + gint *n_ranges) +{ + gint start_char, end_char; + + if (gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start_char, &end_char)) + { + //PangoLayout *layout = gtk_entry_ensure_layout (entry, TRUE); + PangoLayout *layout = gtk_entry_get_layout (entry); + PangoLayoutLine *line = pango_layout_get_lines (layout)->data; + const char *text = pango_layout_get_text (layout); + gint start_index = g_utf8_offset_to_pointer (text, start_char) - text; + gint end_index = g_utf8_offset_to_pointer (text, end_char) - text; + gint real_n_ranges, i; + + pango_layout_line_get_x_ranges (line, start_index, end_index, ranges, &real_n_ranges); + + if (ranges) + { + gint *r = *ranges; + + for (i = 0; i < real_n_ranges; ++i) + { + r[2 * i + 1] = (r[2 * i + 1] - r[2 * i]) / PANGO_SCALE; + r[2 * i] = r[2 * i] / PANGO_SCALE; + } + } + + if (n_ranges) + *n_ranges = real_n_ranges; + } + else + { + if (n_ranges) + *n_ranges = 0; + if (ranges) + *ranges = NULL; + } +} + +static void +gtk_entry_get_cursor_locations (GtkEntry *entry, + CursorType type, + gint *strong_x, + gint *weak_x) +{ + if (!entry->visible && !entry->invisible_char) + { + if (strong_x) + *strong_x = 0; + + if (weak_x) + *weak_x = 0; + } + else + { + //PangoLayout *layout = gtk_entry_ensure_layout (entry, TRUE); + PangoLayout *layout = gtk_entry_get_layout (entry); + const gchar *text = pango_layout_get_text (layout); + PangoRectangle strong_pos, weak_pos; + gint index; + + if (type == CURSOR_STANDARD) + { + index = g_utf8_offset_to_pointer (text, entry->current_pos + entry->preedit_cursor) - text; + } + else /* type == CURSOR_DND */ + { + index = g_utf8_offset_to_pointer (text, entry->dnd_position) - text; + + if (entry->dnd_position > entry->current_pos) + { + if (entry->visible) + index += entry->preedit_length; + else + { + gint preedit_len_chars = g_utf8_strlen (text, -1) - entry->text_length; + index += preedit_len_chars * g_unichar_to_utf8 (entry->invisible_char, NULL); + } + } + } + + pango_layout_get_cursor_pos (layout, index, &strong_pos, &weak_pos); + + if (strong_x) + *strong_x = strong_pos.x / PANGO_SCALE; + + if (weak_x) + *weak_x = weak_pos.x / PANGO_SCALE; + } +} + +static void +gtk_entry_draw_cursor (GtkEntry *entry, + CursorType type) +{ + GdkKeymap *keymap = gdk_keymap_get_for_display (gtk_widget_get_display (GTK_WIDGET (entry))); + PangoDirection keymap_direction = gdk_keymap_get_direction (keymap); + + if (GTK_WIDGET_DRAWABLE (entry)) + { + GtkWidget *widget = GTK_WIDGET (entry); + GdkRectangle cursor_location; + gboolean split_cursor; + + GtkBorder inner_border; + gint xoffset; + gint strong_x, weak_x; + gint text_area_height; + PangoDirection dir1 = PANGO_DIRECTION_NEUTRAL; + PangoDirection dir2 = PANGO_DIRECTION_NEUTRAL; + gint x1 = 0; + gint x2 = 0; + + _gtk_entry_effective_inner_border (entry, &inner_border); + + xoffset = inner_border.left - entry->scroll_offset; + + gdk_drawable_get_size (entry->text_area, NULL, &text_area_height); + + gtk_entry_get_cursor_locations (entry, type, &strong_x, &weak_x); + + g_object_get (gtk_widget_get_settings (widget), + "gtk-split-cursor", &split_cursor, + NULL); + + dir1 = entry->resolved_dir; + + if (split_cursor) + { + x1 = strong_x; + + if (weak_x != strong_x) + { + dir2 = (entry->resolved_dir == PANGO_DIRECTION_LTR) ? PANGO_DIRECTION_RTL : PANGO_DIRECTION_LTR; + x2 = weak_x; + } + } + else + { + if (keymap_direction == entry->resolved_dir) + x1 = strong_x; + else + x1 = weak_x; + } + + cursor_location.x = xoffset + x1; + cursor_location.y = inner_border.top; + cursor_location.width = 0; + cursor_location.height = text_area_height - inner_border.top - inner_border.bottom; + + draw_insertion_cursor (entry, + &cursor_location, TRUE, dir1, + dir2 != PANGO_DIRECTION_NEUTRAL); + + if (dir2 != PANGO_DIRECTION_NEUTRAL) + { + cursor_location.x = xoffset + x2; + draw_insertion_cursor (entry, + &cursor_location, FALSE, dir2, + TRUE); + } + } +} + +static void +get_layout_position (GtkEntry *entry, + gint *x, + gint *y) +{ + PangoLayout *layout; + PangoRectangle logical_rect; + gint area_width, area_height; + GtkBorder inner_border; + gint y_pos; + PangoLayoutLine *line; + +// layout = gtk_entry_ensure_layout (entry, TRUE); + layout = gtk_entry_get_layout(entry); + + get_text_area_size (entry, NULL, NULL, &area_width, &area_height); + _gtk_entry_effective_inner_border (entry, &inner_border); + + area_height = PANGO_SCALE * (area_height - inner_border.top - inner_border.bottom); + + line = pango_layout_get_lines (layout)->data; + pango_layout_line_get_extents (line, NULL, &logical_rect); + + /* Align primarily for locale's ascent/descent */ + y_pos = ((area_height - entry->ascent - entry->descent) / 2 + + entry->ascent + logical_rect.y); + + /* Now see if we need to adjust to fit in actual drawn string */ + if (logical_rect.height > area_height) + y_pos = (area_height - logical_rect.height) / 2; + else if (y_pos < 0) + y_pos = 0; + else if (y_pos + logical_rect.height > area_height) + y_pos = area_height - logical_rect.height; + + y_pos = inner_border.top + y_pos / PANGO_SCALE; + + if (x) + *x = inner_border.left - entry->scroll_offset; + + if (y) + *y = y_pos; +} + +static void +_gtk_entry_effective_inner_border (GtkEntry *entry, + GtkBorder *border) +{ + GtkBorder *tmp_border; + + tmp_border = g_object_get_qdata (G_OBJECT (entry), quark_inner_border); + + if (tmp_border) + { + *border = *tmp_border; + return; + } + + gtk_widget_style_get (GTK_WIDGET (entry), "inner-border", &tmp_border, NULL); + + if (tmp_border) + { + *border = *tmp_border; + gtk_border_free (tmp_border); + return; + } + + *border = default_inner_border; +} + +static void +gtk_entry_draw_text (GtkEntry *entry) +{ + GtkWidget *widget; + + if (!entry->visible && entry->invisible_char == 0) + return; + + if (GTK_WIDGET_DRAWABLE (entry)) + { + //PangoLayout *layout = gtk_entry_ensure_layout (entry, TRUE); + PangoLayout *layout = gtk_entry_get_layout (entry); + cairo_t *cr; + gint x, y; + gint start_pos, end_pos; + + widget = GTK_WIDGET (entry); + + get_layout_position (entry, &x, &y); + + cr = gdk_cairo_create (entry->text_area); + + cairo_move_to (cr, x, y); + gdk_cairo_set_source_color (cr, &widget->style->text [widget->state]); + pango_cairo_show_layout (cr, layout); + + if (gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start_pos, &end_pos)) + { + gint *ranges; + gint n_ranges, i; + PangoRectangle logical_rect; + GdkColor *selection_color, *text_color; + GtkBorder inner_border; + + pango_layout_get_pixel_extents (layout, NULL, &logical_rect); + gtk_entry_get_pixel_ranges (entry, &ranges, &n_ranges); + + if (GTK_WIDGET_HAS_FOCUS (entry)) + { + selection_color = &widget->style->base [GTK_STATE_SELECTED]; + text_color = &widget->style->text [GTK_STATE_SELECTED]; + } + else + { + selection_color = &widget->style->base [GTK_STATE_ACTIVE]; + text_color = &widget->style->text [GTK_STATE_ACTIVE]; + } + + _gtk_entry_effective_inner_border (entry, &inner_border); + + for (i = 0; i < n_ranges; ++i) + cairo_rectangle (cr, + inner_border.left - entry->scroll_offset + ranges[2 * i], + y, + ranges[2 * i + 1], + logical_rect.height); + + cairo_clip (cr); + + gdk_cairo_set_source_color (cr, selection_color); + cairo_paint (cr); + + cairo_move_to (cr, x, y); + gdk_cairo_set_source_color (cr, text_color); + pango_cairo_show_layout (cr, layout); + + g_free (ranges); + } + + cairo_destroy (cr); + } +} + +static void +sugar_address_entry_get_borders (GtkEntry *entry, + gint *xborder, + gint *yborder) +{ + GtkWidget *widget = GTK_WIDGET (entry); + gint focus_width; + gboolean interior_focus; + + gtk_widget_style_get (widget, + "interior-focus", &interior_focus, + "focus-line-width", &focus_width, + NULL); + + if (entry->has_frame) + { + *xborder = widget->style->xthickness; + *yborder = widget->style->ythickness; + } + else + { + *xborder = 0; + *yborder = 0; + } + + if (!interior_focus) + { + *xborder += focus_width; + *yborder += focus_width; + } +} + +static void +get_text_area_size (GtkEntry *entry, + gint *x, + gint *y, + gint *width, + gint *height) +{ + gint xborder, yborder; + GtkRequisition requisition; + GtkWidget *widget = GTK_WIDGET (entry); + + gtk_widget_get_child_requisition (widget, &requisition); + + sugar_address_entry_get_borders (entry, &xborder, &yborder); + + if (x) + *x = xborder; + + if (y) + *y = yborder; + + if (width) + *width = GTK_WIDGET (entry)->allocation.width - xborder * 2; + + if (height) + *height = requisition.height - yborder * 2; +} + +static gint +sugar_address_entry_expose(GtkWidget *widget, + GdkEventExpose *event) +{ + GtkEntry *entry = GTK_ENTRY (widget); + SugarAddressEntry *address_entry = SUGAR_ADDRESS_ENTRY(widget); + cairo_t *cr; + + if (entry->text_area == event->window) { + gint area_width, area_height; + + get_text_area_size (entry, NULL, NULL, &area_width, &area_height); + +/* gtk_paint_flat_box (widget->style, entry->text_area, + GTK_WIDGET_STATE(widget), GTK_SHADOW_NONE, + NULL, widget, "entry_bg", + 0, 0, area_width, area_height); +*/ + + if (address_entry->progress != 0.0 && address_entry->progress != 1.0 && + !GTK_WIDGET_HAS_FOCUS(entry)) { + int bar_width = area_width * address_entry->progress; + float radius = area_height / 2; + + cr = gdk_cairo_create(entry->text_area); + cairo_set_source_rgb(cr, 0xA6 / 255.0, 0xA6 / 255.0, 0xA6 / 255.0); + + cairo_move_to (cr, radius, 0); + cairo_arc (cr, bar_width - radius, radius, radius, M_PI * 1.5, M_PI * 2); + cairo_arc (cr, bar_width - radius, area_height - radius, radius, 0, M_PI * 0.5); + cairo_arc (cr, radius, area_height - radius, radius, M_PI * 0.5, M_PI); + cairo_arc (cr, radius, radius, radius, M_PI, M_PI * 1.5); + + cairo_fill(cr); + cairo_destroy (cr); + } + + + if ((entry->visible || entry->invisible_char != 0) && + GTK_WIDGET_HAS_FOCUS (widget) && + entry->selection_bound == entry->current_pos && entry->cursor_visible) + gtk_entry_draw_cursor (GTK_ENTRY (widget), CURSOR_STANDARD); + + if (entry->dnd_position != -1) + gtk_entry_draw_cursor (GTK_ENTRY (widget), CURSOR_DND); + + gtk_entry_draw_text (GTK_ENTRY (widget)); + } else { + GtkWidgetClass *parent_class; + parent_class = GTK_WIDGET_CLASS(sugar_address_entry_parent_class); + parent_class->expose_event(widget, event); + } + + return FALSE; +} + +static void +entry_changed_cb(SugarAddressEntry *entry) +{ + if (entry->address) { + g_free (entry->address); + } + + entry->address = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1); +} + +static void +update_entry_text(SugarAddressEntry *address_entry, + gboolean has_focus) +{ + g_signal_handlers_block_by_func(address_entry, entry_changed_cb, NULL); + + if (has_focus || address_entry->title == NULL) { + gtk_entry_set_text(GTK_ENTRY(address_entry), + address_entry->address); + } else { + gtk_entry_set_text(GTK_ENTRY(address_entry), + address_entry->title); + } + + g_signal_handlers_unblock_by_func(address_entry, entry_changed_cb, NULL); +} + +static void +sugar_address_entry_set_address(SugarAddressEntry *address_entry, + const char *address) +{ + g_free(address_entry->address); + address_entry->address = g_strdup(address); + + update_entry_text(address_entry, + gtk_widget_is_focus(GTK_WIDGET(address_entry))); +} + +static void +sugar_address_entry_set_title(SugarAddressEntry *address_entry, + const char *title) +{ + g_free(address_entry->title); + address_entry->title = g_strdup(title); + + update_entry_text(address_entry, + gtk_widget_is_focus(GTK_WIDGET(address_entry))); +} + +static void +sugar_address_entry_set_property(GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SugarAddressEntry *address_entry = SUGAR_ADDRESS_ENTRY(object); + GtkEntry *entry = GTK_ENTRY(object); + + switch (prop_id) { + case PROP_PROGRESS: + address_entry->progress = g_value_get_double(value); + if (GTK_WIDGET_REALIZED(entry)) + gdk_window_invalidate_rect(entry->text_area, NULL, FALSE); + break; + case PROP_ADDRESS: + sugar_address_entry_set_address(address_entry, + g_value_get_string(value)); + break; + case PROP_TITLE: + sugar_address_entry_set_title(address_entry, + g_value_get_string(value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +sugar_address_entry_get_property(GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SugarAddressEntry *entry = SUGAR_ADDRESS_ENTRY(object); + + switch (prop_id) { + case PROP_PROGRESS: + g_value_set_double(value, entry->progress); + break; + case PROP_TITLE: + g_value_set_string(value, entry->title); + break; + case PROP_ADDRESS: + g_value_set_string(value, entry->address); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +sugar_address_entry_class_init(SugarAddressEntryClass *klass) +{ + GtkWidgetClass *widget_class = (GtkWidgetClass*)klass; + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + widget_class->expose_event = sugar_address_entry_expose; + + gobject_class->set_property = sugar_address_entry_set_property; + gobject_class->get_property = sugar_address_entry_get_property; + + quark_inner_border = g_quark_from_static_string ("gtk-entry-inner-border"); + + g_object_class_install_property (gobject_class, PROP_PROGRESS, + g_param_spec_double("progress", + "Progress", + "Progress", + 0.0, 1.0, 0.0, + G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, PROP_TITLE, + g_param_spec_string("title", + "Title", + "Title", + "", + G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, PROP_ADDRESS, + g_param_spec_string("address", + "Address", + "Address", + "", + G_PARAM_READWRITE)); +} + +static gboolean +button_press_event_cb (GtkWidget *widget, GdkEventButton *event) +{ + if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) { + gtk_editable_select_region(GTK_EDITABLE(widget), 0, -1); + gtk_widget_grab_focus(widget); + + return TRUE; + } + + return FALSE; +} + +static gboolean +focus_in_event_cb(GtkWidget *widget, GdkEventFocus *event) +{ + update_entry_text(SUGAR_ADDRESS_ENTRY(widget), TRUE); + return FALSE; +} + +static gboolean +focus_out_event_cb(GtkWidget *widget, GdkEventFocus *event) +{ + update_entry_text(SUGAR_ADDRESS_ENTRY(widget), FALSE); + return FALSE; +} + +static void +popup_unmap_cb(GtkWidget *popup, SugarAddressEntry *entry) +{ + g_signal_handlers_unblock_by_func(entry, focus_out_event_cb, NULL); +} + +static void +populate_popup_cb(SugarAddressEntry *entry, GtkWidget *menu) +{ + g_signal_handlers_block_by_func(entry, focus_out_event_cb, NULL); + + g_signal_connect(menu, "unmap", + G_CALLBACK(popup_unmap_cb), entry); +} + +static void +sugar_address_entry_init(SugarAddressEntry *entry) +{ + entry->progress = 0.0; + entry->address = NULL; + entry->title = g_strdup(""); + + g_signal_connect(entry, "focus-in-event", + G_CALLBACK(focus_in_event_cb), NULL); + g_signal_connect(entry, "focus-out-event", + G_CALLBACK(focus_out_event_cb), NULL); + g_signal_connect(entry, "changed", + G_CALLBACK(entry_changed_cb), NULL); + g_signal_connect(entry, "button-press-event", + G_CALLBACK(button_press_event_cb), NULL); + g_signal_connect(entry, "populate-popup", + G_CALLBACK(populate_popup_cb), NULL); +} diff --git a/sugar/sugar-address-entry.h b/sugar/sugar-address-entry.h new file mode 100644 index 0000000..60c56ab --- /dev/null +++ b/sugar/sugar-address-entry.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2006-2007 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __SUGAR_ADDRESS_ENTRY_H__ +#define __SUGAR_ADDRESS_ENTRY_H__ + +#include + +G_BEGIN_DECLS + +typedef struct _SugarAddressEntry SugarAddressEntry; +typedef struct _SugarAddressEntryClass SugarAddressEntryClass; +typedef struct _SugarAddressEntryPrivate SugarAddressEntryPrivate; + +#define SUGAR_TYPE_ADDRESS_ENTRY (sugar_address_entry_get_type()) +#define SUGAR_ADDRESS_ENTRY(object) (G_TYPE_CHECK_INSTANCE_CAST((object), SUGAR_TYPE_ADDRESS_ENTRY, SugarAddressEntry)) +#define SUGAR_ADDRESS_ENTRY_CLASS(klass) (G_TYPE_CHACK_CLASS_CAST((klass), SUGAR_TYPE_ADDRESS_ENTRY, SugarAddressEntryClass)) +#define SUGAR_IS_ADDRESS_ENTRY(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), SUGAR_TYPE_ADDRESS_ENTRY)) +#define SUGAR_IS_ADDRESS_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SUGAR_TYPE_ADDRESS_ENTRY)) +#define SUGAR_ADDRESS_ENTRY_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_ADDRESS_ENTRY, SugarAddressEntryClass)) + +struct _SugarAddressEntry { + GtkEntry base_instance; + + float progress; + char *title; + char *address; +}; + +struct _SugarAddressEntryClass { + GtkEntryClass base_class; +}; + +GType sugar_address_entry_get_type (void); + +G_END_DECLS + +#endif /* __SUGAR_ADDRESS_ENTRY_H__ */ diff --git a/sugar/sugar-key-grabber.c b/sugar/sugar-key-grabber.c new file mode 100644 index 0000000..baddab5 --- /dev/null +++ b/sugar/sugar-key-grabber.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2006-2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include +#include +#include +#include + +#include "sugar-key-grabber.h" +#include "eggaccelerators.h" +#include "sugar-marshal.h" + +/* we exclude shift, GDK_CONTROL_MASK and GDK_MOD1_MASK since we know what + these modifiers mean + these are the mods whose combinations are bound by the keygrabbing code */ +#define IGNORED_MODS (0x2000 /*Xkb modifier*/ | GDK_LOCK_MASK | \ + GDK_MOD2_MASK | GDK_MOD3_MASK | GDK_MOD4_MASK | GDK_MOD5_MASK) +/* these are the ones we actually use for global keys, we always only check + * for these set */ +#define USED_MODS (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK) + +enum { + KEY_PRESSED, + KEY_RELEASED, + N_SIGNALS +}; + +typedef struct { + char *key; + guint keysym; + guint state; + guint keycode; +} Key; + +G_DEFINE_TYPE(SugarKeyGrabber, sugar_key_grabber, G_TYPE_OBJECT) + +static guint signals[N_SIGNALS]; + +static void +free_key_info(Key *key_info) +{ + g_free(key_info->key); + g_free(key_info); +} + +static void +sugar_key_grabber_dispose (GObject *object) +{ + SugarKeyGrabber *grabber = SUGAR_KEY_GRABBER(object); + + if (grabber->keys) { + g_list_foreach(grabber->keys, (GFunc)free_key_info, NULL); + g_list_free(grabber->keys); + grabber->keys = NULL; + } +} + +static void +sugar_key_grabber_class_init(SugarKeyGrabberClass *grabber_class) +{ + GObjectClass *g_object_class = G_OBJECT_CLASS (grabber_class); + + g_object_class->dispose = sugar_key_grabber_dispose; + + signals[KEY_PRESSED] = g_signal_new ("key-pressed", + G_TYPE_FROM_CLASS (grabber_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (SugarKeyGrabberClass, key_pressed), + NULL, NULL, + sugar_marshal_BOOLEAN__UINT_UINT, + G_TYPE_BOOLEAN, 2, + G_TYPE_UINT, + G_TYPE_UINT); + signals[KEY_RELEASED] = g_signal_new ("key-released", + G_TYPE_FROM_CLASS (grabber_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (SugarKeyGrabberClass, key_released), + NULL, NULL, + sugar_marshal_BOOLEAN__UINT_UINT, + G_TYPE_BOOLEAN, 2, + G_TYPE_UINT, + G_TYPE_UINT); +} + +char * +sugar_key_grabber_get_key(SugarKeyGrabber *grabber, guint keycode, guint state) +{ + GList *l; + + for (l = grabber->keys; l != NULL; l = l->next) { + Key *keyinfo = (Key *)l->data; + if ((keyinfo->keycode == keycode) && + ((state & USED_MODS) == keyinfo->state)) { + return g_strdup(keyinfo->key); + } + } + + return NULL; +} + +static GdkFilterReturn +filter_events(GdkXEvent *xevent, GdkEvent *event, gpointer data) +{ + SugarKeyGrabber *grabber = (SugarKeyGrabber *)data; + XEvent *xev = (XEvent *)xevent; + + if (xev->type == KeyRelease) { + int return_value; + g_signal_emit (grabber, signals[KEY_RELEASED], 0, xev->xkey.keycode, + xev->xkey.state, &return_value); + if(return_value) + return GDK_FILTER_REMOVE; + } + + if (xev->type == KeyPress) { + int return_value; + g_signal_emit (grabber, signals[KEY_PRESSED], 0, xev->xkey.keycode, + xev->xkey.state, &return_value); + if(return_value) + return GDK_FILTER_REMOVE; + } + + return GDK_FILTER_CONTINUE; +} + +static void +sugar_key_grabber_init(SugarKeyGrabber *grabber) +{ + GdkScreen *screen; + + screen = gdk_screen_get_default(); + grabber->root = gdk_screen_get_root_window(screen); + grabber->keys = NULL; + + gdk_window_add_filter(grabber->root, filter_events, grabber); +} + +/* grab_key and grab_key_real are from + * gnome-control-center/gnome-settings-daemon/gnome-settings-multimedia-keys.c + */ + +static gboolean +grab_key_real (Key *key, GdkWindow *root, gboolean grab, int result) +{ + gdk_error_trap_push (); + if (grab) + XGrabKey (GDK_DISPLAY(), key->keycode, (result | key->state), + GDK_WINDOW_XID (root), True, GrabModeAsync, GrabModeAsync); + else + XUngrabKey(GDK_DISPLAY(), key->keycode, (result | key->state), + GDK_WINDOW_XID (root)); + gdk_flush (); + + gdk_error_trap_pop (); + + return TRUE; +} + +#define N_BITS 32 +static void +grab_key (SugarKeyGrabber *grabber, Key *key, gboolean grab) +{ + int indexes[N_BITS];/*indexes of bits we need to flip*/ + int i, bit, bits_set_cnt; + int uppervalue; + guint mask_to_traverse = IGNORED_MODS & ~ key->state; + + bit = 0; + for (i = 0; i < N_BITS; i++) { + if (mask_to_traverse & (1<root, grab, result) == FALSE) + return; + } +} + +void +sugar_key_grabber_grab(SugarKeyGrabber *grabber, const char *key) +{ + Key *keyinfo; + + keyinfo = g_new0 (Key, 1); + keyinfo->key = g_strdup(key); + egg_accelerator_parse_virtual (key, &keyinfo->keysym, + &keyinfo->keycode, &keyinfo->state); + + grab_key(grabber, keyinfo, TRUE); + + grabber->keys = g_list_append(grabber->keys, keyinfo); +} diff --git a/sugar/sugar-key-grabber.h b/sugar/sugar-key-grabber.h new file mode 100644 index 0000000..5b734e7 --- /dev/null +++ b/sugar/sugar-key-grabber.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2006-2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __SUGAR_KEY_GRABBER_H__ +#define __SUGAR_KEY_GRABBER_H__ + +#include +#include + +G_BEGIN_DECLS + +typedef struct _SugarKeyGrabber SugarKeyGrabber; +typedef struct _SugarKeyGrabberClass SugarKeyGrabberClass; +typedef struct _SugarKeyGrabberPrivate SugarKeyGrabberPrivate; + +#define SUGAR_TYPE_KEY_GRABBER (sugar_key_grabber_get_type()) +#define SUGAR_KEY_GRABBER(object) (G_TYPE_CHECK_INSTANCE_CAST((object), SUGAR_TYPE_KEY_GRABBER, SugarKeyGrabber)) +#define SUGAR_KEY_GRABBER_CLASS(klass) (G_TYPE_CHACK_CLASS_CAST((klass), SUGAR_TYPE_KEY_GRABBER, SugarKeyGrabberClass)) +#define SUGAR_IS_KEY_GRABBER(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), SUGAR_TYPE_KEY_GRABBER)) +#define SUGAR_IS_KEYGRABBER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SUGAR_TYPE_KEY_GRABBER)) +#define SUGAR_KEY_GRABBER_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_KEY_GRABBER, SugarKeyGrabberClass)) + +struct _SugarKeyGrabber { + GObject base_instance; + + GdkWindow *root; + GList *keys; +}; + +struct _SugarKeyGrabberClass { + GObjectClass base_class; + + gboolean (* key_pressed) (SugarKeyGrabber *grabber, + guint keycode, + guint state); + gboolean (* key_released) (SugarKeyGrabber *grabber, + guint keycode, + guint state); +}; + +GType sugar_key_grabber_get_type (void); +void sugar_key_grabber_grab (SugarKeyGrabber *grabber, + const char *key); +char *sugar_key_grabber_get_key (SugarKeyGrabber *grabber, + guint keycode, + guint state); + +G_END_DECLS + +#endif /* __SUGAR_KEY_GRABBER_H__ */ diff --git a/sugar/sugar-marshal.list b/sugar/sugar-marshal.list new file mode 100644 index 0000000..41ce620 --- /dev/null +++ b/sugar/sugar-marshal.list @@ -0,0 +1 @@ +BOOLEAN:UINT,UINT diff --git a/sugar/sugar-menu.c b/sugar/sugar-menu.c new file mode 100644 index 0000000..f19dc4b --- /dev/null +++ b/sugar/sugar-menu.c @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2006-2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include + +#include "sugar-menu.h" + +static void sugar_menu_class_init (SugarMenuClass *menu_class); +static void sugar_menu_init (SugarMenu *menu); + + +G_DEFINE_TYPE(SugarMenu, sugar_menu, GTK_TYPE_MENU) + +void +sugar_menu_set_active(SugarMenu *menu, gboolean active) +{ + GTK_MENU_SHELL(menu)->active = active; +} + +void +sugar_menu_embed(SugarMenu *menu, GtkContainer *parent) +{ + menu->orig_toplevel = GTK_MENU(menu)->toplevel; + + GTK_MENU(menu)->toplevel = gtk_widget_get_toplevel(GTK_WIDGET(parent)); + gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(parent)); +} + +void +sugar_menu_unembed(SugarMenu *menu) +{ + if (menu->orig_toplevel) { + GTK_MENU(menu)->toplevel = menu->orig_toplevel; + gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(menu->orig_toplevel)); + } +} + +static void +sugar_menu_class_init(SugarMenuClass *menu_class) +{ +} + +static void +sugar_menu_init(SugarMenu *menu) +{ + menu->orig_toplevel = NULL; +} diff --git a/sugar/sugar-menu.h b/sugar/sugar-menu.h new file mode 100644 index 0000000..8773a31 --- /dev/null +++ b/sugar/sugar-menu.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2006-2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __SUGAR_MENU_H__ +#define __SUGAR_MENU_H__ + +#include + +G_BEGIN_DECLS + +typedef struct _SugarMenu SugarMenu; +typedef struct _SugarMenuClass SugarMenuClass; + +#define SUGAR_TYPE_MENU (sugar_menu_get_type()) +#define SUGAR_MENU(object) (G_TYPE_CHECK_INSTANCE_CAST((object), SUGAR_TYPE_MENU, SugarMenu)) +#define SUGAR_MENU_CLASS(klass) (G_TYPE_CHACK_CLASS_CAST((klass), SUGAR_TYPE_MENU, SugarMenuClass)) +#define SUGAR_IS_MENU(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), SUGAR_TYPE_MENU)) +#define SUGAR_IS_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SUGAR_TYPE_MENU)) +#define SUGAR_MENU_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_MENU, SugarMenuClass)) + +struct _SugarMenu { + GtkMenu base_instance; + + GtkWidget *orig_toplevel; + int min_width; +}; + +struct _SugarMenuClass { + GtkMenuClass base_class; +}; + +GType sugar_menu_get_type (void); +void sugar_menu_set_active (SugarMenu *menu, + gboolean active); +void sugar_menu_embed (SugarMenu *menu, + GtkContainer *parent); + +G_END_DECLS + +#endif /* __SUGAR_MENU_H__ */ diff --git a/sugar/sugar-preview.c b/sugar/sugar-preview.c new file mode 100644 index 0000000..f54045b --- /dev/null +++ b/sugar/sugar-preview.c @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include +#include + +#include "sugar-preview.h" + +static void sugar_preview_class_init (SugarPreviewClass *menu_class); +static void sugar_preview_init (SugarPreview *menu); + +G_DEFINE_TYPE(SugarPreview, sugar_preview, G_TYPE_OBJECT) + +void +sugar_preview_set_size(SugarPreview *preview, int width, int height) +{ + preview->width = width; + preview->height = height; +} + +GdkPixbuf * +sugar_preview_get_pixbuf(SugarPreview *preview) +{ + GdkPixbuf *pixbuf; + + if (preview->pixbuf != NULL) { + return preview->pixbuf; + } + + if (preview->image == NULL) { + return NULL; + } + + preview->pixbuf = gdk_pixbuf_get_from_image(NULL, preview->image, NULL, + 0, 0, 0, 0, + preview->image->width, + preview->image->height); + g_object_unref(G_OBJECT(preview->image)); + preview->image = NULL; + + return preview->pixbuf; +} + +void +sugar_preview_clear(SugarPreview *preview) +{ + if (preview->image != NULL) { + g_object_unref(G_OBJECT(preview->image)); + preview->image = NULL; + } + if (preview->pixbuf != NULL) { + g_object_unref(G_OBJECT(preview->pixbuf)); + preview->pixbuf = NULL; + } +} + +void +sugar_preview_take_screenshot(SugarPreview *preview, GdkDrawable *drawable) +{ + GdkScreen *screen; + GdkVisual *visual; + GdkColormap *colormap; + gint width, height; + + sugar_preview_clear(preview); + + gdk_drawable_get_size(drawable, &width, &height); + + screen = gdk_drawable_get_screen(drawable); + visual = gdk_drawable_get_visual(drawable); + colormap = gdk_drawable_get_colormap(drawable); + + preview->image = gdk_image_new(GDK_IMAGE_SHARED, visual, width, height); + gdk_image_set_colormap(preview->image, colormap); + + XShmGetImage(GDK_SCREEN_XDISPLAY(screen), + GDK_DRAWABLE_XID(drawable), + gdk_x11_image_get_ximage(preview->image), + 0, 0, AllPlanes, ZPixmap); +} + +static void +sugar_preview_dispose(GObject *object) +{ + SugarPreview *preview = SUGAR_PREVIEW(object); + sugar_preview_clear(preview); +} + +static void +sugar_preview_class_init(SugarPreviewClass *preview_class) +{ + GObjectClass *g_object_class = G_OBJECT_CLASS (preview_class); + + g_object_class->dispose = sugar_preview_dispose; +} + +static void +sugar_preview_init(SugarPreview *preview) +{ + preview->image = NULL; + preview->pixbuf = NULL; +} diff --git a/sugar/sugar-preview.h b/sugar/sugar-preview.h new file mode 100644 index 0000000..6029cc1 --- /dev/null +++ b/sugar/sugar-preview.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2007, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __SUGAR_PREVIEW_H__ +#define __SUGAR_PREVIEW_H__ + +#include + +G_BEGIN_DECLS + +typedef struct _SugarPreview SugarPreview; +typedef struct _SugarPreviewClass SugarPreviewClass; + +#define SUGAR_TYPE_PREVIEW (sugar_preview_get_type()) +#define SUGAR_PREVIEW(object) (G_TYPE_CHECK_INSTANCE_CAST((object), SUGAR_TYPE_PREVIEW, SugarPreview)) +#define SUGAR_PREVIEW_CLASS(klass) (G_TYPE_CHACK_CLASS_CAST((klass), SUGAR_TYPE_PREVIEW, SugarPreviewClass)) +#define SUGAR_IS_PREVIEW(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), SUGAR_TYPE_PREVIEW)) +#define SUGAR_IS_PREVIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SUGAR_TYPE_PREVIEW)) +#define SUGAR_PREVIEW_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_PREVIEW, SugarPreviewClass)) + +struct _SugarPreview { + GObject base_instance; + + GdkImage *image; + GdkPixbuf *pixbuf; + + int width; + int height; +}; + +struct _SugarPreviewClass { + GObjectClass base_class; +}; + +GType sugar_preview_get_type (void); +void sugar_preview_take_screenshot (SugarPreview *preview, + GdkDrawable *drawable); +void sugar_preview_set_size (SugarPreview *preview, + int width, + int height); +GdkPixbuf *sugar_preview_get_pixbuf (SugarPreview *preview); +void sugar_preview_clear (SugarPreview *preview); + +G_END_DECLS + +#endif /* __SUGAR_PREVIEW_H__ */ diff --git a/sugar/util.py b/sugar/util.py new file mode 100644 index 0000000..8a3fb4a --- /dev/null +++ b/sugar/util.py @@ -0,0 +1,175 @@ +"""Various utility functions""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import time +import sha +import random +import binascii +import string +import os +import logging + +from ConfigParser import ConfigParser +from ConfigParser import NoOptionError + +def printable_hash(in_hash): + """Convert binary hash data into printable characters.""" + printable = "" + for char in in_hash: + printable = printable + binascii.b2a_hex(char) + return printable + +def _sha_data(data): + """sha1 hash some bytes.""" + sha_hash = sha.new() + sha_hash.update(data) + return sha_hash.digest() + +def unique_id(data = ''): + """Generate a likely-unique ID for whatever purpose + + data -- suffix appended to working data before hashing + + Returns a 40-character string with hexidecimal digits + representing an SHA hash of the time, a random digit + within a constrained range and the data passed. + + Note: these are *not* crypotographically secure or + globally unique identifiers. While they are likely + to be unique-enough, no attempt is made to make + perfectly unique values. + """ + data_string = "%s%s%s" % (time.time(), random.randint(10000, 100000), data) + return printable_hash(_sha_data(data_string)) + + +ACTIVITY_ID_LEN = 40 + +def is_hex(s): + return s.strip(string.hexdigits) == '' + +def validate_activity_id(actid): + """Validate an activity ID.""" + if not isinstance(actid, (str,unicode)): + return False + if len(actid) != ACTIVITY_ID_LEN: + return False + if not is_hex(actid): + return False + return True + +def set_proc_title(title): + """Sets the process title so ps and top show more + descriptive names. This does not modify argv[0] + and only the first 15 characters will be shown. + + title -- the title you wish to change the process + title to + + Returns True on success. We don't raise exceptions + because if something goes wrong here it is not a big + deal as this is intended as a nice thing to have for + debugging + """ + try: + import ctypes + libc = ctypes.CDLL('libc.so.6') + libc.prctl(15, str(title), 0, 0, 0) + + return True + except: + return False + +class Node(object): + __slots__ = ['prev', 'next', 'me'] + def __init__(self, prev, me): + self.prev = prev + self.me = me + self.next = None + +class LRU: + """ + Implementation of a length-limited O(1) LRU queue. + Built for and used by PyPE: + http://pype.sourceforge.net + Copyright 2003 Josiah Carlson. + """ + def __init__(self, count, pairs=[]): + self.count = max(count, 1) + self.d = {} + self.first = None + self.last = None + for key, value in pairs: + self[key] = value + def __contains__(self, obj): + return obj in self.d + def __getitem__(self, obj): + a = self.d[obj].me + self[a[0]] = a[1] + return a[1] + def __setitem__(self, obj, val): + if obj in self.d: + del self[obj] + nobj = Node(self.last, (obj, val)) + if self.first is None: + self.first = nobj + if self.last: + self.last.next = nobj + self.last = nobj + self.d[obj] = nobj + if len(self.d) > self.count: + if self.first == self.last: + self.first = None + self.last = None + return + a = self.first + a.next.prev = None + self.first = a.next + a.next = None + del self.d[a.me[0]] + del a + def __delitem__(self, obj): + nobj = self.d[obj] + if nobj.prev: + nobj.prev.next = nobj.next + else: + self.first = nobj.next + if nobj.next: + nobj.next.prev = nobj.prev + else: + self.last = nobj.prev + del self.d[obj] + def __iter__(self): + cur = self.first + while cur != None: + cur2 = cur.next + yield cur.me[1] + cur = cur2 + def iteritems(self): + cur = self.first + while cur != None: + cur2 = cur.next + yield cur.me + cur = cur2 + def iterkeys(self): + return iter(self.d) + def itervalues(self): + for i,j in self.iteritems(): + yield j + def keys(self): + return self.d.keys() diff --git a/sugar/wm.py b/sugar/wm.py new file mode 100644 index 0000000..47356a5 --- /dev/null +++ b/sugar/wm.py @@ -0,0 +1,42 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +def get_activity_id(wnck_window): + window = gtk.gdk.window_foreign_new(wnck_window.get_xid()) + prop_info = window.property_get('_SUGAR_ACTIVITY_ID', 'STRING') + if prop_info is None: + return None + else: + return prop_info[2] + +def get_bundle_id(wnck_window): + window = gtk.gdk.window_foreign_new(wnck_window.get_xid()) + prop_info = window.property_get('_SUGAR_BUNDLE_ID', 'STRING') + if prop_info is None: + return None + else: + return prop_info[2] + +def set_activity_id(window, activity_id): + window.property_change('_SUGAR_ACTIVITY_ID', 'STRING', 8, + gtk.gdk.PROP_MODE_REPLACE, activity_id) + +def set_bundle_id(window, bundle_id): + window.property_change('_SUGAR_BUNDLE_ID', 'STRING', 8, + gtk.gdk.PROP_MODE_REPLACE, bundle_id) -- cgit v0.9.1