diff options
author | Martin Abente <martin.abente.lahaye@gmail.com> | 2010-12-01 13:27:44 (GMT) |
---|---|---|
committer | Anish Mangal <anish@activitycentral.com> | 2012-02-01 12:33:29 (GMT) |
commit | ff5ae4b970cc49140d53a9eda42879f45c147d55 (patch) | |
tree | 1deb7b48e9b6a6dddc6aab535c9664feb763f350 | |
parent | 77b53fc492f128d01424b0f8a39ef97c2a119b6b (diff) |
Simple messages notification extension
Extend jarabe.frame.notification with new graphical
elements in order to display message notifications.
These graphical elements were inspired from Gary
Martin mockups.
Messages notification are accessible through dbus
see http://library.gnome.org/devel/notification-spec/
or jarabe.frame.frame.add_message method.
This implementation only supports icons, summary
and markup body.
When a message is received:
1. A notification icon will appear and remain
as long as the time defined by the caller.
2. A new tray button will be added to the respective
tray, this button will remain present until the
user reads its content to delete it explicitly.
3. The button constains a message palette that will
behave as a messages queue.
Icons-only notications will be accesible and will behave
as before.
VERSION 2: The messages queue was moved from the corners
to the respective trays, in order to mantain
the corners available for other usage.
-rw-r--r-- | src/jarabe/frame/frame.py | 128 | ||||
-rw-r--r-- | src/jarabe/frame/notification.py | 205 | ||||
-rw-r--r-- | src/jarabe/view/pulsingicon.py | 24 |
3 files changed, 322 insertions, 35 deletions
diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py index 079eeeb..96e848b 100644 --- a/src/jarabe/frame/frame.py +++ b/src/jarabe/frame/frame.py @@ -19,6 +19,7 @@ import logging import gtk import gobject import hippo +import os from sugar.graphics import animator from sugar.graphics import style @@ -33,6 +34,7 @@ from jarabe.frame.devicestray import DevicesTray from jarabe.frame.framewindow import FrameWindow from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow from jarabe.frame.notification import NotificationIcon, NotificationWindow +from jarabe.frame.notification import NotificationButton, HistoryPalette from jarabe.model import notifications @@ -44,6 +46,8 @@ BOTTOM_LEFT = 3 _FRAME_HIDING_DELAY = 500 _NOTIFICATION_DURATION = 5000 +_DEFAULT_ICON = 'emblem-notification' + class _Animation(animator.Animation): def __init__(self, frame, end): @@ -119,6 +123,10 @@ class Frame(object): self._event_area.connect('enter', self._enter_corner_cb) self._event_area.show() + self._activities_tray = None + self._devices_tray = None + self._friends_tray = None + self._top_panel = self._create_top_panel() self._bottom_panel = self._create_bottom_panel() self._left_panel = self._create_left_panel() @@ -131,6 +139,7 @@ class Frame(object): self._mouse_listener = _MouseListener(self) self._notif_by_icon = {} + self._notif_by_message = {} notification_service = notifications.get_service() notification_service.notification_received.connect( @@ -190,6 +199,7 @@ class Frame(object): panel.append(hippo.CanvasWidget(widget=activities_tray), hippo.PACK_EXPAND) activities_tray.show() + self._activities_tray = activities_tray return panel @@ -201,6 +211,7 @@ class Frame(object): panel.append(hippo.CanvasWidget(widget=devices_tray), hippo.PACK_EXPAND) devices_tray.show() + self._devices_tray = devices_tray return panel @@ -210,6 +221,7 @@ class Frame(object): tray = FriendsTray() panel.append(hippo.CanvasWidget(widget=tray), hippo.PACK_EXPAND) tray.show() + self._friends_tray = tray return panel @@ -285,15 +297,7 @@ class Frame(object): def _enter_corner_cb(self, event_area): self._mouse_listener.mouse_enter() - def notify_key_press(self): - self._key_listener.key_press() - - def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT, - duration=_NOTIFICATION_DURATION): - - if not isinstance(icon, NotificationIcon): - raise TypeError('icon must be a NotificationIcon.') - + def _create_notification_window(self, corner): window = NotificationWindow() screen = gtk.gdk.screen_get_default() @@ -309,6 +313,46 @@ class Frame(object): else: raise ValueError('Inalid corner: %r' % corner) + return window + + def _add_message_button(self, button, corner): + if corner == gtk.CORNER_BOTTOM_RIGHT: + self._devices_tray.add_item(button) + elif corner == gtk.CORNER_TOP_RIGHT: + self._friends_tray.add_item(button) + else: + self._activities_tray.add_item(button) + + def _remove_message_button(self, button, corner): + if corner == gtk.CORNER_BOTTOM_RIGHT: + self._devices_tray.remove_item(button) + elif corner == gtk.CORNER_TOP_RIGHT: + self._friends_tray.remove_item(button) + else: + self._activities_tray.remove_item(button) + + def _launch_notification_icon(self, icon_name, xo_color, + position, duration): + icon = NotificationIcon() + icon.props.xo_color = xo_color + + if icon_name.startswith(os.sep): + icon.props.icon_filename = icon_name + else: + icon.props.icon_name = icon_name + + self.add_notification(icon, position, duration) + + def notify_key_press(self): + self._key_listener.key_press() + + def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT, + duration=_NOTIFICATION_DURATION): + + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + window = self._create_notification_window(corner) window.add(icon) icon.show() window.show() @@ -327,28 +371,76 @@ class Frame(object): window.destroy() del self._notif_by_icon[icon] + def add_message(self, body, summary='', icon_name=_DEFAULT_ICON, + xo_color=None, corner=gtk.CORNER_TOP_LEFT, + duration=_NOTIFICATION_DURATION): + + if xo_color is None: + xo_color = profile.get_color() + + button = self._notif_by_message.get(corner, None) + if button is None: + button = NotificationButton(xo_color) + button.show() + self._add_message_button(button, corner) + self._notif_by_message[corner] = button + + button.start_pulsing() + + palette = button.get_palette() + if palette is None: + palette = HistoryPalette() + palette.set_group_id('frame') + palette.connect('clear-messages', self.remove_message, corner) + button.set_palette(palette) + + palette.push_message(body, summary, icon_name, xo_color) + self._launch_notification_icon(icon_name, xo_color, corner, duration) + + + def remove_message(self, palette, corner): + if corner not in self._notif_by_message: + logging.debug('Button %s is not active', str(corner)) + return + + button = self._notif_by_message[corner] + self._remove_message_button(button, corner) + del self._notif_by_message[corner] + def __notification_received_cb(self, **kwargs): - logging.debug('__notification_received_cb') - icon = NotificationIcon() + logging.debug('__notification_received_cb %r', kwargs) hints = kwargs['hints'] - icon_file_name = hints.get('x-sugar-icon-file-name', '') - if icon_file_name: - icon.props.icon_filename = icon_file_name - else: - icon.props.icon_name = 'application-octet-stream' + icon_name = hints.get('x-sugar-icon-file-name', '') + if not icon_name: + icon_name = _DEFAULT_ICON icon_colors = hints.get('x-sugar-icon-colors', '') if not icon_colors: icon_colors = profile.get_color() - icon.props.xo_color = icon_colors duration = kwargs.get('expire_timeout', -1) if duration == -1: duration = _NOTIFICATION_DURATION - self.add_notification(icon, gtk.CORNER_TOP_RIGHT, duration) + category = hints.get('category', '') + if category == 'device': + position = gtk.CORNER_BOTTOM_RIGHT + elif category == 'presence': + position = gtk.CORNER_TOP_RIGHT + else: + position = gtk.CORNER_TOP_LEFT + + summary = kwargs.get('summary', '') + body = kwargs.get('body', '') + + if summary or body: + self.add_message(body, summary, icon_name, + icon_colors, position, duration) + else: + self._launch_notification_icon(icon_name, icon_colors, + position, duration) def __notification_cancelled_cb(self, **kwargs): # Do nothing for now. Our notification UI is so simple, there's no diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py index 3471e2c..b5724d5 100644 --- a/src/jarabe/frame/notification.py +++ b/src/jarabe/frame/notification.py @@ -1,4 +1,6 @@ # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Martin Abente +# Copyright (C) 2010 Aleksey Lim # # 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 @@ -16,13 +18,185 @@ import gobject import gtk +import re +import os + +from gettext import gettext as _ from sugar.graphics import style from sugar.graphics.xocolor import XoColor +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.toolbutton import ToolButton +from sugar import profile +from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.view.pulsingicon import PulsingIcon +_PULSE_TIMEOUT = 3 +_PULSE_COLOR = XoColor('%s,%s' % \ + (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) +_BODY_FILTERS = "<img.*?/>" +_NOTIFICATION_ICON = 'emblem-notification' + + +def _create_pulsing_icon(icon_name, xo_color): + icon = PulsingIcon( + pixel_size=style.STANDARD_ICON_SIZE, + pulse_color=_PULSE_COLOR, + base_color=xo_color, + timeout=_PULSE_TIMEOUT, + ) + + if icon_name.startswith(os.sep): + icon.props.file = icon_name + else: + icon.props.icon_name = icon_name + + return icon + + +class _HistoryIconWidget(gtk.Alignment): + __gtype_name__ = 'SugarHistoryIconWidget' + + def __init__(self, icon_name, xo_color): + icon = _create_pulsing_icon(icon_name, xo_color) + icon.props.pulsing = True + + gtk.Alignment.__init__(self, xalign=0.5, yalign=0.0) + self.props.top_padding = style.DEFAULT_PADDING + self.set_size_request( + style.GRID_CELL_SIZE - style.FOCUS_LINE_WIDTH * 2, + style.GRID_CELL_SIZE - style.DEFAULT_PADDING) + self.add(icon) + + +class _HistorySummaryWidget(gtk.Alignment): + __gtype_name__ = 'SugarHistorySummaryWidget' + + def __init__(self, summary): + summary_label = gtk.Label() + summary_label.props.wrap = True + summary_label.set_markup( + '<b>%s</b>' % gobject.markup_escape_text(summary)) + + gtk.Alignment.__init__(self, xalign=0.0, yalign=1.0) + self.props.right_padding = style.DEFAULT_SPACING + self.add(summary_label) + + +class _HistoryBodyWidget(gtk.Alignment): + __gtype_name__ = 'SugarHistoryBodyWidget' + + def __init__(self, body): + body_label = gtk.Label() + body_label.props.wrap = True + body_label.set_markup(body) + + gtk.Alignment.__init__(self, xalign=0, yalign=0.0) + self.props.right_padding = style.DEFAULT_SPACING + self.add(body_label) + + +class _MessagesHistoryBox(gtk.VBox): + __gtype_name__ = 'SugarMessagesHistoryBox' + + def __init__(self): + gtk.VBox.__init__(self) + self._setup_links_style() + + def _setup_links_style(self): + # XXX: find a better way to change style for upstream + link_color = profile.get_color().get_fill_color() + visited_link_color = profile.get_color().get_stroke_color() + + links_style=''' + style "label" { + GtkLabel::link-color="%s" + GtkLabel::visited-link-color="%s" + } + widget_class "*GtkLabel" style "label" + ''' % (link_color, visited_link_color) + gtk.rc_parse_string(links_style) + + def push_message(self, body, summary, icon_name, xo_color): + entry = gtk.HBox() + + icon_widget = _HistoryIconWidget(icon_name, xo_color) + entry.pack_start(icon_widget, False) + + message = gtk.VBox() + message.props.border_width = style.DEFAULT_PADDING + entry.pack_start(message) + + if summary: + summary_widget = _HistorySummaryWidget(summary) + message.pack_start(summary_widget, False) + + body = re.sub(_BODY_FILTERS, '', body) + + if body: + body_widget = _HistoryBodyWidget(body) + message.pack_start(body_widget) + + entry.show_all() + self.pack_start(entry) + self.reorder_child(entry, 0) + + self_width_, self_height = self.size_request() + if (self_height > gtk.gdk.screen_height() / 4 * 3) and \ + (len(self.get_children()) > 1): + self.remove(self.get_children()[-1]) + + +class HistoryPalette(Palette): + __gtype_name__ = 'SugarHistoryPalette' + + __gsignals__ = { + 'clear-messages': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self): + Palette.__init__(self) + + self.set_accept_focus(False) + + self._messages_box = _MessagesHistoryBox() + self._messages_box.show() + + palette_box = self.get_children()[0] + primary_box = palette_box.get_children()[0] + primary_box.hide() + palette_box.add(self._messages_box) + palette_box.reorder_child(self._messages_box, 0) + + clear_option = MenuItem(_('Clear history'), 'dialog-cancel') + clear_option.connect('activate', self.__clear_messages_cb) + clear_option.show() + + self.menu.append(clear_option) + + def __clear_messages_cb(self, clear_option): + self.emit('clear-messages') + + def push_message(self, body, summary, icon_name, xo_color): + self._messages_box.push_message(body, summary, icon_name, xo_color) + + +class NotificationButton(ToolButton): + + def __init__(self, xo_color): + ToolButton.__init__(self) + self._icon = _create_pulsing_icon(_NOTIFICATION_ICON, xo_color) + self.set_icon_widget(self._icon) + self._icon.show() + self.set_palette_invoker(FrameWidgetInvoker(self)) + + def start_pulsing(self): + self._icon.props.pulsing = True + + class NotificationIcon(gtk.EventBox): __gtype_name__ = 'SugarNotificationIcon' @@ -32,28 +206,29 @@ class NotificationIcon(gtk.EventBox): 'icon-filename': (str, None, None, None, gobject.PARAM_READWRITE), } - _PULSE_TIMEOUT = 3 - def __init__(self, **kwargs): self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE) gobject.GObject.__init__(self, **kwargs) self.props.visible_window = False + self.set_app_paintable(True) - self._icon.props.pulse_color = \ - XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), - style.COLOR_TRANSPARENT.get_svg())) - self._icon.props.pulsing = True + color = gtk.gdk.color_parse(style.COLOR_BLACK.get_html()) + self.modify_bg(gtk.STATE_PRELIGHT, color) + + color = gtk.gdk.color_parse(style.COLOR_BUTTON_GREY.get_html()) + self.modify_bg(gtk.STATE_ACTIVE, color) + + self._icon.props.pulse_color = _PULSE_COLOR + self._icon.props.timeout = _PULSE_TIMEOUT self.add(self._icon) self._icon.show() - gobject.timeout_add_seconds(self._PULSE_TIMEOUT, - self.__stop_pulsing_cb) + self.start_pulsing() 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 start_pulsing(self): + self._icon.props.pulsing = True def do_set_property(self, pspec, value): if pspec.name == 'xo-color': @@ -86,17 +261,13 @@ class NotificationIcon(gtk.EventBox): class NotificationWindow(gtk.Window): __gtype_name__ = 'SugarNotificationWindow' - def __init__(self, **kwargs): - - gtk.Window.__init__(self, **kwargs) + def __init__(self): + gtk.Window.__init__(self, gtk.WINDOW_POPUP) 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/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py index 9a98a80..0a97377 100644 --- a/src/jarabe/view/pulsingicon.py +++ b/src/jarabe/view/pulsingicon.py @@ -88,12 +88,23 @@ class PulsingIcon(Icon): self._pulse_color = None self._paused = False self._pulsing = False + self._timeout = 0 + self._pulsing_sid = None Icon.__init__(self, **kwargs) self._palette = None self.connect('destroy', self.__destroy_cb) + def set_timeout(self, timeout): + self._timeout = timeout + + def get_timeout(self): + return self._timeout + + timeout = gobject.property( + type=int, getter=get_timeout, setter=set_timeout) + def set_pulse_color(self, pulse_color): self._pulse_color = pulse_color self._pulser.update() @@ -140,10 +151,20 @@ class PulsingIcon(Icon): type=bool, default=False, getter=get_paused, setter=set_paused) def set_pulsing(self, pulsing): + if self._pulsing == pulsing: + return + + if self._pulsing_sid is not None: + gobject.source_remove(self._pulsing_sid) + self._pulsing_sid = None + self._pulsing = pulsing if self._pulsing: self._pulser.start(restart=True) + if self.props.timeout > 0: + self._pulsing_sid = gobject.timeout_add_seconds( + self.props.timeout, self.__timeout_cb) else: self._pulser.stop() @@ -163,6 +184,9 @@ class PulsingIcon(Icon): palette = property(_get_palette, _set_palette) + def __timeout_cb(self): + self.props.pulsing = False + def __destroy_cb(self, icon): self._pulser.stop() if self._palette is not None: |