[Dextrose] [PATCH] Simple messages notification extension

Martin Abente martin.abente.lahaye at gmail.com
Thu Dec 2 07:51:29 EST 2010


On Wed, Dec 1, 2010 at 11:23 PM, Aleksey Lim <alsroot at member.fsf.org> wrote:

> 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..
>
>
But in 0.88.1 users can't change the colors on the fly, right? The option in
the CP requires restart.


> > +
> >  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
>
>
Sure, I will change that.


> > +
> > +
> > +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.
>
>
Can you configure dynamic styles that way? What i do there is to have the
link and visited link
colours according to users colours.


> > +
> > +    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
>


Thanks for the review :)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.sugarlabs.org/archive/dextrose/attachments/20101202/dc38ccf2/attachment-0001.html>


More information about the Dextrose mailing list