diff options
Diffstat (limited to 'src/jarabe/desktop/favoritesview.py')
-rw-r--r-- | src/jarabe/desktop/favoritesview.py | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py new file mode 100644 index 0000000..654f400 --- /dev/null +++ b/src/jarabe/desktop/favoritesview.py @@ -0,0 +1,702 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import math + +import gobject +import gconf +import glib +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.icon import Icon, CanvasIcon +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.alert import Alert +from sugar.graphics.xocolor import XoColor +from sugar.activity import activityfactory +from sugar import dispatch +from sugar.datastore import datastore + +from jarabe.view.palettes import JournalPalette +from jarabe.view.palettes import CurrentActivityPalette, ActivityPalette +from jarabe.view.buddyicon import BuddyIcon +from jarabe.view.buddymenu import BuddyMenu +from jarabe.model.buddy import get_owner_instance +from jarabe.model import shell +from jarabe.model import bundleregistry +from jarabe.journal import misc + +from jarabe.desktop import schoolserver +from jarabe.desktop.schoolserver import RegisterError +from jarabe.desktop import favoriteslayout + + +_logger = logging.getLogger('FavoritesView') + +_ICON_DND_TARGET = ('activity-icon', gtk.TARGET_SAME_WIDGET, 0) + +LAYOUT_MAP = {favoriteslayout.RingLayout.key: favoriteslayout.RingLayout, + #favoriteslayout.BoxLayout.key: favoriteslayout.BoxLayout, + #favoriteslayout.TriangleLayout.key: favoriteslayout.TriangleLayout, + #favoriteslayout.SunflowerLayout.key: favoriteslayout.SunflowerLayout, + favoriteslayout.RandomLayout.key: favoriteslayout.RandomLayout} +"""Map numeric layout identifiers to uninstantiated subclasses of +`FavoritesLayout` which implement the layouts. Additional information +about the layout can be accessed with fields of the class.""" + +_favorites_settings = None + + +class FavoritesView(hippo.Canvas): + __gtype_name__ = 'SugarFavoritesView' + + def __init__(self, **kwargs): + logging.debug('STARTUP: Loading the favorites view') + + gobject.GObject.__init__(self, **kwargs) + + # DND stuff + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._hot_x = None + self._hot_y = None + self._last_clicked_icon = None + + self._box = hippo.CanvasBox() + self._box.props.background_color = style.COLOR_WHITE.get_int() + self.set_root(self._box) + + self._my_icon = OwnerIcon(style.XLARGE_ICON_SIZE) + self._my_icon.connect('register-activate', self.__register_activate_cb) + self._box.append(self._my_icon) + + self._current_activity = CurrentActivityIcon() + self._box.append(self._current_activity) + + self._layout = None + self._alert = None + self._resume_mode = True + + # More DND stuff + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + self.connect('button-press-event', self.__button_press_event_cb) + self.connect('drag-begin', self.__drag_begin_cb) + self.connect('drag-motion', self.__drag_motion_cb) + self.connect('drag-drop', self.__drag_drop_cb) + self.connect('drag-data-received', self.__drag_data_received_cb) + + gobject.idle_add(self.__connect_to_bundle_registry_cb) + + favorites_settings = get_settings() + favorites_settings.changed.connect(self.__settings_changed_cb) + self._set_layout(favorites_settings.layout) + + def set_filter(self, query): + query = query.strip() + for icon in self._box.get_children(): + if icon not in [self._my_icon, self._current_activity]: + activity_name = icon.get_activity_name().lower() + if activity_name.find(query) > -1: + icon.alpha = 1.0 + else: + icon.alpha = 0.33 + + def __settings_changed_cb(self, **kwargs): + favorites_settings = get_settings() + self._set_layout(favorites_settings.layout) + + def __connect_to_bundle_registry_cb(self): + registry = bundleregistry.get_registry() + + for info in registry: + if registry.is_bundle_favorite(info.get_bundle_id(), + info.get_activity_version()): + self._add_activity(info) + + registry.connect('bundle-added', self.__activity_added_cb) + registry.connect('bundle-removed', self.__activity_removed_cb) + registry.connect('bundle-changed', self.__activity_changed_cb) + + def _add_activity(self, activity_info): + if activity_info.get_bundle_id() == 'org.laptop.JournalActivity': + return + icon = ActivityIcon(activity_info) + icon.props.size = style.STANDARD_ICON_SIZE + icon.set_resume_mode(self._resume_mode) + self._box.insert_sorted(icon, 0, self._layout.compare_activities) + self._layout.append(icon) + + def __activity_added_cb(self, activity_registry, activity_info): + registry = bundleregistry.get_registry() + if registry.is_bundle_favorite(activity_info.get_bundle_id(), + activity_info.get_activity_version()): + self._add_activity(activity_info) + + def _find_activity_icon(self, bundle_id, version): + for icon in self._box.get_children(): + if isinstance(icon, ActivityIcon) and \ + icon.bundle_id == bundle_id and icon.version == version: + return icon + return None + + def __activity_removed_cb(self, activity_registry, activity_info): + icon = self._find_activity_icon(activity_info.get_bundle_id(), + activity_info.get_activity_version()) + if icon is not None: + self._layout.remove(icon) + self._box.remove(icon) + + def __activity_changed_cb(self, activity_registry, activity_info): + if activity_info.get_bundle_id() == 'org.laptop.JournalActivity': + return + icon = self._find_activity_icon(activity_info.get_bundle_id(), + activity_info.get_activity_version()) + if icon is not None: + self._box.remove(icon) + + registry = bundleregistry.get_registry() + if registry.is_bundle_favorite(activity_info.get_bundle_id(), + activity_info.get_activity_version()): + self._add_activity(activity_info) + + def do_size_allocate(self, allocation): + width = allocation.width + height = allocation.height + + min_w_, my_icon_width = self._my_icon.get_width_request() + min_h_, my_icon_height = self._my_icon.get_height_request( + my_icon_width) + x = (width - my_icon_width) / 2 + y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + self._layout.move_icon(self._my_icon, x, y, locked=True) + + min_w_, icon_width = self._current_activity.get_width_request() + min_h_, icon_height = \ + self._current_activity.get_height_request(icon_width) + x = (width - icon_width) / 2 + y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + \ + my_icon_height + style.DEFAULT_PADDING + self._layout.move_icon(self._current_activity, x, y, locked=True) + + hippo.Canvas.do_size_allocate(self, allocation) + + # TODO: Dnd methods. This should be merged somehow inside hippo-canvas. + def __button_press_event_cb(self, widget, event): + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + self._last_clicked_icon = self._get_icon_at_coords(event.x, + event.y) + if self._last_clicked_icon is not None: + self._pressed_button = event.button + self._press_start_x = event.x + self._press_start_y = event.y + + return False + + def _get_icon_at_coords(self, x, y): + for icon in self._box.get_children(): + icon_x, icon_y = icon.get_context().translate_to_widget(icon) + icon_width, icon_height = icon.get_allocation() + + if (x >= icon_x) and (x <= icon_x + icon_width) and \ + (y >= icon_y) and (y <= icon_y + icon_height) and \ + isinstance(icon, ActivityIcon): + return icon + return None + + def __motion_notify_event_cb(self, widget, event): + if not self._pressed_button: + return False + + # if the mouse button is not pressed, no drag should occurr + if not event.state & gtk.gdk.BUTTON1_MASK: + self._pressed_button = None + return False + + if event.is_hint: + x, y, state_ = event.window.get_pointer() + else: + x = event.x + y = event.y + + if widget.drag_check_threshold(int(self._press_start_x), + int(self._press_start_y), + int(x), + int(y)): + context_ = widget.drag_begin([_ICON_DND_TARGET], + gtk.gdk.ACTION_MOVE, + 1, + event) + return False + + def __drag_begin_cb(self, widget, context): + icon_file_name = self._last_clicked_icon.props.file_name + # TODO: we should get the pixbuf from the widget, so it has colors, etc + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_file_name) + + self._hot_x = pixbuf.props.width / 2 + self._hot_y = pixbuf.props.height / 2 + context.set_icon_pixbuf(pixbuf, self._hot_x, self._hot_y) + + def __drag_motion_cb(self, widget, context, x, y, time): + if self._last_clicked_icon is not None: + context.drag_status(context.suggested_action, time) + return True + else: + return False + + def __drag_drop_cb(self, widget, context, x, y, time): + if self._last_clicked_icon is not None: + self.drag_get_data(context, _ICON_DND_TARGET[0]) + + self._layout.move_icon(self._last_clicked_icon, + x - self._hot_x, y - self._hot_y) + + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._hot_x = None + self._hot_y = None + self._last_clicked_icon = None + + return True + else: + return False + + def __drag_data_received_cb(self, widget, context, x, y, selection_data, + info, time): + context.drop_finish(success=True, time=time) + + def _set_layout(self, layout): + if layout not in LAYOUT_MAP: + logging.warn('Unknown favorites layout: %r', layout) + layout = favoriteslayout.RingLayout.key + assert layout in LAYOUT_MAP + + if type(self._layout) == LAYOUT_MAP[layout]: + return + + self._layout = LAYOUT_MAP[layout]() + self._box.set_layout(self._layout) + + #TODO: compatibility hack while sort() gets added to the hippo python + # bindings + if hasattr(self._box, 'sort'): + self._box.sort(self._layout.compare_activities) + + for icon in self._box.get_children(): + if icon not in [self._my_icon, self._current_activity]: + self._layout.append(icon) + + self._layout.append(self._my_icon, locked=True) + self._layout.append(self._current_activity, locked=True) + + if self._layout.allow_dnd(): + self.drag_source_set(0, [], 0) + self.drag_dest_set(0, [], 0) + else: + self.drag_source_unset() + self.drag_dest_unset() + + layout = property(None, _set_layout) + + def add_alert(self, alert): + if self._alert is not None: + self.remove_alert() + alert.set_size_request(gtk.gdk.screen_width(), -1) + self._alert = hippo.CanvasWidget(widget=alert) + self._box.append(self._alert, hippo.PACK_FIXED) + + def remove_alert(self): + self._box.remove(self._alert) + self._alert = None + + def __register_activate_cb(self, icon): + alert = Alert() + try: + schoolserver.register_laptop() + except RegisterError, e: + alert.props.title = _('Registration Failed') + alert.props.msg = '%s' % e + else: + alert.props.title = _('Registration Successful') + alert.props.msg = _('You are now registered ' \ + 'with your school server.') + self._my_icon.set_registered() + + ok_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon) + + self.add_alert(alert) + alert.connect('response', self.__register_alert_response_cb) + + def __register_alert_response_cb(self, alert, response_id): + self.remove_alert() + + def set_resume_mode(self, resume_mode): + self._resume_mode = resume_mode + for icon in self._box.get_children(): + if hasattr(icon, 'set_resume_mode'): + icon.set_resume_mode(self._resume_mode) + + +class ActivityIcon(CanvasIcon): + __gtype_name__ = 'SugarFavoriteActivityIcon' + + _BORDER_WIDTH = style.zoom(3) + _MAX_RESUME_ENTRIES = 5 + + def __init__(self, activity_info): + CanvasIcon.__init__(self, cache=True, + file_name=activity_info.get_icon()) + + self._activity_info = activity_info + self._journal_entries = [] + self._hovering = False + self._resume_mode = True + + self.connect('hovering-changed', self.__hovering_changed_event_cb) + self.connect('button-release-event', self.__button_release_event_cb) + + datastore.updated.connect(self.__datastore_listener_updated_cb) + datastore.deleted.connect(self.__datastore_listener_deleted_cb) + + self._refresh() + self._update() + + def _refresh(self): + bundle_id = self._activity_info.get_bundle_id() + properties = ['uid', 'title', 'icon-color', 'activity', 'activity_id', + 'mime_type', 'mountpoint'] + self._get_last_activity_async(bundle_id, properties) + + def __datastore_listener_updated_cb(self, **kwargs): + bundle_id = self._activity_info.get_bundle_id() + if kwargs['metadata'].get('activity', '') == bundle_id: + self._refresh() + + def __datastore_listener_deleted_cb(self, **kwargs): + for entry in self._journal_entries: + if entry['uid'] == kwargs['object_id']: + self._refresh() + break + + def _get_last_activity_async(self, bundle_id, properties): + query = {'activity': bundle_id} + datastore.find(query, sorting=['+timestamp'], + limit=self._MAX_RESUME_ENTRIES, + properties=properties, + reply_handler=self.__get_last_activity_reply_handler_cb, + error_handler=self.__get_last_activity_error_handler_cb) + + def __get_last_activity_reply_handler_cb(self, entries, total_count): + # If there's a problem with the DS index, we may get entries not + # related to this activity. + checked_entries = [] + for entry in entries: + if entry['activity'] == self.bundle_id: + checked_entries.append(entry) + + self._journal_entries = checked_entries + self._update() + + def __get_last_activity_error_handler_cb(self, error): + logging.error('Error retrieving most recent activities: %r', error) + + def _update(self): + self.palette = None + if not self._resume_mode or not self._journal_entries: + xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + xo_color = misc.get_icon_color(self._journal_entries[0]) + self.props.xo_color = xo_color + + def create_palette(self): + palette = FavoritePalette(self._activity_info, self._journal_entries) + palette.connect('activate', self.__palette_activate_cb) + palette.connect('entry-activate', self.__palette_entry_activate_cb) + return palette + + def __palette_activate_cb(self, palette): + self._activate() + + def __palette_entry_activate_cb(self, palette, metadata): + self._resume(metadata) + + def __hovering_changed_event_cb(self, icon, hovering): + self._hovering = hovering + self.emit_paint_needed(0, 0, -1, -1) + + def do_paint_above_children(self, cr, damaged_box): + if not self._hovering: + return + + width, height = self.get_allocation() + + x = ActivityIcon._BORDER_WIDTH / 2.0 + y = ActivityIcon._BORDER_WIDTH / 2.0 + width -= ActivityIcon._BORDER_WIDTH + height -= ActivityIcon._BORDER_WIDTH + radius = width / 10.0 + + cr.move_to(x + radius, y) + cr.arc(x + width - radius, y + radius, radius, math.pi * 1.5, + math.pi * 2.0) + cr.arc(x + width - radius, x + height - radius, radius, 0, + math.pi * 0.5) + cr.arc(x + radius, y + height - radius, radius, math.pi * 0.5, math.pi) + cr.arc(x + radius, y + radius, radius, math.pi, math.pi * 1.5) + + color = style.COLOR_SELECTION_GREY.get_int() + hippo.cairo_set_source_rgba32(cr, color) + cr.set_line_width(ActivityIcon._BORDER_WIDTH) + cr.stroke() + + def do_get_content_height_request(self, for_width): + height, height = CanvasIcon.do_get_content_height_request(self, + for_width) + height += ActivityIcon._BORDER_WIDTH * 2 + return height, height + + def do_get_content_width_request(self): + width, width = CanvasIcon.do_get_content_width_request(self) + width += ActivityIcon._BORDER_WIDTH * 2 + return width, width + + def __button_release_event_cb(self, icon, event): + self._activate() + + def _resume(self, journal_entry): + if not journal_entry['activity_id']: + journal_entry['activity_id'] = activityfactory.create_activity_id() + misc.resume(journal_entry, self._activity_info.get_bundle_id()) + + def _activate(self): + if self.palette is not None: + self.palette.popdown(immediate=True) + + if self._resume_mode and self._journal_entries: + self._resume(self._journal_entries[0]) + else: + misc.launch(self._activity_info) + + def get_bundle_id(self): + return self._activity_info.get_bundle_id() + bundle_id = property(get_bundle_id, None) + + def get_version(self): + return self._activity_info.get_activity_version() + version = property(get_version, None) + + def get_activity_name(self): + return self._activity_info.get_name() + + def _get_installation_time(self): + return self._activity_info.get_installation_time() + installation_time = property(_get_installation_time, None) + + def _get_fixed_position(self): + registry = bundleregistry.get_registry() + return registry.get_bundle_position(self.bundle_id, self.version) + fixed_position = property(_get_fixed_position, None) + + def set_resume_mode(self, resume_mode): + self._resume_mode = resume_mode + self._update() + + +class FavoritePalette(ActivityPalette): + __gtype_name__ = 'SugarFavoritePalette' + + __gsignals__ = { + 'entry-activate': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + } + + def __init__(self, activity_info, journal_entries): + ActivityPalette.__init__(self, activity_info) + + if not journal_entries: + xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + xo_color = misc.get_icon_color(journal_entries[0]) + + self.props.icon = Icon(file=activity_info.get_icon(), + xo_color=xo_color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + + if journal_entries: + title = journal_entries[0]['title'] + self.props.secondary_text = glib.markup_escape_text(title) + + menu_items = [] + for entry in journal_entries: + icon_file_name = misc.get_icon_name(entry) + color = misc.get_icon_color(entry) + + menu_item = MenuItem(text_label=entry['title'], + file_name=icon_file_name, + xo_color=color) + menu_item.connect('activate', self.__resume_entry_cb, entry) + menu_items.append(menu_item) + menu_item.show() + + if journal_entries: + separator = gtk.SeparatorMenuItem() + menu_items.append(separator) + separator.show() + + for i in range(0, len(menu_items)): + self.menu.insert(menu_items[i], i) + + def __resume_entry_cb(self, menu_item, entry): + if entry is not None: + self.emit('entry-activate', entry) + + +class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): + def __init__(self): + CanvasIcon.__init__(self, cache=True) + self._home_model = shell.get_model() + self._home_activity = self._home_model.get_active_activity() + + if self._home_activity is not None: + self._update() + + self._home_model.connect('active-activity-changed', + self.__active_activity_changed_cb) + + self.connect('button-release-event', self.__button_release_event_cb) + + def __button_release_event_cb(self, icon, event): + window = self._home_model.get_active_activity().get_window() + window.activate(gtk.get_current_event_time()) + + def _update(self): + self.props.file_name = self._home_activity.get_icon_path() + self.props.xo_color = self._home_activity.get_icon_color() + self.props.size = style.STANDARD_ICON_SIZE + + if self.palette is not None: + self.palette.destroy() + self.palette = None + + def create_palette(self): + if self._home_activity.is_journal(): + palette = JournalPalette(self._home_activity) + else: + palette = CurrentActivityPalette(self._home_activity) + return palette + + def __active_activity_changed_cb(self, home_model, home_activity): + self._home_activity = home_activity + self._update() + + +class OwnerIcon(BuddyIcon): + __gtype_name__ = 'SugarFavoritesOwnerIcon' + + __gsignals__ = { + 'register-activate': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])), + } + + def __init__(self, size): + BuddyIcon.__init__(self, buddy=get_owner_instance(), size=size) + + self.palette_invoker.cache_palette = True + + self._palette_enabled = False + self._register_menu = None + + def create_palette(self): + if not self._palette_enabled: + self._palette_enabled = True + return + + palette = BuddyMenu(get_owner_instance()) + + client = gconf.client_get_default() + backup_url = client.get_string('/desktop/sugar/backup_url') + + if not backup_url: + self._register_menu = MenuItem(_('Register'), 'media-record') + else: + self._register_menu = MenuItem(_('Register again'), + 'media-record') + + self._register_menu.connect('activate', self.__register_activate_cb) + palette.menu.append(self._register_menu) + self._register_menu.show() + + return palette + + def get_toplevel(self): + return hippo.get_canvas_for_item(self).get_toplevel() + + def __register_activate_cb(self, menuitem): + self.emit('register-activate') + + def set_registered(self): + self.palette.menu.remove(self._register_menu) + self._register_menu = MenuItem(_('Register again'), 'media-record') + self._register_menu.connect('activate', self.__register_activate_cb) + self.palette.menu.append(self._register_menu) + self._register_menu.show() + + +class FavoritesSetting(object): + + _FAVORITES_KEY = '/desktop/sugar/desktop/favorites_layout' + + def __init__(self): + client = gconf.client_get_default() + self._layout = client.get_string(self._FAVORITES_KEY) + logging.debug('FavoritesSetting layout %r', self._layout) + + self._mode = None + + self.changed = dispatch.Signal() + + def get_layout(self): + return self._layout + + def set_layout(self, layout): + logging.debug('set_layout %r %r', layout, self._layout) + if layout != self._layout: + self._layout = layout + + client = gconf.client_get_default() + client.set_string(self._FAVORITES_KEY, layout) + + self.changed.send(self) + + layout = property(get_layout, set_layout) + + +def get_settings(): + global _favorites_settings + if _favorites_settings is None: + _favorites_settings = FavoritesSetting() + return _favorites_settings |