diff --git a/.gitignore b/.gitignore index 8701f960..18202757 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ Makefile.in *.lo *.loT .*.sw? +*.service # Absolute @@ -51,9 +52,6 @@ browser/sugar-marshal.c browser/sugar-marshal.h browser/stamp-sugar-marshal.c browser/stamp-sugar-marshal.h -services/clipboard/org.laptop.Clipboard.service -services/console/org.laptop.sugar.Console.service -services/presence/org.laptop.Sugar.Presence.service bin/sugar shell/extensions/_extensions.c data/sugar.gtkrc diff --git a/build-snapshot.sh b/build-snapshot.sh index 9606dc00..b0624f36 100755 --- a/build-snapshot.sh +++ b/build-snapshot.sh @@ -1,12 +1,10 @@ -VERSION=0.63 -DATE=`date +%Y%m%d` -RELEASE=2.87 -TARBALL=sugar-$VERSION-$RELEASE.${DATE}git.tar.bz2 +VERSION=0.64 +ALPHATAG=`git-show-ref --hash=10 refs/heads/master` +TARBALL=sugar-$VERSION-git$ALPHATAG.tar.bz2 rm sugar-$VERSION.tar.bz2 -XUL_SDK=/home/marco/sugar-jhbuild/build/lib/xulrunner-1.9a5pre-dev -DISTCHECK_CONFIGURE_FLAGS="--with-libxul-sdk=$XUL_SDK" make distcheck +make distcheck mv sugar-$VERSION.tar.bz2 $TARBALL scp $TARBALL mpg@devserv.devel.redhat.com:~ diff --git a/configure.ac b/configure.ac index 3966d46b..e3463161 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([Sugar],[0.63],[],[sugar]) +AC_INIT([Sugar],[0.64],[],[sugar]) AC_PREREQ([2.59]) diff --git a/services/clipboard/Makefile.am b/services/clipboard/Makefile.am index a656ee2f..7a957c24 100644 --- a/services/clipboard/Makefile.am +++ b/services/clipboard/Makefile.am @@ -1,17 +1,22 @@ servicedir = $(datadir)/dbus-1/services -service_in_files = org.laptop.Clipboard.service.in + +service_in_files = \ + org.laptop.Clipboard.service.in \ + org.laptop.ObjectTypeRegistry.service.in + service_DATA = $(service_in_files:.service.in=.service) $(service_DATA): $(service_in_files) Makefile @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ sugardir = $(pkgdatadir)/services/clipboard -sugar_PYTHON = \ - __init__.py \ - clipboardobject.py \ - clipboardservice.py \ - typeregistry.py +sugar_PYTHON = \ + __init__.py \ + clipboardobject.py \ + clipboardservice.py \ + objecttypeservice.py \ + typeregistry.py bin_SCRIPTS = sugar-clipboard diff --git a/services/clipboard/objecttypeservice.py b/services/clipboard/objecttypeservice.py new file mode 100644 index 00000000..bf9083f6 --- /dev/null +++ b/services/clipboard/objecttypeservice.py @@ -0,0 +1,62 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import dbus +import dbus.service + +from sugar.objects.objecttype import ObjectType + +_REGISTRY_IFACE = "org.laptop.ObjectTypeRegistry" +_REGISTRY_PATH = "/org/laptop/ObjectTypeRegistry" + +class ObjectTypeRegistry(dbus.service.Object): + def __init__(self): + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(self._REGISTRY_IFACE, bus=bus) + dbus.service.Object.__init__(self, bus_name, self._REGISTRY_PATH) + + self._types = {} + + self._add_primitive('Text', _('Text'), 'object-text', + [ 'text/rtf' ]) + self._add_primitive('Image', _('Image'), 'object-image', + [ 'image/png' ]) + + def _add_primitive(self, type_id, name, icon, mime_types): + object_type = ObjectType(type_id, name, icon, mime_types) + self._types.add(object_type) + + def _get_type_for_mime(self, mime_type): + for object_type in self._types.values(): + if mime_type in object_type.mime_types: + return object_type + + @dbus.service.method(_CLIPBOARD_DBUS_INTERFACE, + in_signature="s", out_signature="a{sv}") + def GetType(self, type_id): + if self._types.has_key(type_id): + return self._types[type_id].to_dict() + else: + return [] + + @dbus.service.method(_CLIPBOARD_DBUS_INTERFACE, + in_signature="s", out_signature="a{sv}") + def GetTypeForMIME(self, mime_type): + object_type = self._get_type_for_mime(mime_type) + if object_type: + return object_type.to_dict() + else: + return [] diff --git a/services/clipboard/org.laptop.ObjectTypeRegistry.service.in b/services/clipboard/org.laptop.ObjectTypeRegistry.service.in new file mode 100644 index 00000000..66477eb6 --- /dev/null +++ b/services/clipboard/org.laptop.ObjectTypeRegistry.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name = org.laptop.ObjectTypeRegistry +Exec = @bindir@/sugar-clipboard + diff --git a/sugar/activity/activity.py b/sugar/activity/activity.py index dbcbd603..f1bda6c5 100644 --- a/sugar/activity/activity.py +++ b/sugar/activity/activity.py @@ -42,14 +42,14 @@ class ActivityToolbar(gtk.Toolbar): activity.connect('shared', self._activity_shared_cb) activity.connect('joined', self._activity_shared_cb) - if activity.jobject: + if activity.metadata: self.title = gtk.Entry() self.title.set_size_request(int(gtk.gdk.screen_width() / 6), -1) - self.title.set_text(activity.jobject['title']) + self.title.set_text(activity.metadata['title']) self.title.connect('focus-out-event', self._title_focus_out_event_cb) self._add_widget(self.title) - activity.jobject.connect('updated', self._jobject_updated_cb) + activity.metadata.connect('updated', self._jobject_updated_cb) separator = gtk.SeparatorToolItem() separator.props.draw = False @@ -84,8 +84,8 @@ class ActivityToolbar(gtk.Toolbar): self.title.set_text(jobject['title']) def _title_focus_out_event_cb(self, entry, event): - if self._activity.jobject['title'] != self.title.get_text(): - self._activity.jobject['title'] = self.title.get_text() + if self._activity.metadata['title'] != self.title.get_text(): + self._activity.metadata['title'] = self.title.get_text() self._activity.save() def _add_widget(self, widget, expand=False): @@ -199,54 +199,62 @@ class Activity(Window, gtk.Container): self._bus = ActivityService(self) if handle.object_id: - self.jobject = datastore.get(handle.object_id) - self.jobject.object_id = '' - del self.jobject['ctime'] - del self.jobject['mtime'] + self._jobject = datastore.get(handle.object_id) + self._jobject.object_id = '' + del self._jobject.metadata['ctime'] + del self._jobject.metadata['mtime'] elif create_jobject: logging.debug('Creating a jobject.') - self.jobject = datastore.create() - self.jobject['title'] = '%s %s' % (get_bundle_name(), 'Activity') - self.jobject['activity'] = self.get_service_name() - self.jobject['keep'] = '0' - self.jobject['buddies'] = '' - self.jobject['preview'] = '' - self.jobject['icon-color'] = profile.get_color().to_string() - self.jobject.file_path = '' - datastore.write(self.jobject, + self._jobject = datastore.create() + self._jobject.metadata['title'] = '%s %s' % (get_bundle_name(), 'Activity') + self._jobject.metadata['activity'] = self.get_service_name() + self._jobject.metadata['keep'] = '0' + self._jobject.metadata['buddies'] = '' + self._jobject.metadata['preview'] = '' + self._jobject.metadata['icon-color'] = profile.get_color().to_string() + self._jobject.file_path = '' + datastore.write(self._jobject, reply_handler=self._internal_jobject_create_cb, error_handler=self._internal_jobject_error_cb) else: - self.jobject = None + self._jobject = None 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: + if not self._active and self._jobject: self.save() def do_get_property(self, pspec): if pspec.name == 'active': return self._active + def set_canvas(self, canvas): + 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 _internal_jobject_create_cb(self): pass def _internal_jobject_error_cb(self, err): logging.debug("Error creating activity datastore object: %s" % err) - def read_file(self): + def read_file(self, file_path): """ Subclasses implement this method if they support resuming objects from - the journal. Can access the object through the jobject attribute. + the journal. 'file_path' is the file to read from. """ raise NotImplementedError - def write_file(self): + def write_file(self, file_path): """ Subclasses implement this method if they support saving data to objects - in the journal. Can access the object through the jobject attribute. + in the journal. 'file_path' is the file to write to. """ raise NotImplementedError @@ -259,11 +267,12 @@ class Activity(Window, gtk.Container): def save(self): """Request that the activity is saved to the Journal.""" try: - self.jobject.file_path = os.path.join('/tmp', '%i.txt' % time.time()) - self.write_file() + file_path = os.path.join('/tmp', '%i' % time.time()) + self.write_file(file_path) + self._jobject.file_path = file_path except NotImplementedError: - self.jobject.file_path = '' - datastore.write(self.jobject, + pass + datastore.write(self._jobject, reply_handler=self._internal_save_cb, error_handler=self._internal_save_error_cb) @@ -322,7 +331,7 @@ class Activity(Window, gtk.Container): self._shared_activity.leave() def close(self): - if self.jobject: + if self._jobject: try: self.save() except: @@ -330,6 +339,14 @@ class Activity(Window, gtk.Container): raise self.destroy() + def get_metadata(self): + if self._jobject: + return self._jobject.metadata + else: + return None + + metadata = property(get_metadata, None) + def get_bundle_name(): """Return the bundle name for the current process' bundle """ diff --git a/sugar/datastore/datastore.py b/sugar/datastore/datastore.py index 82f50266..52d735f9 100644 --- a/sugar/datastore/datastore.py +++ b/sugar/datastore/datastore.py @@ -19,38 +19,48 @@ import gobject from sugar.datastore import dbus_helpers -class DSObject(gobject.GObject): +class DSMetadata(gobject.GObject): __gsignals__ = { 'updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) } - def __init__(self, object_id, metadata=None, file_path=None): + def __init__(self, props={}): gobject.GObject.__init__(self) + self._props = props + + 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 has_key(self, key): + return self._props.has_key(key) + + def get_dictionary(self): + return self._props + +class DSObject: + def __init__(self, object_id, metadata=None, file_path=None): self.object_id = object_id self._metadata = metadata self._file_path = file_path - def __getitem__(self, key): - return self.metadata[key] - - def __setitem__(self, key, value): - if not self.metadata.has_key(key) or self.metadata[key] != value: - self.metadata[key] = value - self.emit('updated') - - def __delitem__(self, key): - del self.metadata[key] - def get_metadata(self): if self._metadata is None and not self.object_id is None: - self.set_metadata(dbus_helpers.get_properties(self.object_id)) + 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 - self.emit('updated') metadata = property(get_metadata, set_metadata) @@ -62,7 +72,6 @@ class DSObject(gobject.GObject): def set_file_path(self, file_path): if self._file_path != file_path: self._file_path = file_path - self.emit('updated') file_path = property(get_file_path, set_file_path) @@ -71,23 +80,23 @@ def get(object_id): metadata = dbus_helpers.get_properties(object_id) file_path = dbus_helpers.get_filename(object_id) - ds_object = DSObject(object_id, metadata, file_path) + ds_object = DSObject(object_id, DSMetadata(metadata), file_path) # TODO: register the object for updates return ds_object def create(): - return DSObject(object_id=None, metadata={}, file_path=None) + return DSObject(object_id=None, metadata=DSMetadata(), file_path=None) def write(ds_object, reply_handler=None, error_handler=None): logging.debug('datastore.write') if ds_object.object_id: dbus_helpers.update(ds_object.object_id, - ds_object.metadata, + ds_object.metadata.get_dictionary(), ds_object.file_path, reply_handler=reply_handler, error_handler=error_handler) else: - ds_object.object_id = dbus_helpers.create(ds_object.metadata, + ds_object.object_id = dbus_helpers.create(ds_object.metadata.get_dictionary(), ds_object.file_path) # TODO: register the object for updates logging.debug('Written object %s to the datastore.' % ds_object.object_id) @@ -114,7 +123,7 @@ def find(query, sorting=None, limit=None, offset=None, reply_handler=None, object_id = props['uid'] del props['uid'] - ds_object = DSObject(object_id, props, file_path) + ds_object = DSObject(object_id, DSMetadata(props), file_path) objects.append(ds_object) return objects, total_count diff --git a/sugar/graphics/palette.py b/sugar/graphics/palette.py index c174e6e6..3daeaadc 100644 --- a/sugar/graphics/palette.py +++ b/sugar/graphics/palette.py @@ -17,6 +17,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gtk +from gtk import gdk, keysyms import gobject import pango @@ -49,6 +50,7 @@ class Palette(gtk.Window): self._palette_label = gtk.Label() self._palette_label.set_ellipsize(pango.ELLIPSIZE_START) + self._palette_label.show() self._separator = gtk.HSeparator() self._separator.hide() @@ -58,7 +60,10 @@ class Palette(gtk.Window): self._menu_bar.show() self._content = gtk.HBox() + self._content.show() + self._button_bar = gtk.HButtonBox() + self._button_bar.show() # Set main container vbox = gtk.VBox(False, 0) @@ -69,11 +74,25 @@ class Palette(gtk.Window): vbox.pack_start(self._button_bar, True, True, self._PADDING) vbox.show() - # FIXME - self.connect('focus_out_event', self._close_palette) + # Widget events + self.connect('motion-notify-event', self._mouse_over_widget) + self.connect('leave-notify-event', self._mouse_out_widget) + self.connect('button-press-event', self._close_palette) + self.connect('key-press-event', self._on_key_press_event) self.set_border_width(self._WIN_BORDER) self.add(vbox) + + def _is_mouse_out(self, window, event): + # If we're clicking outside of the Palette + # return True + if (event.window != self.window or + (tuple(self.allocation.intersect( + gdk.Rectangle(x=int(event.x), y=int(event.y), + width=1, height=1)))) == (0, 0, 0, 0)): + return True + else: + return False def do_set_property(self, pspec, value): @@ -99,7 +118,7 @@ class Palette(gtk.Window): elif self._alignment == ALIGNMENT_BOTTOM_RIGHT: move_x = parent_rectangle.x - palette_width move_y = window_axis[1] + parent_rectangle.y + parent_rectangle.height - + elif self._alignment == ALIGNMENT_LEFT_BOTTOM: move_x = parent_rectangle.x - palette_width move_y = palette_rectangle.y @@ -126,8 +145,9 @@ class Palette(gtk.Window): self.move(move_x, move_y) - def _close_palette(self, widget, event): - self.destroy() + def _close_palette(self, widget=None, event=None): + gtk.gdk.pointer_ungrab() + self.hide() def set_primary_state(self, label, accel_path=None): if accel_path != None: @@ -148,9 +168,42 @@ class Palette(gtk.Window): widget.show() def append_button(self, button): + button.connect('released', self._close_palette) self._button_bar.pack_start(button, True, True, self._PADDING) button.show() def display(self, button): self.show() self.set_position() + self._pointer_grab() + + def _pointer_grab(self): + gtk.gdk.pointer_grab(self.window, owner_events=False, + event_mask=gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.BUTTON_RELEASE_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK | + gtk.gdk.POINTER_MOTION_MASK) + + gdk.keyboard_grab(self.window, False) + + def _mouse_out_widget(self, widget, event): + if (widget == self) and self._is_mouse_out(widget, event): + self._pointer_grab() + + def _mouse_over_widget(self, widget, event): + gtk.gdk.pointer_ungrab() + + def _on_key_press_event(self, window, event): + + # Escape or Alt+Up: Close + # Enter, Return or Space: Select + + keyval = event.keyval + state = event.state & gtk.accelerator_get_default_mod_mask() + if (keyval == keysyms.Escape or + ((keyval == keysyms.Up or keyval == keysyms.KP_Up) and + state == gdk.MOD1_MASK)): + self._close_palette() + elif keyval == keysyms.Tab: + self._close_palette() diff --git a/sugar/graphics/toolbutton.py b/sugar/graphics/toolbutton.py index 51edc2e4..a2a6ce2b 100644 --- a/sugar/graphics/toolbutton.py +++ b/sugar/graphics/toolbutton.py @@ -36,4 +36,5 @@ class ToolButton(gtk.ToolButton): palette.props.alignment = ALIGNMENT_BOTTOM_LEFT def set_tooltip(self, text): - pass + tp = gtk.Tooltips() + self.set_tooltip(tp, text, text) diff --git a/sugar/logger.py b/sugar/logger.py index fa2e28fb..495d7a00 100644 --- a/sugar/logger.py +++ b/sugar/logger.py @@ -21,6 +21,7 @@ import os import logging import traceback from cStringIO import StringIO +import time from sugar import env @@ -113,6 +114,67 @@ def start(module_id): sys.excepthook = __exception_handler def cleanup(): - logs_dir = _get_logs_dir() - for f in os.listdir(logs_dir): - os.remove(os.path.join(logs_dir, f)) + logs_dir = _get_logs_dir() + + # File extension for backed up logfiles. + + file_suffix = int(time.time()) + + # Absolute directory path where to store old logfiles. + # It will be created recursivly if it's not present. + + backup_dirpath = os.path.join(logs_dir, 'old') + + # How many versions shall be backed up of every logfile? + + num_backup_versions = 4 + + # Make sure the backup location for old log files exists + + if not os.path.exists(backup_dirpath): + os.makedirs(backup_dirpath) + + # Iterate over every item in 'logs' directory + + for filename in os.listdir(logs_dir): + + old_filepath = os.path.join(logs_dir, filename) + + if os.path.isfile(old_filepath): + + # Backup every file + + new_filename = filename + '.' + str(file_suffix) + new_filepath = os.path.join(backup_dirpath, new_filename) + os.rename(old_filepath, new_filepath) + + backup_map = {} + + # Temporarily map all backup logfiles + + for filename in os.listdir(backup_dirpath): + + # Remove the 'file_suffix' from the filename. + + end = filename.rfind(".") + key = filename[0:end].lower() + key = key.replace(".", "_") + + if key not in backup_map: + backup_map[key] = [] + + backup_list = backup_map[key] + + backup_list.append( os.path.join(backup_dirpath, filename) ) + + # Only keep 'num_backup_versions' versions of every logfile. + # Remove the others. + + for key in backup_map: + backup_list = backup_map[key] + backup_list.sort() + backup_list.reverse() + + for i in range(num_backup_versions, len(backup_list)): + os.remove(backup_list[i]) + diff --git a/sugar/objects/objecttype.py b/sugar/objects/objecttype.py new file mode 100644 index 00000000..81b8ec12 --- /dev/null +++ b/sugar/objects/objecttype.py @@ -0,0 +1,43 @@ +_SERVICE = "org.laptop.ObjectTypeRegistry" +_PATH = "/org/laptop/ObjectTypeRegistry" +_IFACE = "org.laptop.ObjectTypeRegistry" + +def _object_type_from_dict(info_dict): + if info_dict: + return ObjectType(info_dict['type_id'], + info_dict['name'], + info_dict['icon']) + else: + return None + +class ObjectType(object): + def __init__(self, type_id, name, icon, mime_types): + self.type_id = type_id + self.name = name + self.icon = icon + self.mime_types = [] + + def to_dict(self): + return { 'type_id' : self.type_id, + 'name' : self.name, + 'icon' : self.icon + } + +class ObjectTypeRegistry(object): + def __init__(self): + bus = dbus.SessionBus() + bus_object = bus.get_object(_SERVICE, _PATH) + self._registry = dbus.Interface(bus_object, _IFACE) + + def get_type(type_id): + type_dict = self._registry.GetType(type_id) + return _object_type_from_dict(type_dict) + + def get_type_for_mime(mime_type): + type_dict = self._registry.GetTypeForMime(type_id) + return _object_type_from_dict(type_dict) + +_registry = ObjectRegistry() + +def get_registry(): + return _registry