From ed447a5718e706750ca64d8b84abd2d643efda8d Mon Sep 17 00:00:00 2001 From: Ajay Garg Date: Tue, 16 Oct 2012 17:47:15 +0000 Subject: Notifications (gconf-controllable) Signed-off-by: Ajay Garg --- diff --git a/bin/sugar-session b/bin/sugar-session index 7455f38..4f69f18 100755 --- a/bin/sugar-session +++ b/bin/sugar-session @@ -22,6 +22,7 @@ import time import subprocess import shutil + # Change the default encoding to avoid UnicodeDecodeError # http://lists.sugarlabs.org/archive/sugar-devel/2012-August/038928.html reload(sys) @@ -43,6 +44,10 @@ from gi.repository import GObject from gi.repository import Gst import dbus.glib from gi.repository import Wnck +from gi.repository import Gio + +MONITORS = [] +MONITOR_ACTION_TAKEN = False _USE_XKL = False try: @@ -138,6 +143,15 @@ def setup_notification_service_cb(): from jarabe.model import notifications notifications.init() +def show_notifications_cb(): + client = GConf.Client.get_default() + if not client.get_bool('/desktop/sugar/frame/show_notifications'): + return + + from ceibal.notifier import Notifier + n = Notifier() + n.show_messages_from_shell() + def setup_file_transfer_cb(): from jarabe.model import filetransfer filetransfer.init() @@ -213,14 +227,31 @@ def setup_window_manager(): shell=True): logging.warning('Can not disable metacity mouse button modifiers') +def file_monitor_changed_cb(monitor, one_file, other_file, event_type): + global MONITOR_ACTION_TAKEN + if (not MONITOR_ACTION_TAKEN) and \ + (one_file.get_path() == os.path.expanduser('~/.sugar/journal_created')): + if event_type == Gio.FileMonitorEvent.CREATED: + GObject.idle_add(show_notifications_cb) + GObject.idle_add(setup_frame_cb) + MONITOR_ACTION_TAKEN = True + +def arrange_for_setup_frame_cb(): + path = Gio.File.new_for_path(os.path.expanduser('~/.sugar/journal_created')) + monitor = path.monitor_file(Gio.FileMonitorFlags.NONE, None) + monitor.connect('changed', file_monitor_changed_cb) + MONITORS.append(monitor) + def bootstrap(): setup_window_manager() from jarabe.view import launcher launcher.setup() - GObject.idle_add(setup_frame_cb) GObject.idle_add(setup_keyhandler_cb) + + arrange_for_setup_frame_cb() + GObject.idle_add(setup_gesturehandler_cb) GObject.idle_add(setup_journal_cb) GObject.idle_add(setup_notification_service_cb) diff --git a/src/jarabe/frame/__init__.py b/src/jarabe/frame/__init__.py index b3e4b80..8732b96 100644 --- a/src/jarabe/frame/__init__.py +++ b/src/jarabe/frame/__init__.py @@ -14,13 +14,13 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from jarabe.frame.frame import Frame - _view = None def get_view(): + from jarabe.frame.frame import Frame + global _view if not _view: _view = Frame() diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py index 38fde7b..ef8435f 100644 --- a/src/jarabe/frame/activitiestray.py +++ b/src/jarabe/frame/activitiestray.py @@ -246,6 +246,24 @@ class ActivitiesTray(HTray): button.connect('clicked', self.__activity_clicked_cb, home_activity) button.show() + # JournalActivity is always the first activity to be added. + # Broadcast the signal-of-its-creation. + if group is None: + self._signal_addition_of_journal_activity() + + def _signal_addition_of_journal_activity(self): + monitor_file = os.path.expanduser('~/.sugar/journal_created') + + # Remove the file, if it exists. + # This is important, since we are checking for the + # FILE_CREATED event in the monitor. + if os.path.exists(monitor_file): + os.remove(monitor_file) + + # Now, create the file. + f = open(monitor_file, 'w') + f.close() + def __activity_removed_cb(self, home_model, home_activity): logging.debug('__activity_removed_cb: %r', home_activity) button = self._buttons[home_activity] diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py index 410e08b..659df19 100644 --- a/src/jarabe/frame/frame.py +++ b/src/jarabe/frame/frame.py @@ -15,6 +15,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging +import os from gi.repository import Gtk from gi.repository import Gdk @@ -33,6 +34,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 NotificationButton, HistoryPalette from jarabe.model import notifications @@ -43,6 +45,8 @@ BOTTOM_LEFT = 3 _NOTIFICATION_DURATION = 5000 +_DEFAULT_ICON = 'emblem-notification' + class _Animation(animator.Animation): def __init__(self, frame, end): @@ -83,6 +87,10 @@ class Frame(object): self._event_area.connect('enter', self._enter_corner_cb) self._event_area.show() + self._activities_tray = None + self._devices_tray = None + self._friends_tray = None + self._top_panel = self._create_top_panel() self._bottom_panel = self._create_bottom_panel() self._left_panel = self._create_left_panel() @@ -94,6 +102,7 @@ class Frame(object): self._key_listener = _KeyListener(self) self._notif_by_icon = {} + self._notif_by_message = {} notification_service = notifications.get_service() notification_service.notification_received.connect( @@ -143,6 +152,8 @@ class Frame(object): panel.append(activities_tray) activities_tray.show() + self._activities_tray = activities_tray + return panel def _create_bottom_panel(self): @@ -152,6 +163,8 @@ class Frame(object): panel.append(devices_tray) devices_tray.show() + self._devices_tray = devices_tray + return panel def _create_right_panel(self): @@ -161,6 +174,8 @@ class Frame(object): panel.append(tray) tray.show() + self._friends_tray = tray + return panel def _create_left_panel(self): @@ -211,15 +226,7 @@ class Frame(object): else: self.show() - def notify_key_press(self): - self._key_listener.key_press() - - def add_notification(self, icon, corner=Gtk.CornerType.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 = Gdk.Screen.get_default() @@ -235,6 +242,47 @@ class Frame(object): else: raise ValueError('Inalid corner: %r' % corner) + return window + + def _add_message_button(self, button, corner): + if corner == Gtk.CornerType.BOTTOM_RIGHT: + self._devices_tray.add_item(button) + elif corner == Gtk.CornerType.TOP_RIGHT: + self._friends_tray.add_item(button) + else: + self._activities_tray.add_item(button) + + def _remove_message_button(self, button, corner): + if corner == Gtk.CornerType.BOTTOM_RIGHT: + self._devices_tray.remove_item(button) + elif corner == Gtk.CornerType.TOP_RIGHT: + self._friends_tray.remove_item(button) + else: + self._activities_tray.remove_item(button) + + def _launch_notification_icon(self, icon_name, xo_color, + position, duration): + icon = NotificationIcon() + icon.props.xo_color = xo_color + + if icon_name.startswith(os.sep): + icon.props.icon_filename = icon_name + else: + icon.props.icon_name = icon_name + + self.add_notification(icon, position, duration) + + def notify_key_press(self): + self._key_listener.key_press() + + def add_notification(self, icon, corner=Gtk.CornerType.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() @@ -253,28 +301,77 @@ class Frame(object): window.destroy() del self._notif_by_icon[icon] + def add_message(self, body, summary='', icon_name=_DEFAULT_ICON, + xo_color=None, corner=Gtk.CornerType.TOP_LEFT, + duration=_NOTIFICATION_DURATION): + + if xo_color is None: + xo_color = profile.get_color() + + button = self._notif_by_message.get(corner, None) + if button is None: + button = NotificationButton(_DEFAULT_ICON, xo_color) + button.show() + self._add_message_button(button, corner) + self._notif_by_message[corner] = button + + palette = button.get_palette() + if palette is None: + palette = HistoryPalette() + palette.set_group_id('frame') + palette.connect('clear-messages', self.remove_message, corner) + palette.connect('notice-messages', button.stop_pulsing) + button.set_palette(palette) + + button.start_pulsing() + + palette.push_message(body, summary, icon_name, xo_color) + if not self.visible: + self._launch_notification_icon(_DEFAULT_ICON, xo_color, corner, duration) + + def remove_message(self, palette, corner): + if corner not in self._notif_by_message: + logging.debug('Button %s is not active', str(corner)) + return + + button = self._notif_by_message[corner] + self._remove_message_button(button, corner) + del self._notif_by_message[corner] + def __notification_received_cb(self, **kwargs): - logging.debug('__notification_received_cb') - icon = NotificationIcon() + logging.debug('__notification_received_cb %r', kwargs) 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 = hints.get('x-sugar-icon-file-name', '') + if not icon_name: + 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 duration = kwargs.get('expire_timeout', -1) if duration == -1: duration = _NOTIFICATION_DURATION - self.add_notification(icon, Gtk.CornerType.TOP_RIGHT, duration) + category = hints.get('category', '') + if category == 'device': + position = Gtk.CornerType.BOTTOM_RIGHT + elif category == 'presence': + position = Gtk.CornerType.TOP_RIGHT + else: + position = Gtk.CornerType.TOP_LEFT + + summary = kwargs.get('summary', '') + body = kwargs.get('body', '') + + if summary or body: + self.add_message(body, summary, icon_name, + icon_colors, position, duration) + else: + self._launch_notification_icon(icon_name, icon_colors, + 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 184a779..3adaed1 100644 --- a/src/jarabe/frame/notification.py +++ b/src/jarabe/frame/notification.py @@ -18,11 +18,197 @@ from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk +import re +import os + +from gettext import gettext as _ + from sugar3.graphics import style from sugar3.graphics.xocolor import XoColor +from sugar3.graphics.palette import Palette +from sugar3.graphics.palettemenuitem import PaletteMenuItem +from sugar3.graphics.toolbutton import ToolButton +from sugar3 import profile + +from jarabe.frame.frameinvoker import FrameWidgetInvoker 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 = "" + + +def _create_pulsing_icon(icon_name, xo_color, timeout=None): + icon = PulsingIcon( + pixel_size=style.STANDARD_ICON_SIZE, + pulse_color=_PULSE_COLOR, + base_color=xo_color + ) + + if timeout is not None: + icon.timeout = timeout + + if icon_name.startswith(os.sep): + icon.props.file = icon_name + else: + icon.props.icon_name = icon_name + + return icon + + +class _HistoryIconWidget(Gtk.Alignment): + __gtype_name__ = 'SugarHistoryIconWidget' + + def __init__(self, icon_name, xo_color): + icon = _create_pulsing_icon(icon_name, xo_color, _PULSE_TIMEOUT) + 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( + '%s' % 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): + # XXX: find a better way to change style for upstream + 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) + + 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, False, 0) + + message = Gtk.VBox() + message.props.border_width = style.DEFAULT_PADDING + entry.pack_start(message, True, True, 0) + + if summary: + summary_widget = _HistorySummaryWidget(summary) + message.pack_start(summary_widget, False, False, 0) + + body = re.sub(_BODY_FILTERS, '', body) + + if body: + body_widget = _HistoryBodyWidget(body) + message.pack_start(body_widget, True, True, 0) + + entry.show_all() + self.pack_start(entry, True, True, 0) + self.reorder_child(entry, 0) + + self_width_ = self.props.width_request + self_height = self.props.height_request + if (self_height > 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.SignalFlags.RUN_FIRST, None, ([])), + 'notice-messages': (GObject.SignalFlags.RUN_FIRST, None, ([])) + } + + def __init__(self): + Palette.__init__(self) + + self._update_accept_focus() + + self._messages_box = _MessagesHistoryBox() + self._messages_box.show() + + palette_box = self._palette_box + primary_box = self._primary_box + primary_box.hide() + palette_box.add(self._messages_box) + palette_box.reorder_child(self._messages_box, 0) + + clear_option = PaletteMenuItem(_('Clear history'), 'dialog-cancel') + clear_option.connect('activate', self.__clear_messages_cb) + clear_option.show() + + vbox = Gtk.VBox() + self.set_content(vbox) + vbox.show() + + vbox.add(clear_option) + + self.connect('popup', self.__notice_messages_cb) + + def __clear_messages_cb(self, clear_option): + self.emit('clear-messages') + + def __notice_messages_cb(self, palette): + self.emit('notice-messages') + + def push_message(self, body, summary, icon_name, xo_color): + self._messages_box.push_message(body, summary, icon_name, xo_color) + + +class NotificationButton(ToolButton): + + def __init__(self, icon_name, xo_color): + ToolButton.__init__(self) + self._icon = _create_pulsing_icon(icon_name, xo_color) + self.set_icon_widget(self._icon) + self._icon.show() + self.set_palette_invoker(FrameWidgetInvoker(self)) + + def start_pulsing(self): + self._icon.props.pulsing = True + + def stop_pulsing(self, widget): + self._icon.props.pulsing = False + class NotificationIcon(Gtk.EventBox): __gtype_name__ = 'SugarNotificationIcon' @@ -33,28 +219,29 @@ 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) Gtk.EventBox.__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 = Gdk.color_parse(style.COLOR_BLACK.get_html()) + self.modify_bg(Gtk.StateType.PRELIGHT, color) + + color = Gdk.color_parse(style.COLOR_BUTTON_GREY.get_html()) + self.modify_bg(Gtk.StateType.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_set_property(self, pspec, value): if pspec.name == 'xo-color': @@ -87,17 +274,13 @@ class NotificationIcon(Gtk.EventBox): class NotificationWindow(Gtk.Window): __gtype_name__ = 'SugarNotificationWindow' - def __init__(self, **kwargs): - - Gtk.Window.__init__(self, **kwargs) + def __init__(self): + Gtk.Window.__init__(self) self.set_decorated(False) self.set_resizable(False) self.connect('realize', self._realize_cb) def _realize_cb(self, widget): - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - self.window.set_accept_focus(False) - color = Gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) self.modify_bg(Gtk.StateType.NORMAL, color) diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py index 33f2c80..70e711e 100644 --- a/src/jarabe/view/pulsingicon.py +++ b/src/jarabe/view/pulsingicon.py @@ -90,12 +90,23 @@ class PulsingIcon(Icon): self._pulse_color = None self._paused = False self._pulsing = False + self._timeout = 0 + self._pulsing_sid = None Icon.__init__(self, **kwargs) self._palette = None self.connect('destroy', self.__destroy_cb) + def set_timeout(self, timeout): + self._timeout = timeout + + def get_timeout(self): + return self._timeout + + timeout = GObject.property( + type=int, getter=get_timeout, setter=set_timeout) + def set_pulse_color(self, pulse_color): self._pulse_color = pulse_color self._pulser.update() @@ -142,10 +153,20 @@ class PulsingIcon(Icon): type=bool, default=False, getter=get_paused, setter=set_paused) def set_pulsing(self, pulsing): + if self._pulsing == pulsing: + return + + if self._pulsing_sid is not None: + GObject.source_remove(self._pulsing_sid) + self._pulsing_sid = None + self._pulsing = pulsing if self._pulsing: self._pulser.start(restart=True) + if self.props.timeout > 0: + self._pulsing_sid = GObject.timeout_add_seconds( + self.props.timeout, self.__timeout_cb) else: self._pulser.stop() @@ -165,6 +186,9 @@ class PulsingIcon(Icon): palette = property(_get_palette, _set_palette) + def __timeout_cb(self): + self.props.pulsing = False + def __destroy_cb(self, icon): self._pulser.stop() if self._palette is not None: -- cgit v0.9.1