diff options
author | John (J5) Palmieri <quinticent@localhost.localdomain> | 2007-08-20 18:41:03 (GMT) |
---|---|---|
committer | John (J5) Palmieri <quinticent@localhost.localdomain> | 2007-08-20 18:41:03 (GMT) |
commit | 655d7f41741d90617aa341db7542153261c9f6e7 (patch) | |
tree | 88c0df40a8bb109500672136fb7916b34348275b /sugar | |
parent | 735b0cf2c8a5c85b89a0b82d4fc1f40ec6ab5d12 (diff) | |
parent | 1a65f81d10186d2f82aafb17533e6b5946761ec8 (diff) |
Merge branch 'master' of git+ssh://j5@dev.laptop.org/git/sugar
Diffstat (limited to 'sugar')
27 files changed, 493 insertions, 148 deletions
diff --git a/sugar/_sugaruiext.defs b/sugar/_sugaruiext.defs index 3c011e1..6a9129d 100644 --- a/sugar/_sugaruiext.defs +++ b/sugar/_sugaruiext.defs @@ -22,8 +22,25 @@ (gtype-id "SUGAR_TYPE_MENU") ) +(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 @@ -94,3 +111,61 @@ '("const-char*" "property") ) ) + +;; 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") + ) +) + +(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") +) + diff --git a/sugar/_sugaruiext.override b/sugar/_sugaruiext.override index 6daafc3..beeaad0 100644 --- a/sugar/_sugaruiext.override +++ b/sugar/_sugaruiext.override @@ -8,6 +8,7 @@ headers #include "sugar-key-grabber.h" #include "sugar-menu.h" #include "sugar-x11-util.h" +#include "sexy-icon-entry.h" #include <pygtk/pygtk.h> #include <glib.h> @@ -20,6 +21,7 @@ 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.Image as PyGtkImage_Type %% ignore-glob *_get_type diff --git a/sugar/_sugaruiextmodule.c b/sugar/_sugaruiextmodule.c index 8c74010..719b153 100644 --- a/sugar/_sugaruiextmodule.c +++ b/sugar/_sugaruiextmodule.c @@ -27,6 +27,7 @@ extern PyMethodDef py_sugaruiext_functions[]; void py_sugaruiext_register_classes (PyObject *d); +void py_sugaruiext_add_constants (PyObject *module, const gchar *strip_prefix); DL_EXPORT(void) init_sugaruiext(void) @@ -39,6 +40,7 @@ init_sugaruiext(void) d = PyModule_GetDict (m); py_sugaruiext_register_classes (d); + py_sugaruiext_add_constants(m, "SEXY_"); if (PyErr_Occurred ()) { Py_FatalError ("can't initialise module _sugaruiext"); diff --git a/sugar/activity/activity.py b/sugar/activity/activity.py index 262d89b..553b1a1 100644 --- a/sugar/activity/activity.py +++ b/sugar/activity/activity.py @@ -52,6 +52,8 @@ class ActivityToolbar(gtk.Toolbar): self._activity = activity 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() @@ -74,11 +76,11 @@ class ActivityToolbar(gtk.Toolbar): 'theme:zoom-home-mini') self.share.combo.append_item(None, _('My Neighborhood'), 'theme:zoom-neighborhood-mini') - self._update_share() - 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) @@ -94,6 +96,9 @@ class ActivityToolbar(gtk.Toolbar): self._update_title_sid = None def _update_share(self): + 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(self.SHARE_NEIGHBORHOOD) @@ -139,6 +144,9 @@ class ActivityToolbar(gtk.Toolbar): def _activity_shared_cb(self, activity): self._update_share() + def _max_participants_changed_cb(self, activity, pspec): + self._update_share() + class EditToolbar(gtk.Toolbar): def __init__(self): gtk.Toolbar.__init__(self) @@ -185,7 +193,10 @@ class Activity(Window, gtk.Container): } __gproperties__ = { - 'active': (bool, None, None, False, gobject.PARAM_READWRITE) + '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): @@ -235,6 +246,7 @@ class Activity(Window, gtk.Container): self._preview = None self._updating_jobject = False self._closing = False + self._max_participants = 0 shared_activity = handle.get_shared_activity() if shared_activity: @@ -280,10 +292,14 @@ class Activity(Window, gtk.Container): self._active = value if not self._active and self._jobject: self.save() + elif pspec.name == 'max-participants': + self._max_participants = value def do_get_property(self, pspec): if pspec.name == 'active': return self._active + elif pspec.name == 'max-participants': + return self._max_participants def get_id(self): return self._activity_id diff --git a/sugar/activity/activityservice.py b/sugar/activity/activityservice.py index c9ee482..f2b3394 100644 --- a/sugar/activity/activityservice.py +++ b/sugar/activity/activityservice.py @@ -56,6 +56,6 @@ class ActivityService(dbus.service.Object): self._activity = activity @dbus.service.method(_ACTIVITY_INTERFACE) - def set_active(self, active): + def SetActive(self, active): logging.debug('ActivityService.set_active: %s.' % active) self._activity.props.active = active diff --git a/sugar/activity/bundlebuilder.py b/sugar/activity/bundlebuilder.py index 3bbe454..b0e46ab 100644 --- a/sugar/activity/bundlebuilder.py +++ b/sugar/activity/bundlebuilder.py @@ -21,6 +21,7 @@ import zipfile import shutil import subprocess import re +import gettext from sugar import env from sugar.activity.bundle import Bundle @@ -117,7 +118,7 @@ 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 genmo - compile gettext po files in mo \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\ @@ -157,17 +158,26 @@ def _get_po_list(manifest): return file_list -def _get_mo_list(manifest): - mo_list = [] +def _get_l10n_list(manifest): + l10n_list = [] for lang in _get_po_list(manifest).keys(): filename = _get_service_name() + '.mo' - mo_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename)) + l10n_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename)) + l10n_list.append(os.path.join('locale', lang, 'activity.linfo')) - return mo_list + 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_genmo(bundle_name, manifest) + cmd_genl10n(bundle_name, manifest) file_list = _get_file_list(manifest) zipname = _get_package_name(bundle_name) @@ -177,7 +187,7 @@ def cmd_dist(bundle_name, manifest): for filename in file_list: bundle_zip.write(filename, os.path.join(base_dir, filename)) - for filename in _get_mo_list(manifest): + for filename in _get_l10n_list(manifest): bundle_zip.write(filename, os.path.join(base_dir, filename)) bundle_zip.close() @@ -205,8 +215,21 @@ def cmd_genpot(bundle_name, manifest): 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) - args = [ 'xgettext', '--language=Python', + 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=_', '--output=%s' % pot_file ] args += python_files @@ -220,14 +243,16 @@ def cmd_genpot(bundle_name, manifest): if retcode: print 'ERROR - msgmerge failed with return code %i.' % retcode -def cmd_genmo(bundle_name, manifest): +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] - mo_path = os.path.join(source_path, 'locale', lang, 'LC_MESSAGES') + 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) @@ -237,6 +262,13 @@ def cmd_genmo(bundle_name, manifest): 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' @@ -358,8 +390,8 @@ def start(bundle_name, manifest='MANIFEST'): cmd_uninstall(sys.argv[2]) elif sys.argv[1] == 'genpot': cmd_genpot(bundle_name, manifest) - elif sys.argv[1] == 'genmo': - cmd_genmo(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': diff --git a/sugar/clipboard/clipboardservice.py b/sugar/clipboard/clipboardservice.py index dbdf41d..0e357fe 100644 --- a/sugar/clipboard/clipboardservice.py +++ b/sugar/clipboard/clipboardservice.py @@ -23,7 +23,7 @@ NAME_KEY = 'NAME' PERCENT_KEY = 'PERCENT' ICON_KEY = 'ICON' PREVIEW_KEY = 'PREVIEW' -ACTIVITY_KEY = 'ACTIVITY' +ACTIVITIES_KEY = 'ACTIVITIES' FORMATS_KEY = 'FORMATS' TYPE_KEY = 'TYPE' @@ -51,7 +51,7 @@ class ClipboardService(gobject.GObject): '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, str])), + ([str, str, int, str, str, object])), } def __init__(self): @@ -118,13 +118,13 @@ class ClipboardService(gobject.GObject): percent icon preview - activity + 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[ACTIVITY_KEY]) + values[ACTIVITIES_KEY]) def add_object(self, name): """Add a new object to the path @@ -193,7 +193,7 @@ class ClipboardService(gobject.GObject): PERCENT_KEY: number, ICON_KEY: str, PREVIEW_KEY: XXX what is it?, - ACTIVITY_KEY: source activity id, + 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),) diff --git a/sugar/datastore/datastore.py b/sugar/datastore/datastore.py index 5ba994d..0dbe35b 100644 --- a/sugar/datastore/datastore.py +++ b/sugar/datastore/datastore.py @@ -120,6 +120,9 @@ class DSObject(object): def resume(self, service_name=None): if self.is_bundle(): + if service_name is not None: + raise ValueError('Object is a bundle, cannot be resumed as an activity.') + bundle = Bundle(self.file_path) if not bundle.is_installed(): bundle.install() diff --git a/sugar/datastore/dbus_helpers.py b/sugar/datastore/dbus_helpers.py index f0cfa3b..442a35d 100644 --- a/sugar/datastore/dbus_helpers.py +++ b/sugar/datastore/dbus_helpers.py @@ -28,55 +28,63 @@ DS_DBUS_SERVICE = "org.laptop.sugar.DataStore" DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore" DS_DBUS_PATH = "/org/laptop/sugar/DataStore" -_bus = dbus.SessionBus() -_data_store = dbus.Interface(_bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), - DS_DBUS_INTERFACE) +_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): - object_id = _data_store.create(dbus.Dictionary(properties), filename) + object_id = _get_data_store().create(dbus.Dictionary(properties), filename) logging.debug('dbus_helpers.create: ' + object_id) return object_id def update(uid, properties, filename, reply_handler=None, error_handler=None, timeout=-1): logging.debug('dbus_helpers.update: %s, %s, %s' % (uid, filename, properties)) if reply_handler and error_handler: - _data_store.update(uid, dbus.Dictionary(properties), filename, + _get_data_store().update(uid, dbus.Dictionary(properties), filename, reply_handler=reply_handler, error_handler=error_handler, timeout=timeout) else: - _data_store.update(uid, dbus.Dictionary(properties), filename) + _get_data_store().update(uid, dbus.Dictionary(properties), filename) def delete(uid): logging.debug('dbus_helpers.delete: %r' % uid) - _data_store.delete(uid) + _get_data_store().delete(uid) def get_properties(uid): logging.debug('dbus_helpers.get_properties: %s' % uid) - return _data_store.get_properties(uid) + return _get_data_store().get_properties(uid) def get_filename(uid): - filename = _data_store.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, reply_handler, error_handler): logging.debug('dbus_helpers.find: %r' % query) if reply_handler and error_handler: - return _data_store.find(query, reply_handler=reply_handler, + return _get_data_store().find(query, reply_handler=reply_handler, error_handler=error_handler) else: - return _data_store.find(query) + return _get_data_store().find(query) def mount(uri, options): - return _data_store.mount(uri, options) + return _get_data_store().mount(uri, options) def unmount(mount_point_id): - _data_store.unmount(mount_point_id) + _get_data_store().unmount(mount_point_id) def mounts(): - return _data_store.mounts() + return _get_data_store().mounts() def get_unique_values(key): - return _data_store.get_uniquevaluesfor(key, dbus.Dictionary({}, signature='ss')) + return _get_data_store().get_uniquevaluesfor(key, dbus.Dictionary({}, signature='ss')) diff --git a/sugar/date.py b/sugar/date.py index 3b68818..3f4dcc2 100644 --- a/sugar/date.py +++ b/sugar/date.py @@ -21,12 +21,13 @@ import datetime class Date(object): """Date-object storing a simple time.time() float - - XXX not sure about the rationale for this class, - possibly it makes transfer over dbus easier? + + Useful to display dates in the UI in an + abbreviated and easy to read format. """ def __init__(self, timestamp): """Initialise via a timestamp (floating point value)""" + self._today = datetime.date.today() self._timestamp = timestamp def __str__(self): @@ -39,14 +40,13 @@ class Date(object): the year in the date. """ date = datetime.date.fromtimestamp(self._timestamp) - today = datetime.date.today() # FIXME localization - if date == today: + if date == self._today: result = 'Today' - elif date == today - datetime.timedelta(1): + elif date == self._today - datetime.timedelta(1): result = 'Yesterday' - elif date.year == today.year: + elif date.year == self._today.year: result = date.strftime('%B %d') else: result = date.strftime('%B %d, %Y') diff --git a/sugar/graphics/Makefile.am b/sugar/graphics/Makefile.am index af66cfb..2d0dc17 100644 --- a/sugar/graphics/Makefile.am +++ b/sugar/graphics/Makefile.am @@ -9,7 +9,7 @@ sugar_PYTHON = \ combobox.py \ icon.py \ iconbutton.py \ - menuitem.py \ + iconentry.py \ notebook.py \ objectchooser.py \ radiotoolbutton.py \ diff --git a/sugar/graphics/canvasbutton.py b/sugar/graphics/canvasbutton.py index 31bc833..fc869f9 100644 --- a/sugar/graphics/canvasbutton.py +++ b/sugar/graphics/canvasbutton.py @@ -25,7 +25,7 @@ class CanvasButton(hippo.CanvasButton): hippo.CanvasButton.__init__(self, text=label) if icon_name: - icon = Icon(icon_name, gtk.ICON_SIZE_BUTTON) + icon = Icon(icon_name,icon_size=gtk.ICON_SIZE_BUTTON) self.props.widget.set_image(icon) icon.show() diff --git a/sugar/graphics/canvasicon.py b/sugar/graphics/canvasicon.py index 39f1358..a77eecd 100644 --- a/sugar/graphics/canvasicon.py +++ b/sugar/graphics/canvasicon.py @@ -142,6 +142,8 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): gobject.PARAM_READWRITE), 'size' : (int, None, None, 0, 1024, 0, gobject.PARAM_READWRITE), + 'scale' : (int, None, None, 0, 1024, 0, + gobject.PARAM_READWRITE), 'cache' : (bool, None, None, False, gobject.PARAM_READWRITE), 'active' : (bool, None, None, True, @@ -156,6 +158,7 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): self._buffers = {} self._cur_buffer = None self._size = 0 + self._scale = 0 self._fill_color = None self._stroke_color = None self._icon_name = None @@ -210,6 +213,11 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): self._clear_buffers() self._size = value self.emit_request_changed() + elif pspec.name == 'scale': + if self._scale != value and not self._cache: + self._clear_buffers() + self._scale = value + self.emit_request_changed() elif pspec.name == 'cache': self._cache = value elif pspec.name == 'active': @@ -277,6 +285,8 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): return self._active elif pspec.name == 'badge-name': return self._badge_name + elif pspec.name == 'scale': + return self._scale def _get_icon_size(self, handle): if handle: @@ -286,9 +296,11 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): return [0, 0] def _get_size(self, handle): - if self._size == 0: - width, height = self._get_icon_size(handle) - else: + width, height = self._get_icon_size(handle) + if self._scale != 0: + width = int(width * self._scale) + height = int(height * self._scale) + elif self._size != 0: width = height = self._size return [width, height] diff --git a/sugar/graphics/icon.py b/sugar/graphics/icon.py index 731412f..e324d45 100644 --- a/sugar/graphics/icon.py +++ b/sugar/graphics/icon.py @@ -34,17 +34,18 @@ class Icon(gtk.Image): gobject.PARAM_READWRITE) } - def __init__(self, name, size=gtk.ICON_SIZE_LARGE_TOOLBAR, **kwargs): + def __init__(self, name, **kwargs): + self._constructed = False self._fill_color = None self._stroke_color = None self._icon_name = name - self._size = size self._theme = gtk.icon_theme_get_default() + self._data = None + gobject.GObject.__init__(self, **kwargs) - # If we have a non-styled-icon - if not self._fill_color and not self._stroke_color: - self._update_normal_icon() + self._constructed = True + self._update_icon() def _get_pixbuf(self, data, width, height): loader = gtk.gdk.PixbufLoader('svg') @@ -77,9 +78,12 @@ class Icon(gtk.Image): source.set_state(gtk.STATE_INSENSITIVE) icon_set.add_source(source) - self.set_from_icon_set(icon_set, self._size) + self.props.icon_set = icon_set def _update_icon(self): + if not self._constructed: + return + if not self._fill_color and not self._stroke_color: self._update_normal_icon() return @@ -100,12 +104,12 @@ class Icon(gtk.Image): self._data = data # Redraw pixbuf - [w, h] = gtk.icon_size_lookup(self._size) + [w, h] = gtk.icon_size_lookup(self.props.icon_size) pixbuf = self._get_pixbuf(self._data, w, h) self.set_from_pixbuf(pixbuf) def _get_real_name(self, name): - info = self._theme.lookup_icon(name, self._size, 0) + info = self._theme.lookup_icon(name, self.props.icon_size, 0) if not info: raise ValueError("Icon '" + name + "' not found.") fname = info.get_filename() @@ -122,9 +126,16 @@ class Icon(gtk.Image): elif pspec.name == 'stroke-color': self._stroke_color = value self._update_icon() + else: + gtk.Image.do_set_property(self, pspec, value) + + if pspec.name == 'icon-size': + self._update_icon() def do_get_property(self, pspec): if pspec.name == 'fill-color': return self._fill_color elif pspec.name == 'stroke-color': return self._stroke_color + else: + return gtk.Image.do_get_property(self, pspec) diff --git a/sugar/graphics/iconentry.py b/sugar/graphics/iconentry.py new file mode 100644 index 0000000..2f7584f --- /dev/null +++ b/sugar/graphics/iconentry.py @@ -0,0 +1,45 @@ +# 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 _sugaruiext + +ICON_ENTRY_PRIMARY = _sugaruiext.ICON_ENTRY_PRIMARY +ICON_ENTRY_SECONDARY = _sugaruiext.ICON_ENTRY_SECONDARY + +class IconEntry(_sugaruiext.IconEntry): + 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) + + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.get_filename()) + + 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()) + _sugaruiext.IconEntry.set_icon(self, position, image) + diff --git a/sugar/graphics/menuitem.py b/sugar/graphics/menuitem.py index 492f5f1..db4a293 100644 --- a/sugar/graphics/menuitem.py +++ b/sugar/graphics/menuitem.py @@ -22,7 +22,7 @@ class MenuItem(gtk.ImageMenuItem): def __init__(self, text_label, icon_name=None): gtk.ImageMenuItem.__init__(self, text_label) if icon_name: - icon = Icon(icon_name, gtk.ICON_SIZE_MENU) + icon = Icon(icon_name, icon_size=gtk.ICON_SIZE_MENU) self.set_image(icon) icon.show() diff --git a/sugar/graphics/objectchooser.py b/sugar/graphics/objectchooser.py index 5b09e13..46739f6 100644 --- a/sugar/graphics/objectchooser.py +++ b/sugar/graphics/objectchooser.py @@ -150,7 +150,7 @@ class CollapsedEntry(CanvasRoundBox): self._icon_name = type.icon if not self._icon_name: - self._icon_name = 'theme:stock-missing' + self._icon_name = 'theme:image-missing' return self._icon_name diff --git a/sugar/graphics/palette.py b/sugar/graphics/palette.py index 368a0f6..6b1209a 100644 --- a/sugar/graphics/palette.py +++ b/sugar/graphics/palette.py @@ -87,9 +87,7 @@ class Palette(gtk.Window): 'invoker' : (object, None, None, gobject.PARAM_READWRITE), 'position' : (gobject.TYPE_INT, None, None, 0, 6, - 0, gobject.PARAM_READWRITE), - 'draw-gap' : (bool, None, None, False, - gobject.PARAM_READWRITE) + 0, gobject.PARAM_READWRITE) } __gsignals__ = { @@ -114,7 +112,6 @@ class Palette(gtk.Window): self._group_id = None self._up = False self._position = self.DEFAULT - self._draw_gap = False self._palette_popup_sid = None self._popup_anim = animator.Animator(0.3, 10) @@ -162,6 +159,7 @@ class Palette(gtk.Window): self._leave_notify_event_cb) self.set_primary_text(label, accel_path) + self.set_group_id('default') def is_up(self): return self._up @@ -178,8 +176,9 @@ class Palette(gtk.Window): return gtk.gdk.Rectangle(x, y, width, height) def set_primary_text(self, label, accel_path=None): - self._label.set_text(label) - self._label.show() + if label is not None: + self._label.set_text(label) + self._label.show() def set_content(self, widget): if len(self._content.get_children()) > 0: @@ -196,6 +195,7 @@ class Palette(gtk.Window): 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) @@ -206,9 +206,6 @@ class Palette(gtk.Window): self._invoker.connect('mouse-leave', self._invoker_mouse_leave_cb) elif pspec.name == 'position': self._position = value - elif pspec.name == 'draw-gap': - self._draw_gap = value - self.queue_draw() else: raise AssertionError @@ -217,8 +214,6 @@ class Palette(gtk.Window): return self._invoker elif pspec.name == 'position': return self._position - elif pspec.name == 'draw-gap': - return self._draw_gap else: raise AssertionError @@ -228,7 +223,7 @@ class Palette(gtk.Window): def do_expose_event(self, event): # We want to draw a border with a beautiful gap - if self._draw_gap: + if self._invoker.has_rectangle_gap(): invoker = self._invoker.get_rect() palette = self.get_rect() @@ -398,6 +393,9 @@ class Palette(gtk.Window): self.menu.set_active(True) self.show() + if self._invoker: + self._invoker.notify_popup() + self._up = True _palette_observer.emit('popup', self) self.emit('popup') @@ -406,22 +404,31 @@ class Palette(gtk.Window): 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): + def popup(self, immediate=False): self._popdown_anim.stop() - self._popup_anim.start() + + if not immediate: + self._popup_anim.start() + else: + self._show() + self._secondary_anim.start() - def popdown(self, inmediate=False): + def popdown(self, immediate=False): self._secondary_anim.stop() self._popup_anim.stop() - if not inmediate: + if not immediate: self._popdown_anim.start() else: self._hide() @@ -440,7 +447,15 @@ class Palette(gtk.Window): self._state = state def _invoker_mouse_enter_cb(self, invoker): - self.popup() + immediate = False + if self._group_id: + group = palettegroup.get_group(self._group_id) + if group and group.is_up(): + immediate = True + group.popdown() + + print immediate + self.popup(immediate=immediate) def _invoker_mouse_leave_cb(self, invoker): self.popdown() @@ -535,6 +550,12 @@ class Invoker(gobject.GObject): def __init__(self): gobject.GObject.__init__(self) + def has_rectangle_gap(self): + return False + + def draw_rectangle(self, event, palette): + pass + def get_default_position(self): return Palette.AROUND @@ -543,6 +564,12 @@ class Invoker(gobject.GObject): height = gtk.gdk.screen_height() return gtk.gdk.Rectangle(0, 0, width, height) + def notify_popup(self): + pass + + def notify_popdown(self): + pass + class WidgetInvoker(Invoker): def __init__(self, widget): Invoker.__init__(self) @@ -562,31 +589,24 @@ class WidgetInvoker(Invoker): return gtk.gdk.Rectangle(x, y, width, height) - def draw_invoker_rect(self, event, palette): + def has_rectangle_gap(self): + return True + + def draw_rectangle(self, event, palette): style = self._widget.style - if palette.is_up(): - 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, + 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) + self._widget.allocation.height, + gap[0], gap[1], gap[2]) else: style.paint_box(event.window, gtk.STATE_PRELIGHT, - gtk.SHADOW_NONE, event.area, self._widget, + gtk.SHADOW_IN, event.area, self._widget, "palette-invoker", self._widget.allocation.x, self._widget.allocation.y, @@ -602,6 +622,12 @@ class WidgetInvoker(Invoker): def get_toplevel(self): return self._widget.get_toplevel() + def notify_popup(self): + self._widget.queue_draw() + + def notify_popdown(self): + self._widget.queue_draw() + class CanvasInvoker(Invoker): def __init__(self, item): Invoker.__init__(self) diff --git a/sugar/graphics/palettegroup.py b/sugar/graphics/palettegroup.py index d5e1125..d6ae122 100644 --- a/sugar/graphics/palettegroup.py +++ b/sugar/graphics/palettegroup.py @@ -39,6 +39,7 @@ class Group(gobject.GObject): gobject.GObject.__init__(self) self._up = False self._palettes = [] + self._sig_ids = {} def is_up(self): return self._up @@ -46,18 +47,26 @@ class Group(gobject.GObject): def add(self, palette): self._palettes.append(palette) + self._sig_ids[palette] = [] + sid = palette.connect('popup', self._palette_popup_cb) - palette.popup_sid = sid + self._sig_ids[palette].append(sid) sid = palette.connect('popdown', self._palette_popdown_cb) - palette.podown_sid = sid + self._sig_ids[palette].append(sid) def remove(self, palette): - self.disconnect(palette.popup_sid) - self.disconnect(palette.popdown_sid) + sig_ids = self._sig_ids[palette] + for sid in sig_ids: + palette.disconnect(sid) self._palettes.remove(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') diff --git a/sugar/graphics/radiotoolbutton.py b/sugar/graphics/radiotoolbutton.py index fb584ee..4ffc7d1 100644 --- a/sugar/graphics/radiotoolbutton.py +++ b/sugar/graphics/radiotoolbutton.py @@ -40,25 +40,15 @@ class RadioToolButton(gtk.RadioToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) - self._palette.props.draw_gap = True - - self._palette.connect("popup", self._palette_changed) - self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): - self._palette = Palette(text) - self._palette.props.invoker = WidgetInvoker(self.child) + self._set_palette(Palette(text)) def do_expose_event(self, event): - if self._palette and self._palette.props.draw_gap: - if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: - invoker = self._palette.props.invoker - invoker.draw_invoker_rect(event, self._palette) + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) gtk.RadioToolButton.do_expose_event(self, event) - def _palette_changed(self, palette): - # Force a redraw to update the invoker rectangle - self.queue_draw() - palette = property(get_palette, set_palette) diff --git a/sugar/graphics/style.py b/sugar/graphics/style.py index c0094b6..9d561eb 100644 --- a/sugar/graphics/style.py +++ b/sugar/graphics/style.py @@ -128,6 +128,8 @@ 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') diff --git a/sugar/graphics/toggletoolbutton.py b/sugar/graphics/toggletoolbutton.py index 41050e2..74e78a6 100644 --- a/sugar/graphics/toggletoolbutton.py +++ b/sugar/graphics/toggletoolbutton.py @@ -39,25 +39,15 @@ class ToggleToolButton(gtk.ToggleToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) - self._palette.props.draw_gap = True - - self._palette.connect("popup", self._palette_changed) - self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): - self._palette = Palette(text) - self._palette.props.invoker = WidgetInvoker(self.child) + self._set_palette(Palette(text)) def do_expose_event(self, event): - if self._palette and self._palette.props.draw_gap: - if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: - invoker = self._palette.props.invoker - invoker.draw_invoker_rect(event, self._palette) + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) gtk.ToggleToolButton.do_expose_event(self, event) - def _palette_changed(self, palette): - # Force a redraw to update the invoker rectangle - self.queue_draw() - palette = property(get_palette, set_palette) diff --git a/sugar/graphics/toolbutton.py b/sugar/graphics/toolbutton.py index 52a5d62..b42f63e 100644 --- a/sugar/graphics/toolbutton.py +++ b/sugar/graphics/toolbutton.py @@ -28,7 +28,8 @@ class ToolButton(gtk.ToolButton): def __init__(self, icon_name=None): gtk.ToolButton.__init__(self) self._palette = None - self.set_icon(icon_name) + if icon_name: + self.set_icon(icon_name) self.connect('clicked', self._button_clicked_cb) def set_icon(self, icon_name): @@ -42,19 +43,14 @@ class ToolButton(gtk.ToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) - self._palette.props.draw_gap = True - - self._palette.connect("popup", self._palette_changed) - self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): self.set_palette(Palette(text)) def do_expose_event(self, event): - if self._palette and self._palette.props.draw_gap: - if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: - invoker = self._palette.props.invoker - invoker.draw_invoker_rect(event, self._palette) + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) gtk.ToolButton.do_expose_event(self, event) @@ -62,8 +58,4 @@ class ToolButton(gtk.ToolButton): if self._palette: self._palette.popdown(True) - def _palette_changed(self, palette): - # Force a redraw to update the invoker rectangle - self.queue_draw() - palette = property(get_palette, set_palette) diff --git a/sugar/logger.py b/sugar/logger.py index e857044..72c4a99 100644 --- a/sugar/logger.py +++ b/sugar/logger.py @@ -105,11 +105,7 @@ def _get_logs_dir(): def start(module_id): # Only log if logging is set up for the activity module_key = module_id.upper() + "_DEBUG" - emulator = False - if os.environ.has_key("SUGAR_EMULATOR"): - if os.environ["SUGAR_EMULATOR"] == "yes": - emulator = True - if not os.environ.has_key(module_key) and not emulator: + if not os.environ.has_key(module_key) and not env.is_emulator(): return log_writer = LogWriter(module_id) diff --git a/sugar/objects/mime.py b/sugar/objects/mime.py index 80eac9b..b933143 100644 --- a/sugar/objects/mime.py +++ b/sugar/objects/mime.py @@ -1,4 +1,5 @@ # 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 @@ -20,7 +21,13 @@ import logging from sugar import _sugarext def get_for_file(file_name): - return _sugarext.get_mime_type_for_file(file_name) + mime_type = _sugarext.get_mime_type_for_file(file_name) + if mime_type == 'application/octet-stream': + if _file_looks_like_text(file_name): + return 'text/plain' + else: + return 'application/octet-stream' + return mime_type def get_from_file_name(file_name): return _sugarext.get_mime_type_from_file_name(file_name) @@ -51,13 +58,9 @@ def choose_most_significant(mime_types): if 'text/uri-list' in mime_types: return 'text/uri-list' - for mime_category in ['image/', 'text/', 'application/']: + for mime_category in ['image/', 'application/']: for mime_type in mime_types: - # skip text/plain and text/html, these have lower priority. - if mime_type in ['text/plain', 'text/html']: - continue - if mime_type.startswith(mime_category): # skip mozilla private types (second component starts with '_' # or ends with '-priv') @@ -70,6 +73,10 @@ def choose_most_significant(mime_types): logging.debug('Choosed %r!' % mime_type) return mime_type + if 'text/x-moz-url' in mime_types: + logging.debug('Choosed text/x-moz-url!') + return 'text/x-moz-url' + if 'text/html' in mime_types: logging.debug('Choosed text/html!') return 'text/html' @@ -80,3 +87,22 @@ def choose_most_significant(mime_types): logging.debug('Returning first: %r.' % mime_types[0]) return mime_types[0] + +def _file_looks_like_text(file_name): + f = open(file_name, 'r') + try: + sample = f.read(256) + finally: + f.close() + + if '\000' in sample: + return False + + for encoding in ('ascii', 'latin_1', 'utf_8', 'utf_16'): + try: + string = unicode(sample, encoding) + return True + except Exception, e: + pass + + return False diff --git a/sugar/presence/Makefile.am b/sugar/presence/Makefile.am index 6314c1a..cb52a41 100644 --- a/sugar/presence/Makefile.am +++ b/sugar/presence/Makefile.am @@ -3,5 +3,6 @@ sugar_PYTHON = \ __init__.py \ activity.py \ buddy.py \ + tubeconn.py \ presenceservice.py diff --git a/sugar/presence/tubeconn.py b/sugar/presence/tubeconn.py new file mode 100644 index 0000000..d1c1403 --- /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. <http://www.collabora.co.uk/> +# +# 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.GetDBusServerAddress(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, []) |