Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAjay Garg <ajay@activitycentral.com>2012-10-16 17:17:04 (GMT)
committer Ajay Garg <ajay@activitycentral.com>2012-10-16 17:40:23 (GMT)
commit73c3c268dff68e5d3a6d9701418c2a8b72861150 (patch)
tree990d842e5fb3a33c9974477a2cd79f39c6742518
parenteb44a0c2884d335957b53ae6df0b80c2260f3766 (diff)
Notifications (gconf-controllable)
Signed-off-by: Ajay Garg <ajay@activitycentral.com>
-rwxr-xr-xbin/sugar-session33
-rw-r--r--src/jarabe/frame/__init__.py4
-rw-r--r--src/jarabe/frame/activitiestray.py18
-rw-r--r--src/jarabe/frame/frame.py132
-rw-r--r--src/jarabe/frame/notification.py217
-rw-r--r--src/jarabe/view/pulsingicon.py24
6 files changed, 390 insertions, 38 deletions
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..19754c9 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,76 @@ 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)
+ 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 = "<img.*?/>"
+
+
+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(
+ '<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):
+ # 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: