[gnome-shell] Initial implementation of the message tray and notification daemon
- From: Dan Winship <danw src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [gnome-shell] Initial implementation of the message tray and notification daemon
- Date: Wed, 13 Jan 2010 20:13:49 +0000 (UTC)
commit 11276a35053877993df0c665c554e42f079be8bd
Author: Dan Winship <danw gnome org>
Date: Wed Jan 13 15:05:20 2010 -0500
Initial implementation of the message tray and notification daemon
From the message-tray branch. Most of the UI parts were written by
Marina, and most of the D-Bus parts by me.
data/theme/gnome-shell.css | 21 ++++
js/ui/Makefile.am | 2 +
js/ui/main.js | 6 +
js/ui/messageTray.js | 239 +++++++++++++++++++++++++++++++++++++++++++
js/ui/notificationDaemon.js | 218 +++++++++++++++++++++++++++++++++++++++
src/gnome-shell.in | 5 +-
src/shell-global.c | 2 +-
7 files changed, 490 insertions(+), 3 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 71a5084..67d917b 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -472,6 +472,27 @@ StTooltip {
color: #cccccc;
}
+/* Message Tray */
+#message-tray {
+ background-gradient-direction: vertical;
+ background-gradient-start: rgba(0,0,0,0.01);
+ background-gradient-end: rgba(0,0,0,0.95);
+ height: 28px;
+}
+
+#notification {
+ border-radius: 5px;
+ background: rgba(0,0,0,0.9);
+ color: white;
+ padding: 2px 10px;
+ spacing: 10px;
+}
+
+#summary-mode {
+ spacing: 10px;
+ padding: 2px 4px;
+}
+
/* App Switcher */
.switcher-list {
background: rgba(0,0,0,0.8);
diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am
index 5ef241b..4eb3883 100644
--- a/js/ui/Makefile.am
+++ b/js/ui/Makefile.am
@@ -16,6 +16,8 @@ dist_jsui_DATA = \
link.js \
lookingGlass.js \
main.js \
+ messageTray.js \
+ notificationDaemon.js \
overview.js \
panel.js \
placeDisplay.js \
diff --git a/js/ui/main.js b/js/ui/main.js
index f30644d..f8c95b3 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -15,11 +15,13 @@ const St = imports.gi.St;
const Chrome = imports.ui.chrome;
const Environment = imports.ui.environment;
const ExtensionSystem = imports.ui.extensionSystem;
+const MessageTray = imports.ui.messageTray;
const Overview = imports.ui.overview;
const Panel = imports.ui.panel;
const PlaceDisplay = imports.ui.placeDisplay;
const RunDialog = imports.ui.runDialog;
const LookingGlass = imports.ui.lookingGlass;
+const NotificationDaemon = imports.ui.notificationDaemon;
const ShellDBus = imports.ui.shellDBus;
const Sidebar = imports.ui.sidebar;
const WindowManager = imports.ui.windowManager;
@@ -35,6 +37,8 @@ let overview = null;
let runDialog = null;
let lookingGlass = null;
let wm = null;
+let notificationDaemon = null;
+let messageTray = null;
let recorder = null;
let shellDBusService = null;
let modalCount = 0;
@@ -113,6 +117,8 @@ function start() {
panel = new Panel.Panel();
sidebar = new Sidebar.Sidebar();
wm = new WindowManager.WindowManager();
+ notificationDaemon = new NotificationDaemon.NotificationDaemon();
+ messageTray = new MessageTray.MessageTray();
_startDate = new Date();
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
new file mode 100644
index 0000000..60dbebc
--- /dev/null
+++ b/js/ui/messageTray.js
@@ -0,0 +1,239 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const St = imports.gi.St;
+const Signals = imports.signals;
+const Tweener = imports.ui.tweener;
+
+const Main = imports.ui.main;
+
+const ANIMATION_TIME = 0.2;
+const NOTIFICATION_TIMEOUT = 4;
+
+const MESSAGE_TRAY_TIMEOUT = 0.2;
+
+const ICON_SIZE = 24;
+
+function Notification(icon, text) {
+ this._init(icon, text);
+}
+
+Notification.prototype = {
+ _init: function(icon, text) {
+ this.icon = icon;
+ this.text = text;
+ }
+}
+
+function NotificationBox() {
+ this._init();
+}
+
+NotificationBox.prototype = {
+ _init: function() {
+ this.actor = new St.BoxLayout({ name: 'notification' });
+
+ this._iconBox = new St.Bin();
+ this.actor.add(this._iconBox);
+
+ this._text = new St.Label();
+ this.actor.add(this._text, { expand: true, x_fill: false, y_fill: false, y_align: St.Align.MIDDLE });
+ },
+
+ setContent: function(notification) {
+ this._iconBox.child = notification.icon;
+
+ // Support <b>, <i>, and <u>, escape anything else
+ // so it displays as raw markup.
+ let markup = notification.text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, "<$1");
+ this._text.clutter_text.set_markup(markup);
+ }
+};
+
+function Source(id, createIcon) {
+ this._init(id, createIcon);
+}
+
+Source.prototype = {
+ _init: function(id, createIcon) {
+ this.id = id;
+ if (createIcon)
+ this.createIcon = createIcon;
+ },
+
+ // This can be overridden by a subclass, or by the createIcon
+ // parameter to _init()
+ createIcon: function(size) {
+ throw new Error('no implementation of createIcon in ' + this);
+ },
+
+ notify: function(text) {
+ Main.messageTray.showNotification(new Notification(this.createIcon(ICON_SIZE), text));
+ },
+
+ clicked: function() {
+ this.emit('clicked');
+ },
+
+ destroy: function() {
+ this.emit('destroy');
+ }
+};
+Signals.addSignalMethods(Source.prototype);
+
+function MessageTray() {
+ this._init();
+}
+
+MessageTray.prototype = {
+ _init: function() {
+ this.actor = new St.BoxLayout({ name: 'message-tray',
+ reactive: true });
+
+ let primary = global.get_primary_monitor();
+ this.actor.x = 0;
+ this.actor.y = primary.height - 1;
+
+ this.actor.width = primary.width;
+
+ this._summaryBin = new St.Bin({ x_align: St.Align.END });
+ this.actor.add(this._summaryBin, { expand: true });
+ this._summaryBin.hide();
+
+ this._notificationBox = new NotificationBox();
+ this._notificationQueue = [];
+ this.actor.add(this._notificationBox.actor);
+ this._notificationBox.actor.hide();
+
+ Main.chrome.addActor(this.actor, { affectsStruts: false });
+
+ this.actor.connect('enter-event',
+ Lang.bind(this, this._onMessageTrayEntered));
+ this.actor.connect('leave-event',
+ Lang.bind(this, this._onMessageTrayLeft));
+ this._isShowing = false;
+ this.actor.show();
+
+ this._summary = new St.BoxLayout({ name: 'summary-mode' });
+ this._summaryBin.child = this._summary;
+
+ this._sources = {};
+ this._icons = {};
+ },
+
+ contains: function(source) {
+ return this._sources.hasOwnProperty(source.id);
+ },
+
+ add: function(source) {
+ if (this.contains(source)) {
+ log('Trying to re-add source ' + source.id);
+ return;
+ }
+
+ let iconBox = new St.Bin({ reactive: true });
+ iconBox.child = source.createIcon(ICON_SIZE);
+ this._summary.insert_actor(iconBox, 0);
+ this._icons[source.id] = iconBox;
+ this._sources[source.id] = source;
+
+ iconBox.connect('button-release-event', Lang.bind(this,
+ function () {
+ source.clicked();
+ }));
+
+ source.connect('destroy', Lang.bind(this,
+ function () {
+ this.remove(source);
+ }));
+ },
+
+ remove: function(source) {
+ if (!this.contains(source))
+ return;
+
+ this._summary.remove_actor(this._icons[source.id]);
+ delete this._icons[source.id];
+ delete this._sources[source.id];
+ },
+
+ getSource: function(id) {
+ return this._sources[id];
+ },
+
+ _onMessageTrayEntered: function() {
+ // Don't hide the message tray after a timeout if the user has moved the mouse over it.
+ // We might have a timeout in place if the user moved the mouse away from the message tray for a very short period of time
+ // or if we are showing a notification.
+ if (this._hideTimeoutId > 0)
+ Mainloop.source_remove(this._hideTimeoutId);
+
+ if (this._isShowing)
+ return;
+
+ // If the message tray was not already showing, we'll show it in the summary mode.
+ this._summaryBin.show();
+ this._show();
+ },
+
+ _onMessageTrayLeft: function() {
+ if (!this._isShowing)
+ return;
+
+ // We wait just a little before hiding the message tray in case the user will quickly move the mouse back over it.
+ this._hideTimeoutId = Mainloop.timeout_add(MESSAGE_TRAY_TIMEOUT * 1000, Lang.bind(this, this._hide));
+ },
+
+ _show: function() {
+ this._isShowing = true;
+ let primary = global.get_primary_monitor();
+ Tweener.addTween(this.actor,
+ { y: primary.height - this.actor.height,
+ time: ANIMATION_TIME,
+ transition: "easeOutQuad"
+ });
+ },
+
+ _hide: function() {
+ this._hideTimeoutId = 0;
+
+ let primary = global.get_primary_monitor();
+
+ Tweener.addTween(this.actor,
+ { y: primary.height - 1,
+ time: ANIMATION_TIME,
+ transition: "easeOutQuad",
+ onComplete: this._hideComplete,
+ onCompleteScope: this
+ });
+ return false;
+ },
+
+ _hideComplete: function() {
+ this._isShowing = false;
+ this._summaryBin.hide();
+ this._notificationBox.actor.hide();
+ if (this._notificationQueue.length > 0)
+ this.showNotification(this._notificationQueue.shift());
+ },
+
+ showNotification: function(notification) {
+ if (this._isShowing) {
+ this._notificationQueue.push(notification);
+ return;
+ }
+
+ this._notificationBox.setContent(notification);
+
+ this._notificationBox.actor.x = Math.round((this.actor.width - this._notificationBox.actor.width) / 2);
+ this._notificationBox.actor.show();
+
+ // Because we set up the timeout before we do the animation, we add ANIMATION_TIME to NOTIFICATION_TIMEOUT, so that
+ // NOTIFICATION_TIMEOUT represents the time the notifiation is fully shown.
+ this._hideTimeoutId = Mainloop.timeout_add((NOTIFICATION_TIMEOUT + ANIMATION_TIME) * 1000, Lang.bind(this, this._hide));
+
+ this._show();
+ }
+};
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
new file mode 100644
index 0000000..0190d66
--- /dev/null
+++ b/js/ui/notificationDaemon.js
@@ -0,0 +1,218 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const DBus = imports.dbus;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const Mainloop = imports.mainloop;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const Params = imports.misc.params;
+
+let nextNotificationId = 1;
+
+const NotificationDaemonIface = {
+ name: 'org.freedesktop.Notifications',
+ methods: [{ name: 'Notify',
+ inSignature: 'susssasa{sv}i',
+ outSignature: 'u'
+ },
+ { name: 'CloseNotification',
+ inSignature: 'u',
+ outSignature: ''
+ },
+ { name: 'GetCapabilities',
+ inSignature: '',
+ outSignature: 'as'
+ },
+ { name: 'GetServerInformation',
+ inSignature: '',
+ outSignature: 'ssss'
+ }],
+ signals: [{ name: 'NotificationClosed',
+ inSignature: 'uu' },
+ { name: 'ActionInvoked',
+ inSignature: 'us' }]
+};
+
+const NotificationClosedReason = {
+ EXPIRED: 1,
+ DISMISSED: 2,
+ APP_CLOSED: 3,
+ UNDEFINED: 4
+};
+
+const Urgency = {
+ LOW: 0,
+ NORMAL: 1,
+ CRITICAL: 2
+};
+
+function NotificationDaemon() {
+ this._init();
+}
+
+NotificationDaemon.prototype = {
+ _init: function() {
+ DBus.session.exportObject('/org/freedesktop/Notifications', this);
+
+ this._everAcquiredName = false;
+ DBus.session.acquire_name('org.freedesktop.Notifications',
+ // We pass MANY_INSTANCES so that if
+ // notification-daemon is running, we'll
+ // get queued behind it and then get the
+ // name after killing it below
+ DBus.MANY_INSTANCES,
+ Lang.bind(this, this._acquiredName),
+ Lang.bind(this, this._lostName));
+ },
+
+ _acquiredName: function() {
+ this._everAcquiredName = true;
+ },
+
+ _lostName: function() {
+ if (this._everAcquiredName)
+ log('Lost name org.freedesktop.Notifications!');
+ else if (GLib.getenv('GNOME_SHELL_NO_REPLACE'))
+ log('Failed to acquire org.freedesktop.Notifications');
+ else {
+ log('Failed to acquire org.freedesktop.Notifications; trying again');
+
+ // kill the notification-daemon. pkill is more portable
+ // than killall, but on Linux at least it won't match if
+ // you pass more than 15 characters of the process name...
+ // However, if you use the "-f" flag to match the entire
+ // command line, it will work, but we have to be careful
+ // in that case that we don't match "gedit
+ // notification-daemon.c" or whatever...
+ let p = new Shell.Process({ args: ['pkill', '-f',
+ '^([^ ]*/)?(notification-daemon|notify-osd)$']});
+ p.run();
+ }
+ },
+
+ _sourceId: function(id) {
+ return 'notification-' + id;
+ },
+
+ Notify: function(appName, replacesId, icon, summary, body,
+ actions, hints, timeout) {
+ let id, source = null;
+
+ if (replacesId != 0) {
+ id = replacesId;
+ source = Main.messageTray.getSource(this._sourceId(id));
+ // source may be null if the current source was destroyed
+ // right as the client sent the new notification
+ }
+
+ if (source == null) {
+ id = nextNotificationId++;
+
+ source = new Source(this._sourceId(id), icon, hints);
+ Main.messageTray.add(source);
+
+ source.connect('clicked', Lang.bind(this,
+ function() {
+ source.destroy();
+ this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED);
+ }));
+ }
+
+ summary = GLib.markup_escape_text(summary, -1);
+ if (body)
+ source.notify('<b>' + summary + '</b>: ' + body);
+ else
+ source.notify('<b>' + summary + '</b>');
+ return id;
+ },
+
+ CloseNotification: function(id) {
+ let source = Main.messageTray.getSource(this._sourceId(id));
+ if (source)
+ source.destroy();
+ this._emitNotificationClosed(id, NotificationClosedReason.APP_CLOSED);
+ },
+
+ GetCapabilities: function() {
+ return [
+ // 'actions',
+ 'body',
+ // 'body-hyperlinks',
+ // 'body-images',
+ 'body-markup',
+ // 'icon-multi',
+ 'icon-static'
+ // 'sound',
+ ];
+ },
+
+ GetServerInformation: function() {
+ return [
+ 'GNOME Shell',
+ 'GNOME',
+ '0.1', // FIXME, get this from somewhere
+ '1.0'
+ ];
+ },
+
+ _emitNotificationClosed: function(id, reason) {
+ DBus.session.emit_signal('/org/freedesktop/Notifications',
+ 'org.freedesktop.Notifications',
+ 'NotificationClosed', 'uu',
+ [id, reason]);
+ }
+};
+
+DBus.conformExport(NotificationDaemon.prototype, NotificationDaemonIface);
+
+function Source(sourceId, icon, hints) {
+ this._init(sourceId, icon, hints);
+}
+
+Source.prototype = {
+ __proto__: MessageTray.Source.prototype,
+
+ _init: function(sourceId, icon, hints) {
+ MessageTray.Source.prototype._init.call(this, sourceId);
+
+ hints = Params.parse(hints, { urgency: Urgency.NORMAL }, true);
+
+ this._icon = icon;
+ this._iconData = hints.icon_data;
+ this._urgency = hints.urgency;
+ },
+
+ createIcon: function(size) {
+ let textureCache = Shell.TextureCache.get_default();
+
+ if (this._icon) {
+ if (this._icon.substr(0, 7) == 'file://')
+ return textureCache.load_uri_async(this._icon, size, size);
+ else if (this._icon[0] == '/') {
+ let uri = GLib.filename_to_uri(this._icon, null);
+ return textureCache.load_uri_async(uri, size, size);
+ } else
+ return textureCache.load_icon_name(this._icon, size);
+ } else if (this._iconData) {
+ let [width, height, rowStride, hasAlpha,
+ bitsPerSample, nChannels, data] = this._iconData;
+ return textureCache.load_from_raw(data, data.length, hasAlpha,
+ width, height, rowStride, size);
+ } else {
+ let stockIcon;
+ switch (this._urgency) {
+ case Urgency.LOW:
+ case Urgency.NORMAL:
+ stockIcon = 'gtk-dialog-info';
+ break;
+ case Urgency.CRITICAL:
+ stockIcon = 'gtk-dialog-error';
+ break;
+ }
+ return textureCache.load_icon_name(stockIcon, size);
+ }
+ }
+};
diff --git a/src/gnome-shell.in b/src/gnome-shell.in
old mode 100644
new mode 100755
index 41354ad..254cdee
--- a/src/gnome-shell.in
+++ b/src/gnome-shell.in
@@ -357,8 +357,9 @@ try:
shell = None
if options.xephyr:
xephyr = start_xephyr()
- # This makes us not grab the org.gnome.Panel name
- os.environ['GNOME_SHELL_NO_REPLACE_PANEL'] = '1'
+ # This makes us not grab the org.gnome.Panel or
+ # org.freedesktop.Notifications D-Bus names
+ os.environ['GNOME_SHELL_NO_REPLACE'] = '1'
shell = start_shell()
else:
xephyr = None
diff --git a/src/shell-global.c b/src/shell-global.c
index 7fd37c9..3e422b1 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -751,7 +751,7 @@ shell_global_grab_dbus_service (ShellGlobal *global)
* unless a special environment variable is passed. The environment
* variable is used by the gnome-shell (no --replace) launcher in
* Xephyr */
- if (!g_getenv ("GNOME_SHELL_NO_REPLACE_PANEL"))
+ if (!g_getenv ("GNOME_SHELL_NO_REPLACE"))
{
if (!dbus_g_proxy_call (bus, "RequestName", &error, G_TYPE_STRING,
"org.gnome.Panel", G_TYPE_UINT,
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]