[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