From e3a6ece96596d7bf0b61048902676c2e83bc4e55 Mon Sep 17 00:00:00 2001 From: Tomeu Vizoso Date: Wed, 02 Apr 2008 13:56:53 +0000 Subject: Add notifications for activity launch and invitations. --- (limited to 'src') diff --git a/src/view/devices/battery.py b/src/view/devices/battery.py index 01f8100..8a4caf0 100644 --- a/src/view/devices/battery.py +++ b/src/view/devices/battery.py @@ -38,6 +38,7 @@ class DeviceView(TrayIcon): self._model = model self.palette = BatteryPalette(_('My Battery life')) + self.set_palette(self.palette) self.palette.props.invoker = FrameWidgetInvoker(self) self.palette.set_group_id('frame') diff --git a/src/view/devices/network/mesh.py b/src/view/devices/network/mesh.py index 121e56a..bcdc0b2 100644 --- a/src/view/devices/network/mesh.py +++ b/src/view/devices/network/mesh.py @@ -36,6 +36,7 @@ class DeviceView(TrayIcon): self._model = model self.palette = MeshPalette(_("Mesh Network"), model) + self.set_palette(self.palette) self.palette.props.invoker = FrameWidgetInvoker(self) self.palette.set_group_id('frame') diff --git a/src/view/devices/network/wireless.py b/src/view/devices/network/wireless.py index ff1ba53..6b50769 100644 --- a/src/view/devices/network/wireless.py +++ b/src/view/devices/network/wireless.py @@ -46,6 +46,7 @@ class DeviceView(TrayIcon): self._counter = 0 self.palette = WirelessPalette(self._get_palette_primary_text(), meshdev) + self.set_palette(self.palette) self.palette.props.invoker = FrameWidgetInvoker(self) self.palette.set_group_id('frame') self.palette.set_frequency(self._model.props.frequency) diff --git a/src/view/frame/Makefile.am b/src/view/frame/Makefile.am index b02085a..3c5dc79 100644 --- a/src/view/frame/Makefile.am +++ b/src/view/frame/Makefile.am @@ -9,6 +9,7 @@ sugar_PYTHON = \ friendstray.py \ eventarea.py \ frame.py \ + notification.py \ overlaybox.py \ framewindow.py \ zoomtoolbar.py diff --git a/src/view/frame/activitiestray.py b/src/view/frame/activitiestray.py index e2a448e..4fbd5f6 100644 --- a/src/view/frame/activitiestray.py +++ b/src/view/frame/activitiestray.py @@ -1,3 +1,4 @@ +# 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 @@ -23,11 +24,18 @@ from sugar.graphics import style from sugar.graphics.tray import HTray from sugar.graphics.xocolor import XoColor from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, WidgetInvoker +from sugar.graphics.menuitem import MenuItem +from sugar import activity from model import shellmodel from view.palettes import JournalPalette, CurrentActivityPalette from view.pulsingicon import PulsingIcon from view.frame.frameinvoker import FrameWidgetInvoker +from view.frame.notification import NotificationIcon +import view.frame.frame class ActivityButton(RadioToolButton): def __init__(self, home_activity, group): @@ -48,9 +56,9 @@ class ActivityButton(RadioToolButton): self._icon.show() if self._home_activity.get_type() == "org.laptop.JournalActivity": - palette = JournalPalette(self, self._home_activity) + palette = JournalPalette(self._home_activity) else: - palette = CurrentActivityPalette(self, self._home_activity) + palette = CurrentActivityPalette(self._home_activity) palette.props.invoker = FrameWidgetInvoker(self) palette.set_group_id('frame') self.set_palette(palette) @@ -59,23 +67,136 @@ class ActivityButton(RadioToolButton): self._icon.props.pulsing = True self._notify_launching_hid = home_activity.connect('notify::launching', self.__notify_launching_cb) + + self._notif_icon = NotificationIcon() + self._notif_icon.props.xo_color = home_activity.get_icon_color() + if home_activity.get_icon_path(): + self._notif_icon.props.icon_filename = home_activity.get_icon_path() + else: + self._notif_icon.props.icon_name = 'image-missing' + view.frame.frame.get_instance().add_notification(self._notif_icon) else: self._notify_launching_hid = None + self._notif_icon = None def __notify_launching_cb(self, home_activity, pspec): - self._icon.props.pulsing = False - home_activity.disconnect(self._notify_launching_hid) + if not home_activity.props.launching: + if self._notif_icon is not None: + frame = view.frame.frame.get_instance() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + self._icon.props.pulsing = False + home_activity.disconnect(self._notify_launching_hid) + +class InviteButton(ToolButton): + def __init__(self, activity_model): + ToolButton.__init__(self) + + self._activity_model = activity_model + + self._icon = Icon() + self._icon.props.xo_color = activity_model.get_color() + if activity_model.get_icon_name(): + self._icon.props.file = activity_model.get_icon_name() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + palette = InvitePalette(activity_model) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + self.set_palette(palette) + + self.connect('clicked', self.__clicked_cb) + self.connect('destroy', self.__destroy_cb) + + self._notif_icon = NotificationIcon() + self._notif_icon.props.xo_color = activity_model.get_color() + if activity_model.get_icon_name(): + self._notif_icon.props.icon_filename = activity_model.get_icon_name() + else: + self._notif_icon.props.icon_name = 'image-missing' + self._notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + palette = InvitePalette(activity_model) + palette.props.invoker = WidgetInvoker(self._notif_icon) + palette.set_group_id('frame') + self._notif_icon.palette = palette + + view.frame.frame.get_instance().add_notification(self._notif_icon) + + def __button_release_event_cb(self, icon, event): + self.emit('clicked') + + def __clicked_cb(self, button): + if self._notif_icon is not None: + frame = view.frame.frame.get_instance() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + + shell = view.Shell.get_instance() + shell.join_activity(self._activity_model.get_bundle_id(), + self._activity_model.get_id()) + + def __destroy_cb(self, button): + frame = view.frame.frame.get_instance() + frame.remove_notification(self._notif_icon) + +class InvitePalette(Palette): + def __init__(self, activity_model): + self._activity_model = activity_model + + Palette.__init__(self, '') + + registry = activity.get_registry() + activity_info = registry.get_activity(activity_model.get_bundle_id()) + if activity_info: + self.set_primary_text(activity_info.name) + else: + self.set_primary_text(activity_model.get_bundle_id()) + + menu_item = MenuItem(_('Join'), icon_name='dialog-ok') + menu_item.connect('activate', self.__join_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __join_activate_cb(self, menu_item): + shell = view.Shell.get_instance() + shell.join_activity(self._activity_model.get_bundle_id(), + self._activity_model.get_id()) + + def __decline_activate_cb(self, menu_item): + invites = shellmodel.get_instance().get_invites() + for invite in invites: + if invite.get_activity_id() == self._activity_model.get_id(): + invites.remove_invite(invite) + return class ActivitiesTray(HTray): def __init__(self): HTray.__init__(self) self._buttons = {} + self._invite_to_item = {} + self._home_model = shellmodel.get_instance().get_home() self._home_model.connect('activity-added', self.__activity_added_cb) self._home_model.connect('activity-removed', self.__activity_removed_cb) self._home_model.connect('pending-activity-changed', self.__activity_changed_cb) + self._invites = shellmodel.get_instance().get_invites() + for invite in self._invites: + self._add_invite(invite) + self._invites.connect('invite-added', self.__invite_added_cb) + self._invites.connect('invite-removed', self.__invite_removed_cb) + def __activity_added_cb(self, home_model, home_activity): logging.debug('__activity_added_cb: %r' % home_activity) if self.get_children(): @@ -103,3 +224,28 @@ class ActivitiesTray(HTray): def __activity_toggled_cb(self, button, home_activity): home_activity.get_window().activate(1) + def __invite_clicked_cb(self, icon, invite): + self._invites.remove_invite(invite) + + def __invite_added_cb(self, invites, invite): + self._add_invite(invite) + + def __invite_removed_cb(self, invites, invite): + self._remove_invite(invite) + + def _add_invite(self, invite): + mesh = shellmodel.get_instance().get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + if activity_model: + item = InviteButton(activity_model) + item.connect('clicked', self.__invite_clicked_cb, invite) + self.add_item(item) + item.show() + + self._invite_to_item[invite] = item + + def _remove_invite(self, invite): + self.remove_item(self._invite_to_item[invite]) + self._invite_to_item[invite].destroy() + del self._invite_to_item[invite] + diff --git a/src/view/frame/clipboardtray.py b/src/view/frame/clipboardtray.py index e4e7b5f..044aafd 100644 --- a/src/view/frame/clipboardtray.py +++ b/src/view/frame/clipboardtray.py @@ -108,7 +108,7 @@ class ClipboardTray(VTray): icon.show() self._icons[object_id] = icon - objects_to_delete = self.get_children()[self.MAX_ITEMS:] + objects_to_delete = self.get_children()[:-self.MAX_ITEMS] for icon in objects_to_delete: logging.debug('ClipboardTray: deleting surplus object') cb_service = clipboardservice.get_instance() diff --git a/src/view/frame/frame.py b/src/view/frame/frame.py index 0cafeff..4a407b6 100644 --- a/src/view/frame/frame.py +++ b/src/view/frame/frame.py @@ -33,9 +33,11 @@ from view.frame.friendstray import FriendsTray from view.frame.devicestray import DevicesTray from view.frame.framewindow import FrameWindow from view.frame.clipboardpanelwindow import ClipboardPanelWindow +from view.frame.notification import NotificationIcon, NotificationWindow from model.shellmodel import ShellModel _FRAME_HIDING_DELAY = 500 +_NOTIFICATION_DURATION = 5000 class _Animation(animator.Animation): def __init__(self, frame, end): @@ -121,6 +123,8 @@ class Frame(object): self._key_listener = _KeyListener(self) self._mouse_listener = _MouseListener(self) + self._notif_by_icon = {} + def is_visible(self): return self.current_position != 0.0 @@ -279,6 +283,33 @@ class Frame(object): def notify_key_press(self): self._key_listener.key_press() + def add_notification(self, icon): + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + window = NotificationWindow() + window.move(0, 0) + window.add(icon) + icon.show() + window.show() + + self._notif_by_icon[icon] = window + + gobject.timeout_add(_NOTIFICATION_DURATION, + lambda: self.remove_notification(icon)) + + def remove_notification(self, icon): + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + if icon not in self._notif_by_icon: + logging.debug('icon %r not in list of notifications.' % icon) + return + + window = self._notif_by_icon[icon] + window.destroy() + del self._notif_by_icon[icon] + visible = property(is_visible, None) _instance = None diff --git a/src/view/frame/frameinvoker.py b/src/view/frame/frameinvoker.py index 607290e..31f5933 100644 --- a/src/view/frame/frameinvoker.py +++ b/src/view/frame/frameinvoker.py @@ -18,7 +18,6 @@ import gtk from sugar.graphics import style from sugar.graphics.palette import Palette -from sugar.graphics.palette import CanvasInvoker from sugar.graphics.palette import WidgetInvoker def _get_screen_area(): diff --git a/src/view/frame/notification.py b/src/view/frame/notification.py new file mode 100644 index 0000000..e4f3246 --- /dev/null +++ b/src/view/frame/notification.py @@ -0,0 +1,107 @@ +# 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 math + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.style import Color +from sugar.graphics.xocolor import XoColor + +from view.pulsingicon import PulsingIcon + +class NotificationIcon(gtk.EventBox): + __gtype_name__ = 'SugarNotificationIcon' + + __gproperties__ = { + 'xo-color' : (object, None, None, gobject.PARAM_READWRITE), + 'icon-name' : (str, None, None, None, gobject.PARAM_READWRITE), + 'icon-filename' : (str, None, None, None, gobject.PARAM_READWRITE) + } + + _PULSE_TIMEOUT = 3000 + + def __init__(self, **kwargs): + self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE) + gobject.GObject.__init__(self, **kwargs) + self.props.visible_window = False + + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulsing = True + self.add(self._icon) + self._icon.show() + + gobject.timeout_add(self._PULSE_TIMEOUT, self.__stop_pulsing_cb) + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + def __stop_pulsing_cb(self): + self._icon.props.pulsing = False + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + if self._icon.props.base_color != value: + self._icon.props.base_color = value + elif pspec.name == 'icon-name': + if self._icon.props.icon_name != value: + self._icon.props.icon_name = value + elif pspec.name == 'icon-filename': + if self._icon.props.file != value: + self._icon.props.file = value + else: + gtk.HBox.do_set_property(self, pspec, value) + + def do_get_property(self, pspec): + if pspec.name == 'xo-color': + return self._icon.props.base_color + elif pspec.name == 'icon-name': + return self._icon.props.icon_name + elif pspec.name == 'icon-filename': + return self._icon.props.file + else: + return gtk.HBox.do_get_property(self, pspec) + + def _set_palette(self, palette): + self._icon.palette = palette + + def _get_palette(self): + return self._icon.palette + + palette = property(_get_palette, _set_palette) + +class NotificationWindow(gtk.Window): + __gtype_name__ = 'SugarNotificationWindow' + + def __init__(self, **kwargs): + + gtk.Window.__init__(self, **kwargs) + + self.set_decorated(False) + self.set_resizable(False) + self.connect('realize', self._realize_cb) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) + self.modify_bg(gtk.STATE_NORMAL, color) + diff --git a/src/view/home/activitiesring.py b/src/view/home/activitiesring.py index 9d84e30..52d3427 100644 --- a/src/view/home/activitiesring.py +++ b/src/view/home/activitiesring.py @@ -202,9 +202,9 @@ class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): self.props.size = style.STANDARD_ICON_SIZE if home_activity.get_type() == "org.laptop.JournalActivity": - palette = JournalPalette(self, home_activity) + palette = JournalPalette(home_activity) else: - palette = CurrentActivityPalette(self, home_activity) + palette = CurrentActivityPalette(home_activity) self.set_palette(palette) def __pending_activity_changed_cb(self, home_model, home_activity): diff --git a/src/view/palettes.py b/src/view/palettes.py index ff8fad6..1180e95 100644 --- a/src/view/palettes.py +++ b/src/view/palettes.py @@ -28,7 +28,7 @@ from sugar.graphics.menuitem import MenuItem from sugar.graphics.icon import Icon class BasePalette(Palette): - def __init__(self, widget, home_activity): + def __init__(self, home_activity): Palette.__init__(self, '', menu_after_content=True) if home_activity.props.launching: @@ -45,9 +45,9 @@ class BasePalette(Palette): raise NotImplementedError class CurrentActivityPalette(BasePalette): - def __init__(self, widget, home_activity): + def __init__(self, home_activity): self._home_activity = home_activity - BasePalette.__init__(self, widget, home_activity) + BasePalette.__init__(self, home_activity) def setup_palette(self): self.set_primary_text(self._home_activity.get_title()) @@ -89,12 +89,12 @@ class CurrentActivityPalette(BasePalette): self._home_activity.get_window().close(1) class JournalPalette(BasePalette): - def __init__(self, widget, home_activity): + def __init__(self, home_activity): self._home_activity = home_activity self._progress_bar = None self._free_space_label = None - BasePalette.__init__(self, widget, home_activity) + BasePalette.__init__(self, home_activity) def setup_palette(self): self.set_primary_text(self._home_activity.get_title()) diff --git a/src/view/pulsingicon.py b/src/view/pulsingicon.py index 6d8e53f..0a4a825 100644 --- a/src/view/pulsingicon.py +++ b/src/view/pulsingicon.py @@ -75,6 +75,13 @@ class PulsingIcon(Icon): Icon.__init__(self, **kwargs) + self._palette = None + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette is not None: + self._palette.destroy() + # Hack for sharing code between CanvasPulsingIcon and PulsingIcon _get_as_rgba = _get_as_rgba _update_colors = _update_colors @@ -136,6 +143,16 @@ class PulsingIcon(Icon): else: return Icon.do_get_property(self, pspec) + def _get_palette(self): + return self._palette + + def _set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + + palette = property(_get_palette, _set_palette) + class CanvasPulsingIcon(CanvasIcon): __gtype_name__ = 'SugarCanvasPulsingIcon' -- cgit v0.9.1