[Dextrose] [PATCH] Simple messages notification extension

Martin Abente martin.abente.lahaye at gmail.com
Thu Dec 2 12:00:16 EST 2010


Extend jarabe.frame.notification with new graphical
elements in order to display message notifications.
These graphical elements were taken from Gary Martin
first 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 a notification icon will
appear and remain present until the user reads its
content to delete it explicitly. The message palette
will behave as a message queue.

Icons-only notications will be accesible and will behave
as before.
---
 src/jarabe/frame/frame.py        |  112 +++++++++++++++++++----
 src/jarabe/frame/notification.py |  190 ++++++++++++++++++++++++++++++++++---
 src/jarabe/view/pulsingicon.py   |   24 +++++
 3 files changed, 293 insertions(+), 33 deletions(-)

diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py
index 55f866f..4154e86 100644
--- a/src/jarabe/frame/frame.py
+++ b/src/jarabe/frame/frame.py
@@ -19,10 +19,12 @@ import logging
 import gtk
 import gobject
 import hippo
+import os
 
 from sugar.graphics import animator
 from sugar.graphics import style
 from sugar.graphics import palettegroup
+from sugar.graphics.palette import WidgetInvoker
 from sugar import profile
 
 from jarabe.frame.eventarea import EventArea
@@ -33,6 +35,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 HistoryPalette
 from jarabe.model import notifications
 
 TOP_RIGHT = 0
@@ -43,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):
         start = frame.current_position
@@ -126,6 +131,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(
@@ -279,15 +285,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()
@@ -303,6 +301,18 @@ class Frame(object):
         else:
             raise ValueError('Inalid corner: %r' % corner)
 
+        return window
+
+    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()
@@ -321,28 +331,96 @@ 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):
+        icon = None
+        window = self._notif_by_message.get(corner, None)
+
+        if xo_color is None:
+            xo_color = profile.get_color()
+
+        if window is None:
+            icon = NotificationIcon()
+            icon.show()
+
+            window = self._create_notification_window(corner)
+            window.add(icon)
+            window.show()
+
+            self._notif_by_message[corner] = window
+        else:
+            icon = window.get_children()[0]
+
+        if icon_name.startswith(os.sep):
+            icon.props.icon_filename = icon_name
+        else:
+            icon.props.icon_name = icon_name
+        icon.props.xo_color = xo_color
+        icon.start_pulsing()
+
+        palette = icon.palette
+
+        if palette is None:
+            palette = HistoryPalette()
+            palette.props.invoker = WidgetInvoker(icon)
+            palette.props.invoker.palette = palette
+            palette.set_group_id('frame')
+            palette.connect('clear-messages', self.remove_message, corner)
+            icon.palette = palette
+
+        palette.push_message(body, summary, icon_name, xo_color)
+
+    def remove_message(self, palette, corner):
+        if corner not in self._notif_by_message:
+            logging.debug('Corner %s is not active', str(corner))
+            return
+
+        window = self._notif_by_message[corner]
+        window.destroy()
+        del self._notif_by_message[corner]
+
     def __notification_received_cb(self, **kwargs):
         logging.debug('__notification_received_cb %r', kwargs)
-        icon = NotificationIcon()
 
         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 = None
+        icon_filename = hints.get('x-sugar-icon-file-name', '')
+        if not icon_filename:
+            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:
+            if icon_name is None:
+                icon_name = icon_filename
+
+            self.add_message(body, summary, icon_name,
+                            icon_colors, position)
+        else:
+            icon = NotificationIcon()
+            icon.props.icon_filename = icon_filename
+            icon.props.icon_name = icon_name
+            icon.props.xo_color = icon_colors
+
+            self.add_notification(icon, 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 83dc27e..25ff80a 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,12 +18,164 @@
 
 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 import profile
 
 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.*?/>"
+
+
+class _HistoryIconWidget(gtk.Alignment):
+    __gtype_name__ = 'SugarHistoryIconWidget'
+
+    def __init__(self, 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
+
+        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 NotificationIcon(gtk.EventBox):
     __gtype_name__ = 'SugarNotificationIcon'
 
@@ -31,27 +185,35 @@ 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_expose_event(self, event):
+        if self.palette is not None and self.palette.is_up():
+            invoker = self.palette.props.invoker
+            invoker.draw_rectangle(event, self.palette)
+        gtk.EventBox.do_expose_event(self, event)
 
     def do_set_property(self, pspec, value):
         if pspec.name == 'xo-color':
@@ -83,18 +245,14 @@ 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 43ec358..27fa53c 100644
--- a/src/jarabe/view/pulsingicon.py
+++ b/src/jarabe/view/pulsingicon.py
@@ -92,12 +92,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()
@@ -133,10 +144,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()
 
@@ -156,6 +177,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:
-- 
1.7.1



More information about the Dextrose mailing list