# 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 from gi.repository import GObject from gi.repository import GConf from gi.repository import GLib from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf from sugar3.graphics import style from sugar3.graphics.icon import Icon from sugar3.graphics.icon import CanvasIcon from sugar3.graphics.palettemenu import PaletteMenuBox from sugar3.graphics.palettemenu import PaletteMenuItem from sugar3.graphics.palettemenu import PaletteMenuItemSeparator from sugar3.graphics.alert import Alert from sugar3.graphics.xocolor import XoColor from sugar3.activity import activityfactory from sugar3 import dispatch from sugar3.datastore import datastore from jarabe.view.palettes import JournalPalette from jarabe.view.palettes import CurrentActivityPalette from jarabe.view.palettes import 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 from jarabe.desktop.viewcontainer import ViewContainer from jarabe.util.normalize import normalize_string _logger = logging.getLogger('FavoritesView') _ICON_DND_TARGET = ('activity-icon', Gtk.TargetFlags.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 FavoritesBox(Gtk.VBox): __gtype_name__ = 'SugarFavoritesBox' def __init__(self, favorites): """ If the argument is True, it will load the activities marked as favorites. Else, it will load the activities marked for school. """ Gtk.VBox.__init__(self) self.load_favorites = favorites self._view = FavoritesView(self) self.pack_start(self._view, True, True, 0) self._view.show() self._alert = None def set_filter(self, query): self._view.set_filter(query) def set_resume_mode(self, resume_mode): self._view.set_resume_mode(resume_mode) def grab_focus(self): # overwrite grab focus in order to grab focus from the parent self._view.grab_focus() def add_alert(self, alert): if self._alert is not None: self.remove_alert() self._alert = alert self.pack_start(alert, False, True, 0) self.reorder_child(alert, 0) def remove_alert(self): self.remove(self._alert) self._alert = None class FavoritesView(ViewContainer): __gtype_name__ = 'SugarFavoritesView' def __init__(self, box): self._box = box self._layout = None owner_icon = OwnerIcon(style.XLARGE_ICON_SIZE) owner_icon.connect('register-activate', self.__register_activate_cb) current_activity = CurrentActivityIcon() ViewContainer.__init__(self, layout=self._layout, owner_icon=owner_icon, activity_icon=current_activity) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK) self.drag_dest_set(0, [], 0) # Drag and drop is set only for the Random layout. This is # the flag that enables or disables it. self._dragging_mode = False self._drag_motion_hid = None self._drag_drop_hid = None self._drag_data_received_hid = None self._dragging = False self._pressed_button = None self._press_start_x = 0 self._press_start_y = 0 self._hot_x = None self._hot_y = None self._last_clicked_icon = None self._alert = None self._resume_mode = True 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 __settings_changed_cb(self, **kwargs): favorites_settings = get_settings() layout_set = self._set_layout(favorites_settings.layout) if layout_set: self.set_layout(self._layout) registry = bundleregistry.get_registry() for info in registry: if self._box.load_favorites: if registry.is_bundle_favorite(info.get_bundle_id(), info.get_activity_version()): self._add_activity(info) else: if registry.is_bundle_for_school(info.get_bundle_id(), info.get_activity_version()): self._add_activity(info) 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 False if self._layout is not None and self._dragging_mode: self.disconnect(self._drag_motion_hid) self.disconnect(self._drag_drop_hid) self.disconnect(self._drag_data_received_hid) if layout == favoriteslayout.RandomLayout.key: self._dragging_mode = True self._drag_motion_hid = self.connect( 'drag-motion', self.__drag_motion_cb) self._drag_drop_hid = self.connect( 'drag-drop', self.__drag_drop_cb) self._drag_data_received_hid = self.connect( 'drag-data-received', self.__drag_data_received_cb) else: self._dragging_mode = False self._layout = LAYOUT_MAP[layout]() return True layout = property(None, _set_layout) def do_add(self, child): if child != self._owner_icon and child != self._activity_icon: self._children.append(child) child.connect('button-press-event', self.__button_press_cb) child.connect('button-release-event', self.__button_release_cb) child.connect('motion-notify-event', self.__motion_notify_event_cb) child.connect('drag-begin', self.__drag_begin_cb) if child.get_realized(): child.set_parent_window(self.get_parent_window()) child.set_parent(self) def __button_release_cb(self, widget, event): if self._dragging: return True else: return False def __button_press_cb(self, widget, event): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self._last_clicked_icon = widget self._pressed_button = event.button self._press_start_x = event.x self._press_start_y = event.y return False def __motion_notify_event_cb(self, widget, event): if not self._dragging_mode: return False # if the mouse button is not pressed, no drag should occurr if not event.get_state() & Gdk.ModifierType.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)): self._dragging = True target_entry = Gtk.TargetEntry.new(*_ICON_DND_TARGET) target_list = Gtk.TargetList.new([target_entry]) context_ = widget.drag_begin(target_list, Gdk.DragAction.MOVE, 1, event) return False def __drag_begin_cb(self, widget, context): if not self._dragging_mode: return False pixbuf = GdkPixbuf.Pixbuf.new_from_file(widget.props.file_name) self._hot_x = pixbuf.props.width / 2 self._hot_y = pixbuf.props.height / 2 Gtk.drag_set_icon_pixbuf(context, 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: Gdk.drag_status(context, context.get_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: target = Gdk.Atom.intern_static_string(_ICON_DND_TARGET[0]) self.drag_get_data(context, target, time) self._layout.move_icon(self._last_clicked_icon, x - self._hot_x, y - self._hot_y, self.get_allocation()) 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._dragging = False return True else: return False def __drag_data_received_cb(self, widget, context, x, y, selection_data, info, time): Gdk.drop_finish(context, success=True, time_=time) def __connect_to_bundle_registry_cb(self): registry = bundleregistry.get_registry() if self._box.load_favorites: for info in registry: if registry.is_bundle_favorite(info.get_bundle_id(), info.get_activity_version()): self._add_activity(info) else: for info in registry: if registry.is_bundle_for_school(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.pixel_size = style.STANDARD_ICON_SIZE #icon.set_resume_mode(self._resume_mode) self.add(icon) icon.show() def __activity_added_cb(self, activity_registry, activity_info): registry = bundleregistry.get_registry() if self._box.load_favorites: if registry.is_bundle_favorite(activity_info.get_bundle_id(), activity_info.get_activity_version()): self._add_activity(activity_info) else: if registry.is_bundle_for_school(activity_info.get_bundle_id(), activity_info.get_activity_version()): self._add_activity(activity_info) 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.remove(icon) def _find_activity_icon(self, bundle_id, version): for icon in self.get_children(): if isinstance(icon, ActivityIcon) and \ icon.bundle_id == bundle_id and icon.version == version: return icon return None 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.remove(icon) registry = bundleregistry.get_registry() if self._box.load_favorites: if registry.is_bundle_favorite(activity_info.get_bundle_id(), activity_info.get_activity_version()): self._add_activity(activity_info) else: if registry.is_bundle_for_school(activity_info.get_bundle_id(), activity_info.get_activity_version()): self._add_activity(activity_info) def set_filter(self, query): query = query.strip() for icon in self.get_children(): if icon not in [self._owner_icon, self._activity_icon]: activity_name = icon.get_activity_name().decode('utf-8') normalized_name = normalize_string(activity_name) if normalized_name.find(query) > -1: icon.alpha = 1.0 else: icon.alpha = 0.33 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._owner_icon.set_registered() ok_icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon) self._box.add_alert(alert) alert.connect('response', self.__register_alert_response_cb) def __register_alert_response_cb(self, alert, response_id): self._box.remove_alert() def set_resume_mode(self, resume_mode): self._resume_mode = resume_mode for icon in self.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(9) _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._resume_mode = True self.connect_after('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) self.connect_to_palette_pop_events(palette) return palette def __palette_activate_cb(self, palette): self._activate() def __palette_entry_activate_cb(self, palette, metadata): self._resume(metadata) def do_get_preferred_width(self): width = CanvasIcon.do_get_preferred_width(self)[0] width += ActivityIcon._BORDER_WIDTH * 2 return (width, width) def do_get_preferred_height(self): height = CanvasIcon.do_get_preferred_height(self)[0] height += ActivityIcon._BORDER_WIDTH * 2 return (height, height) 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.SignalFlags.RUN_FIRST, 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.IconSize.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 = PaletteMenuItem(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 = PaletteMenuItemSeparator() menu_items.append(separator) separator.show() for i in range(0, len(menu_items)): self.menu_box.pack_start(menu_items[i], True, True, 0) def __resume_entry_cb(self, menu_item, entry): if entry is not None: self.emit('entry-activate', entry) class CurrentActivityIcon(CanvasIcon): def __init__(self): CanvasIcon.__init__(self, icon_name='activity-journal', pixel_size=style.STANDARD_ICON_SIZE, 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_after('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.pixel_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) self.connect_to_palette_pop_events(palette) 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.SignalFlags.RUN_FIRST, None, ([])), } def __init__(self, size): BuddyIcon.__init__(self, buddy=get_owner_instance(), pixel_size=size) self.palette_invoker.cache_palette = True self._palette_enabled = False self._register_menu = None # This is a workaround to skip the callback for # enter-notify-event in the parent class the first time. def __enter_notify_event_cb(icon, event): self.unset_state_flags(Gtk.StateFlags.PRELIGHT) self.disconnect(self._enter_notify_hid) self._enter_notify_hid = self.connect('enter-notify-event', __enter_notify_event_cb) 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 = PaletteMenuItem(_('Register'), 'media-record') else: self._register_menu = PaletteMenuItem(_('Register again'), 'media-record') self._register_menu.connect('activate', self.__register_activate_cb) palette.menu_box.pack_end(self._register_menu, True, True, 0) self._register_menu.show() self.connect_to_palette_pop_events(palette) return palette def __register_activate_cb(self, menuitem): self.emit('register-activate') def set_registered(self): self.palette.menu_box.remove(self._register_menu) self._register_menu = PaletteMenuItem(_('Register again'), 'media-record') self._register_menu.connect('activate', self.__register_activate_cb) self.palette.menu_box.pack_end(self._register_menu, True, True, 0) 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