[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