<br><br><div class="gmail_quote">On Wed, Dec 1, 2010 at 11:23 PM, Aleksey Lim <span dir="ltr"><<a href="mailto:alsroot@member.fsf.org">alsroot@member.fsf.org</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
Reviewed-by: Aleksey Lim <<a href="mailto:alsroot@member.fsf.org">alsroot@member.fsf.org</a>><br>
<div><div></div><div class="h5"><br>
On Wed, Dec 01, 2010 at 12:22:06PM -0300, Martin Abente wrote:<br>
> Extend jarabe.frame.notification with new graphical<br>
> elements in order to display message notifications.<br>
> These graphical elements were taken from Gary Martin<br>
> first mockups.<br>
><br>
> Messages notification are accessible through dbus<br>
> see <a href="http://library.gnome.org/devel/notification-spec/" target="_blank">http://library.gnome.org/devel/notification-spec/</a><br>
> or jarabe.frame.frame.add_message method.<br>
> This implementation only supports icons, summary<br>
> and markup body.<br>
><br>
> When a message is received a notification icon will<br>
> appear and remain present until the user reads its<br>
> content to delete it explicitly. The message palette<br>
> will behave as a message queue.<br>
><br>
> Icons-only notications will be accesible and will behave<br>
> as before.<br>
> ---<br>
>  src/jarabe/frame/frame.py        |  112 +++++++++++++++++++----<br>
>  src/jarabe/frame/notification.py |  189 ++++++++++++++++++++++++++++++++++---<br>
>  src/jarabe/view/pulsingicon.py   |   24 +++++<br>
>  3 files changed, 291 insertions(+), 34 deletions(-)<br>
><br>
> diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py<br>
> index 55f866f..c67a228 100644<br>
> --- a/src/jarabe/frame/frame.py<br>
> +++ b/src/jarabe/frame/frame.py<br>
> @@ -19,10 +19,12 @@ import logging<br>
>  import gtk<br>
>  import gobject<br>
>  import hippo<br>
> +import os<br>
><br>
>  from sugar.graphics import animator<br>
>  from sugar.graphics import style<br>
>  from sugar.graphics import palettegroup<br>
> +from sugar.graphics.palette import WidgetInvoker<br>
>  from sugar import profile<br>
><br>
>  from jarabe.frame.eventarea import EventArea<br>
> @@ -33,6 +35,7 @@ from jarabe.frame.devicestray import DevicesTray<br>
>  from jarabe.frame.framewindow import FrameWindow<br>
>  from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow<br>
>  from jarabe.frame.notification import NotificationIcon, NotificationWindow<br>
> +from jarabe.frame.notification import HistoryPalette<br>
>  from jarabe.model import notifications<br>
><br>
>  TOP_RIGHT = 0<br>
> @@ -43,6 +46,9 @@ BOTTOM_LEFT = 3<br>
>  _FRAME_HIDING_DELAY = 500<br>
>  _NOTIFICATION_DURATION = 5000<br>
><br>
> +_DEFAULT_ICON = 'emblem-notification'<br>
<br>
> +_DEFAULT_COLOR = profile.get_color()<br>
</div></div>----------<br>
><br>
It won't work if colors were changed on the fly.<br>
See related changes below..<br>
<div><div></div><div class="h5"><br></div></div></blockquote><div><br>But in 0.88.1 users can't change the colors on the fly, right? The option in the CP requires restart.<br> </div><blockquote class="gmail_quote" style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
<div><div class="h5">
> +<br>
>  class _Animation(animator.Animation):<br>
>      def __init__(self, frame, end):<br>
>          start = frame.current_position<br>
> @@ -126,6 +132,7 @@ class Frame(object):<br>
>          self._mouse_listener = _MouseListener(self)<br>
><br>
>          self._notif_by_icon = {}<br>
> +        self._notif_by_message = {}<br>
><br>
>          notification_service = notifications.get_service()<br>
>          notification_service.notification_received.connect(<br>
> @@ -279,15 +286,7 @@ class Frame(object):<br>
>      def _enter_corner_cb(self, event_area):<br>
>          self._mouse_listener.mouse_enter()<br>
><br>
> -    def notify_key_press(self):<br>
> -        self._key_listener.key_press()<br>
> -<br>
> -    def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT,<br>
> -                         duration=_NOTIFICATION_DURATION):<br>
> -<br>
> -        if not isinstance(icon, NotificationIcon):<br>
> -            raise TypeError('icon must be a NotificationIcon.')<br>
> -<br>
> +    def _create_notification_window(self, corner):<br>
>          window = NotificationWindow()<br>
><br>
>          screen = gtk.gdk.screen_get_default()<br>
> @@ -303,6 +302,18 @@ class Frame(object):<br>
>          else:<br>
>              raise ValueError('Inalid corner: %r' % corner)<br>
><br>
> +        return window<br>
> +<br>
> +    def notify_key_press(self):<br>
> +        self._key_listener.key_press()<br>
> +<br>
> +    def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT,<br>
> +                         duration=_NOTIFICATION_DURATION):<br>
> +<br>
> +        if not isinstance(icon, NotificationIcon):<br>
> +            raise TypeError('icon must be a NotificationIcon.')<br>
> +<br>
> +        window = self._create_notification_window(corner)<br>
>          window.add(icon)<br>
>          icon.show()<br>
>          window.show()<br>
> @@ -321,28 +332,93 @@ class Frame(object):<br>
>          window.destroy()<br>
>          del self._notif_by_icon[icon]<br>
><br>
> +    def add_message(self, body, summary='', icon_name=_DEFAULT_ICON,<br>
<br>
> +                    xo_color=_DEFAULT_COLOR, corner=gtk.CORNER_TOP_LEFT):<br>
</div></div>----------<br>
> +                    xo_color=None, corner=gtk.CORNER_TOP_LEFT):<br>
> +        if xo_color is None:<br>
> +            xo_color = profile.get_color()<br>
<div><div></div><div class="h5"><br>
> +        icon = None<br>
> +        window = self._notif_by_message.get(corner, None)<br>
> +<br>
> +        if window is None:<br>
> +            icon = NotificationIcon()<br>
> +            icon.show()<br>
> +<br>
> +            window = self._create_notification_window(corner)<br>
> +            window.add(icon)<br>
> +            window.show()<br>
> +<br>
> +            self._notif_by_message[corner] = window<br>
> +        else:<br>
> +            icon = window.get_children()[0]<br>
> +<br>
> +        if icon_name.startswith(os.sep):<br>
> +            icon.props.icon_filename = icon_name<br>
> +        else:<br>
> +            icon.props.icon_name = icon_name<br>
> +        icon.props.xo_color = xo_color<br>
> +        icon.start_pulsing()<br>
> +<br>
> +        palette = icon.palette<br>
> +<br>
> +        if palette is None:<br>
> +            palette = HistoryPalette()<br>
> +            palette.props.invoker = WidgetInvoker(icon)<br>
> +            palette.props.invoker.palette = palette<br>
> +            palette.set_group_id('frame')<br>
> +            palette.connect('clear-messages', self.remove_message, corner)<br>
> +            icon.palette = palette<br>
> +<br>
> +        palette.push_message(body, summary, icon_name, xo_color)<br>
> +<br>
> +    def remove_message(self, palette, corner):<br>
> +        if corner not in self._notif_by_message:<br>
> +            logging.debug('Corner %s is not active', str(corner))<br>
> +            return<br>
> +<br>
> +        window = self._notif_by_message[corner]<br>
> +        window.destroy()<br>
> +        del self._notif_by_message[corner]<br>
> +<br>
>      def __notification_received_cb(self, **kwargs):<br>
>          logging.debug('__notification_received_cb %r', kwargs)<br>
> -        icon = NotificationIcon()<br>
><br>
>          hints = kwargs['hints']<br>
><br>
> -        icon_file_name = hints.get('x-sugar-icon-file-name', '')<br>
> -        if icon_file_name:<br>
> -            icon.props.icon_filename = icon_file_name<br>
> -        else:<br>
> -            icon.props.icon_name = 'application-octet-stream'<br>
> +        icon_name = None<br>
> +        icon_filename = hints.get('x-sugar-icon-file-name', '')<br>
> +        if not icon_filename:<br>
> +            icon_name = _DEFAULT_ICON<br>
><br>
>          icon_colors = hints.get('x-sugar-icon-colors', '')<br>
>          if not icon_colors:<br>
> -            icon_colors = profile.get_color()<br>
> -        icon.props.xo_color = icon_colors<br>
<br>
> +            icon_colors = _DEFAULT_COLOR<br>
</div></div>----------<br>
> +            icon_colors = profile.get_color()<br>
<div><div></div><div class="h5"><br>
><br>
>          duration = kwargs.get('expire_timeout', -1)<br>
>          if duration == -1:<br>
>              duration = _NOTIFICATION_DURATION<br>
><br>
> -        self.add_notification(icon, gtk.CORNER_TOP_RIGHT, duration)<br>
> +        category = hints.get('category', '')<br>
> +        if category == 'device':<br>
> +            position = gtk.CORNER_BOTTOM_RIGHT<br>
> +        elif category == 'presence':<br>
> +            position = gtk.CORNER_TOP_RIGHT<br>
> +        else:<br>
> +            position = gtk.CORNER_TOP_LEFT<br>
> +<br>
> +        summary = kwargs.get('summary', '')<br>
> +        body = kwargs.get('body', '')<br>
> +<br>
> +        if summary or body:<br>
> +            if icon_name is None:<br>
> +                icon_name = icon_filename<br>
> +<br>
> +            self.add_message(body, summary, icon_name,<br>
> +                            icon_colors, position)<br>
> +        else:<br>
> +            icon = NotificationIcon()<br>
> +            icon.props.icon_filename = icon_filename<br>
> +            icon.props.icon_name = icon_name<br>
> +            icon.props.xo_color = icon_colors<br>
> +<br>
> +            self.add_notification(icon, position, duration)<br>
><br>
>      def __notification_cancelled_cb(self, **kwargs):<br>
>          # Do nothing for now. Our notification UI is so simple, there's no<br>
> diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py<br>
> index 83dc27e..29ebdb7 100644<br>
> --- a/src/jarabe/frame/notification.py<br>
> +++ b/src/jarabe/frame/notification.py<br>
> @@ -1,4 +1,6 @@<br>
>  # Copyright (C) 2008 One Laptop Per Child<br>
> +# Copyright (C) 2010 Martin Abente<br>
> +# Copyright (C) 2010 Aleksey Lim<br>
>  #<br>
>  # This program is free software; you can redistribute it and/or modify<br>
>  # it under the terms of the GNU General Public License as published by<br>
> @@ -16,12 +18,163 @@<br>
><br>
>  import gobject<br>
>  import gtk<br>
> +import re<br>
> +import os<br>
> +<br>
> +from gettext import gettext as _<br>
><br>
>  from sugar.graphics import style<br>
>  from sugar.graphics.xocolor import XoColor<br>
> +from sugar.graphics.palette import Palette<br>
> +from sugar.graphics.menuitem import MenuItem<br>
> +from sugar import profile<br>
><br>
>  from jarabe.view.pulsingicon import PulsingIcon<br>
><br>
> +<br>
> +_PULSE_TIMEOUT = 3<br>
> +_PULSE_COLOR = XoColor('%s,%s' % \<br>
> +        (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg()))<br>
<br>
> +_BODY_FILTERS = "<img.*/>"<br>
</div></div>----------<br>
> +_BODY_FILTERS = "<img.*?/>"<br>
quantificators are eager by default, so need to be restricted<br>
<br></blockquote><div><br>Sure, I will change that.<br> </div><blockquote class="gmail_quote" style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;"><div><div class="h5">> +<br>

> +<br>
> +class _HistoryIconWidget(gtk.Alignment):<br>
> +    __gtype_name__ = 'SugarHistoryIconWidget'<br>
> +<br>
> +    def __init__(self, icon_name, xo_color):<br>
> +        icon = PulsingIcon(<br>
> +                pixel_size=style.STANDARD_ICON_SIZE,<br>
> +                pulse_color=_PULSE_COLOR,<br>
> +                base_color=xo_color,<br>
> +                timeout=_PULSE_TIMEOUT,<br>
> +                )<br>
> +<br>
> +        if icon_name.startswith(os.sep):<br>
> +            icon.props.file = icon_name<br>
> +        else:<br>
> +            icon.props.icon_name = icon_name<br>
> +<br>
> +        icon.props.pulsing = True<br>
> +<br>
> +        gtk.Alignment.__init__(self, xalign=0.5, yalign=0.0)<br>
> +        self.props.top_padding = style.DEFAULT_PADDING<br>
> +        self.set_size_request(<br>
> +                style.GRID_CELL_SIZE - style.FOCUS_LINE_WIDTH * 2,<br>
> +                style.GRID_CELL_SIZE - style.DEFAULT_PADDING)<br>
> +        self.add(icon)<br>
> +<br>
> +<br>
> +class _HistorySummaryWidget(gtk.Alignment):<br>
> +    __gtype_name__ = 'SugarHistorySummaryWidget'<br>
> +<br>
> +    def __init__(self, summary):<br>
> +        summary_label = gtk.Label()<br>
> +        summary_label.props.wrap = True<br>
> +        summary_label.set_markup(<br>
> +                '<b>%s</b>' % gobject.markup_escape_text(summary))<br>
> +<br>
> +        gtk.Alignment.__init__(self, xalign=0.0, yalign=1.0)<br>
> +        self.props.right_padding = style.DEFAULT_SPACING<br>
> +        self.add(summary_label)<br>
> +<br>
> +<br>
> +class _HistoryBodyWidget(gtk.Alignment):<br>
> +    __gtype_name__ = 'SugarHistoryBodyWidget'<br>
> +<br>
> +    def __init__(self, body):<br>
> +        body_label = gtk.Label()<br>
> +        body_label.props.wrap = True<br>
> +        body_label.set_markup(body)<br>
> +<br>
> +        gtk.Alignment.__init__(self, xalign=0, yalign=0.0)<br>
> +        self.props.right_padding = style.DEFAULT_SPACING<br>
> +        self.add(body_label)<br>
> +<br>
> +<br>
> +class _MessagesHistoryBox(gtk.VBox):<br>
> +    __gtype_name__ = 'SugarMessagesHistoryBox'<br>
> +<br>
> +    def __init__(self):<br>
> +        gtk.VBox.__init__(self)<br>
> +        self._setup_links_style()<br>
> +<br>
> +    def _setup_links_style(self):<br>
> +        link_color = profile.get_color().get_fill_color()<br>
> +        visited_link_color = profile.get_color().get_stroke_color()<br>
> +<br>
<br>
> +        links_style='''<br>
> +        style "label" {<br>
> +          GtkLabel::link-color="%s"<br>
> +          GtkLabel::visited-link-color="%s"<br>
> +        }<br>
> +        widget_class "*GtkLabel" style "label"<br>
> +        ''' % (link_color, visited_link_color)<br>
> +        gtk.rc_parse_string(links_style)<br>
</div></div>Sorry, I missed it last time.<br>
It might be not so useful to tweak gtk style from the code. Better to have<br>
it in sugar-artwork, you can ping Benjamin Berg (benzea on #sugar), he<br>
is sugar-artwork maint.<br>
<div><div></div><div class="h5"><br></div></div></blockquote><div><br>Can you configure dynamic styles that way? What i do there is to have the link and visited link<br>colours according to users colours.<br> </div><blockquote class="gmail_quote" style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
<div><div>
> +<br>
> +    def push_message(self, body, summary, icon_name, xo_color):<br>
> +        entry = gtk.HBox()<br>
> +<br>
> +        icon_widget = _HistoryIconWidget(icon_name, xo_color)<br>
> +        entry.pack_start(icon_widget, False)<br>
> +<br>
> +        message = gtk.VBox()<br>
> +        message.props.border_width = style.DEFAULT_PADDING<br>
> +        entry.pack_start(message)<br>
> +<br>
> +        if summary:<br>
> +            summary_widget = _HistorySummaryWidget(summary)<br>
> +            message.pack_start(summary_widget, False)<br>
> +<br>
> +        body = re.sub(_BODY_FILTERS, '', body)<br>
> +<br>
> +        if body:<br>
> +            body_widget = _HistoryBodyWidget(body)<br>
> +            message.pack_start(body_widget)<br>
> +<br>
> +        entry.show_all()<br>
> +        self.pack_start(entry)<br>
> +        self.reorder_child(entry, 0)<br>
> +<br>
> +        self_width_, self_height = self.size_request()<br>
> +        if (self_height > gtk.gdk.screen_height() / 4 * 3) and \<br>
> +                (len(self.get_children()) > 1):<br>
> +            self.remove(self.get_children()[-1])<br>
> +<br>
> +<br>
> +class HistoryPalette(Palette):<br>
> +    __gtype_name__ = 'SugarHistoryPalette'<br>
> +<br>
> +    __gsignals__ = {<br>
> +        'clear-messages': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([]))<br>
> +    }<br>
> +<br>
> +    def __init__(self):<br>
> +        Palette.__init__(self)<br>
> +<br>
> +        self.set_accept_focus(False)<br>
> +<br>
> +        self._messages_box = _MessagesHistoryBox()<br>
> +        self._messages_box.show()<br>
> +<br>
> +        palette_box = self.get_children()[0]<br>
> +        primary_box = palette_box.get_children()[0]<br>
> +        primary_box.hide()<br>
> +        palette_box.add(self._messages_box)<br>
> +        palette_box.reorder_child(self._messages_box, 0)<br>
> +<br>
> +        clear_option = MenuItem(_('Clear history'), 'dialog-cancel')<br>
> +        clear_option.connect('activate', self.__clear_messages_cb)<br>
> +        clear_option.show()<br>
> +<br>
> +        self.menu.append(clear_option)<br>
> +<br>
> +    def __clear_messages_cb(self, clear_option):<br>
> +        self.emit('clear-messages')<br>
> +<br>
> +    def push_message(self, body, summary, icon_name, xo_color):<br>
> +        self._messages_box.push_message(body, summary, icon_name, xo_color)<br>
> +<br>
> +<br>
>  class NotificationIcon(gtk.EventBox):<br>
>      __gtype_name__ = 'SugarNotificationIcon'<br>
><br>
> @@ -31,27 +184,35 @@ class NotificationIcon(gtk.EventBox):<br>
>          'icon-filename' : (str, None, None, None, gobject.PARAM_READWRITE)<br>
>      }<br>
><br>
> -    _PULSE_TIMEOUT = 3<br>
> -<br>
>      def __init__(self, **kwargs):<br>
>          self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE)<br>
>          gobject.GObject.__init__(self, **kwargs)<br>
>          self.props.visible_window = False<br>
> +        self.set_app_paintable(True)<br>
><br>
> -        self._icon.props.pulse_color = \<br>
> -                XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),<br>
> -                                   style.COLOR_TRANSPARENT.get_svg()))<br>
> -        self._icon.props.pulsing = True<br>
> +        color = gtk.gdk.color_parse(style.COLOR_BLACK.get_html())<br>
> +        self.modify_bg(gtk.STATE_PRELIGHT, color)<br>
> +<br>
> +        color = gtk.gdk.color_parse(style.COLOR_BUTTON_GREY.get_html())<br>
> +        self.modify_bg(gtk.STATE_ACTIVE, color)<br>
> +<br>
> +        self._icon.props.pulse_color = _PULSE_COLOR<br>
> +        self._icon.props.timeout = _PULSE_TIMEOUT<br>
>          self.add(self._icon)<br>
>          self._icon.show()<br>
><br>
> -        gobject.timeout_add_seconds(self._PULSE_TIMEOUT, self.__stop_pulsing_cb)<br>
> +        self.start_pulsing()<br>
><br>
>          self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)<br>
><br>
> -    def __stop_pulsing_cb(self):<br>
> -        self._icon.props.pulsing = False<br>
> -        return False<br>
> +    def start_pulsing(self):<br>
> +        self._icon.props.pulsing = True<br>
> +<br>
> +    def do_expose_event(self, event):<br>
> +        if self.palette is not None and self.palette.is_up():<br>
> +            invoker = self.palette.props.invoker<br>
> +            invoker.draw_rectangle(event, self.palette)<br>
> +        gtk.EventBox.do_expose_event(self, event)<br>
><br>
>      def do_set_property(self, pspec, value):<br>
>          if <a href="http://pspec.name" target="_blank">pspec.name</a> == 'xo-color':<br><br><br></div></div></blockquote><blockquote class="gmail_quote" style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
<div><div class="h5">
</div></div>--<br>
<font color="#888888">Aleksey<br>
</font></blockquote></div><br><br>Thanks for the review :)<br><br>