[Dextrose] [PATCH] Simple messages notification extension

Aleksey Lim alsroot at member.fsf.org
Wed Dec 1 21:23:05 EST 2010


Reviewed-by: Aleksey Lim <alsroot at member.fsf.org>

On Wed, Dec 01, 2010 at 12:22:06PM -0300, Martin Abente wrote:
> 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 |  189 ++++++++++++++++++++++++++++++++++---
>  src/jarabe/view/pulsingicon.py   |   24 +++++
>  3 files changed, 291 insertions(+), 34 deletions(-)
> 
> diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py
> index 55f866f..c67a228 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,9 @@ BOTTOM_LEFT = 3
>  _FRAME_HIDING_DELAY = 500
>  _NOTIFICATION_DURATION = 5000
>  
> +_DEFAULT_ICON = 'emblem-notification'

> +_DEFAULT_COLOR = profile.get_color()
----------
>
It won't work if colors were changed on the fly.
See related changes below..

> +
>  class _Animation(animator.Animation):
>      def __init__(self, frame, end):
>          start = frame.current_position
> @@ -126,6 +132,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 +286,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 +302,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 +332,93 @@ class Frame(object):
>          window.destroy()
>          del self._notif_by_icon[icon]
>  
> +    def add_message(self, body, summary='', icon_name=_DEFAULT_ICON,

> +                    xo_color=_DEFAULT_COLOR, corner=gtk.CORNER_TOP_LEFT):
----------
> +                    xo_color=None, corner=gtk.CORNER_TOP_LEFT):
> +        if xo_color is None:
> +            xo_color = profile.get_color()

> +        icon = None
> +        window = self._notif_by_message.get(corner, None)
> +
> +        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

> +            icon_colors = _DEFAULT_COLOR
----------
> +            icon_colors = profile.get_color()

>  
>          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..29ebdb7 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,163 @@
>  
>  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.*/>"
----------
> +_BODY_FILTERS = "<img.*?/>"
quantificators are eager by default, so need to be restricted

> +
> +
> +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):
> +        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)
Sorry, I missed it last time.
It might be not so useful to tweak gtk style from the code. Better to have
it in sugar-artwork, you can ping Benjamin Berg (benzea on #sugar), he
is sugar-artwork maint.

> +
> +    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 +184,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':

-- 
Aleksey


More information about the Dextrose mailing list