[gnome-shell] Initial implementation of the message tray and notification daemon



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, "&lt;$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]