[gnome-shell] MessageTray: show multiple notifications per source



commit 812812d8170a43d2308ad5fbd45d64ef2bb509c9
Author: Marina Zhurakhinskaya <marinaz redhat com>
Date:   Mon Mar 21 17:43:34 2011 -0400

    MessageTray: show multiple notifications per source
    
    Add an ability to show multple notifications per source, so that
    the user doesn't miss seeing any notifications.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=611611

 js/ui/messageTray.js        |  254 ++++++++++++++++++++++++++++---------------
 js/ui/notificationDaemon.js |    6 +-
 js/ui/overview.js           |    8 +-
 3 files changed, 174 insertions(+), 94 deletions(-)
---
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index 22d15c5..9b82f02 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -408,12 +408,12 @@ Notification.prototype = {
         this._spacing = 0;
 
         source.connect('destroy', Lang.bind(this,
-            // Avoid passing 'source' as an argument to this.destroy()
-            function () {
-                this.destroy();
+            function (source, reason) {
+                this.destroy(reason);
             }));
 
         this.actor = new St.Button();
+        this.actor._delegate = this;
         this.actor.connect('clicked', Lang.bind(this, this._onClicked));
         this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
 
@@ -516,6 +516,10 @@ Notification.prototype = {
         this._updated();
     },
 
+    setIconVisible: function(visible) {
+        this._icon.visible = visible;
+    },
+
     _createScrollArea: function() {
         this._table.add_style_class_name('multi-line-notification');
         this._scrollArea = new St.ScrollView({ name: 'notification-scrollview',
@@ -806,6 +810,7 @@ Notification.prototype = {
     destroy: function(reason) {
         this._destroyedReason = reason;
         this.actor.destroy();
+        this.actor._delegate = null;
     }
 };
 Signals.addSignalMethods(Notification.prototype);
@@ -825,6 +830,8 @@ Source.prototype = {
                                      y_fill: true });
         this.isTransient = false;
         this.isChat = false;
+
+        this.notifications = [];
     },
 
     setTransient: function(isTransient) {
@@ -845,23 +852,21 @@ Source.prototype = {
     },
 
     pushNotification: function(notification) {
-        if (this.notification) {
-            this.notification.disconnect(this._notificationClickedId);
-            this.notification.disconnect(this._notificationDestroyedId);
+        if (this.notifications.indexOf(notification) < 0) {
+            this.notifications.push(notification);
+            this.emit('notification-added', notification);
         }
 
-        // FIXME: Right now, we don't save multiple notifications.
-        this.notification = notification;
-
-        this._notificationClickedId = notification.connect('clicked', Lang.bind(this, this.open));
-        this._notificationDestroyedId = notification.connect('destroy', Lang.bind(this,
+        notification.connect('clicked', Lang.bind(this, this.open));
+        notification.connect('destroy', Lang.bind(this,
             function () {
-                if (this.notification == notification) {
-                    this.notification = null;
-                    this._notificationDestroyedId = 0;
-                    this._notificationClickedId = 0;
-                    this._notificationRemoved();
-                }
+                let index = this.notifications.indexOf(notification);
+                if (index < 0)
+                    return;
+
+                this.notifications.splice(index, 1);
+                if (this.notifications.length == 0)
+                    this._lastNotificationRemoved();
             }));
     },
 
@@ -870,8 +875,8 @@ Source.prototype = {
         this.emit('notify', notification);
     },
 
-    destroy: function() {
-        this.emit('destroy');
+    destroy: function(reason) {
+        this.emit('destroy', reason);
     },
 
     // A subclass can redefine this to "steal" clicks from the
@@ -895,8 +900,14 @@ Source.prototype = {
     open: function(notification) {
     },
 
+    destroyNonResidentNotifications: function() {
+        for (let i = this.notifications.length - 1; i >= 0; i--)
+            if (!this.notifications[i].resident)
+                this.notifications[i].destroy();
+    },
+
     // Default implementation is to destroy this source, but subclasses can override
-    _notificationRemoved: function() {
+    _lastNotificationRemoved: function() {
         this.destroy();
     }
 };
@@ -909,6 +920,8 @@ function SummaryItem(source) {
 SummaryItem.prototype = {
     _init: function(source) {
         this.source = source;
+        this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource));
+
         this.actor = new St.Button({ style_class: 'summary-source-button',
                                      y_fill: true,
                                      reactive: true,
@@ -930,6 +943,13 @@ SummaryItem.prototype = {
         this._sourceBox.add(this._sourceIcon, { y_fill: false });
         this._sourceBox.add(this._sourceTitleBin, { expand: true, y_fill: false });
         this.actor.child = this._sourceBox;
+
+        this.notificationStack = new St.BoxLayout({ name: 'summary-notification-stack',
+                                                     vertical: true });
+        this._notificationExpandedIds = [];
+        this._notificationDoneDisplayingIds = [];
+        this._notificationDestroyedIds = [];
+
         this.rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu',
                                                  vertical: true });
 
@@ -938,14 +958,14 @@ SummaryItem.prototype = {
         item = new PopupMenu.PopupMenuItem(_("Open"));
         item.connect('activate', Lang.bind(this, function() {
             source.open();
-            this.emit('right-click-menu-done-displaying');
+            this.emit('done-displaying-content');
         }));
         this.rightClickMenu.add(item.actor);
 
         item = new PopupMenu.PopupMenuItem(_("Remove"));
         item.connect('activate', Lang.bind(this, function() {
             source.destroy();
-            this.emit('right-click-menu-done-displaying');
+            this.emit('done-displaying-content');
         }));
         this.rightClickMenu.add(item.actor);
 
@@ -975,6 +995,72 @@ SummaryItem.prototype = {
 
     setEllipsization: function(mode) {
         this._sourceTitle.clutter_text.ellipsize = mode;
+    },
+
+    prepareNotificationStackForShowing: function() {
+        if (this.notificationStack.get_children().length > 0)
+            return;
+
+        for (let i = 0; i < this.source.notifications.length; i++) {
+            this._appendNotificationToStack(this.source.notifications[i]);
+        }
+    },
+
+    doneShowingNotificationStack: function() {
+        let notificationActors = this.notificationStack.get_children();
+        for (let i = 0; i < notificationActors.length; i++) {
+            notificationActors[i]._delegate.collapseCompleted();
+            notificationActors[i]._delegate.disconnect(this._notificationExpandedIds[i]);
+            notificationActors[i]._delegate.disconnect(this._notificationDoneDisplayingIds[i]);
+            notificationActors[i]._delegate.disconnect(this._notificationDestroyedIds[i]);
+            this.notificationStack.remove_actor(notificationActors[i]);
+            notificationActors[i]._delegate.setIconVisible(true);
+        }
+        this._notificationExpandedIds = [];
+        this._notificationDoneDisplayingIds = [];
+        this._notificationDestroyedIds = [];
+    },
+
+    _notificationAddedToSource: function(source, notification) {
+        if (this.notificationStack.mapped)
+            this._appendNotificationToStack(notification);
+    },
+
+    _appendNotificationToStack: function(notification) {
+        let notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated));
+        this._notificationExpandedIds.push(notificationExpandedId);
+        let notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying));
+        this._notificationDoneDisplayingIds.push(notificationDoneDisplayingId);
+        let notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed));
+        this._notificationDestroyedIds.push(notificationDestroyedId);
+        if (this.notificationStack.get_children().length > 0)
+            notification.setIconVisible(false);
+        this.notificationStack.add(notification.actor);
+        notification.expand(false);
+    },
+
+    _contentUpdated: function() {
+        this.emit('content-updated');
+    },
+
+    _notificationDoneDisplaying: function() {
+        this.emit('done-displaying-content');
+    },
+
+    _notificationDestroyed: function(notification) {
+        let index = this.notificationStack.get_children().indexOf(notification.actor);
+        if (index >= 0) {
+            notification.disconnect(this._notificationExpandedIds[index]);
+            this._notificationExpandedIds.splice(index, 1);
+            notification.disconnect(this._notificationDoneDisplayingIds[index]);
+            this._notificationDoneDisplayingIds.splice(index, 1);
+            notification.disconnect(this._notificationDestroyedIds[index]);
+            this._notificationDestroyedIds.splice(index, 1);
+            this.notificationStack.remove_actor(notification.actor);
+            this._contentUpdated();
+        }
+        if (this.notificationStack.get_children().length > 0)
+            this.notificationStack.get_children()[0]._delegate.setIconVisible(true);
     }
 };
 Signals.addSignalMethods(SummaryItem.prototype);
@@ -1023,9 +1109,9 @@ MessageTray.prototype = {
         this._summaryBoxPointer.actor.lower_bottom();
         this._summaryBoxPointer.actor.hide();
 
-        this._summaryNotification = null;
-        this._summaryNotificationClickedId = 0;
-        this._summaryRightClickMenuClickedId = 0;
+        this._summaryBoxPointerItem = null;
+        this._summaryBoxPointerContentUpdatedId = 0;
+        this._summaryBoxPointerDoneDisplayingId = 0;
         this._clickedSummaryItem = null;
         this._clickedSummaryItemMouseButton = -1;
         this._clickedSummaryItemAllocationChangedId = 0;
@@ -1066,11 +1152,10 @@ MessageTray.prototype = {
         this._notificationTimeoutId = 0;
         this._notificationExpandedId = 0;
         this._summaryBoxPointerState = State.HIDDEN;
-        this._summaryNotificationTimeoutId = 0;
-        this._summaryNotificationExpandedId = 0;
+        this._summaryBoxPointerTimeoutId = 0;
         this._overviewVisible = Main.overview.visible;
         this._notificationRemoved = false;
-        this._reNotifyWithSummaryNotificationAfterHide = false;
+        this._reNotifyAfterHideNotification = null;
 
         Main.chrome.addActor(this.actor, { affectsStruts: false,
                                            visibleInOverview: true });
@@ -1251,13 +1336,6 @@ MessageTray.prototype = {
 
         if (needUpdate);
             this._updateState();
-
-        // remove all notifications with this source from the queue
-        let newNotificationQueue = [];
-        for (let i = this._notificationQueue.length - 1; i >= 0; i--) {
-            if (this._notificationQueue[i].source == source)
-                this._notificationQueue[i].destroy();
-        }
     },
 
     _onNotificationDestroy: function(notification) {
@@ -1286,14 +1364,18 @@ MessageTray.prototype = {
     },
 
     _onNotify: function(source, notification) {
-        if (notification == this._summaryNotification) {
-            if (!this._summaryNotificationExpandedId)
-                // We must be in the process of hiding the summary notification.
-                // If the summary notification is updated while it is being
-                // hidden, we show the update as a new notification. However,
-                // we must first wait till the hide is complete and the
-                // notification actor is not part of the stage.
-                this._reNotifyWithSummaryNotificationAfterHide = true;
+        if (this._summaryBoxPointerItem && this._summaryBoxPointerItem.source == source) {
+            if (this._summaryBoxPointerState == State.HIDING)
+                // We are in the process of hiding the summary box pointer.
+                // If there is an update for one of the notifications or
+                // a new notification to be added to the notification stack
+                // while it is in the process of being hidden, we show it as
+                // a new notification. However, we first wait till the hide
+                // is complete. This is especially important if one of the
+                // notifications in the stack was updated because we will
+                // need to be able to re-parent its actor to a different
+                // part of the stage.
+                this._reNotifyAfterHideNotification = notification;
             return;
         }
 
@@ -1629,15 +1711,18 @@ MessageTray.prototype = {
         let summarySourceIsMainNotificationSource = (haveClickedSummaryItem && this._notification &&
                                                      this._clickedSummaryItem.source == this._notification.source);
         let canShowSummaryBoxPointer = this._summaryState == State.SHOWN;
-        let wrongSummaryNotification = (this._clickedSummaryItemMouseButton == 1 &&
-                                        this._summaryNotification != this._clickedSummaryItem.source.notification);
+        // We only have sources with empty notification stacks for legacy tray icons. Currently, we never attempt
+        // to show notifications for legacy tray icons, but this would be necessary if we did.
+        let requestedNotificationStackIsEmpty = (this._clickedSummaryItemMouseButton == 1 && this._clickedSummaryItem.source.notifications.length == 0);
+        let wrongSummaryNotificationStack = (this._clickedSummaryItemMouseButton == 1 &&
+                                             this._summaryBoxPointer.bin.child != this._clickedSummaryItem.notificationStack);
         let wrongSummaryRightClickMenu = (this._clickedSummaryItemMouseButton == 3 &&
                                           this._summaryBoxPointer.bin.child != this._clickedSummaryItem.rightClickMenu);
         let wrongSummaryBoxPointer = (haveClickedSummaryItem &&
-                                      (wrongSummaryNotification || wrongSummaryRightClickMenu));
+                                      (wrongSummaryNotificationStack || wrongSummaryRightClickMenu));
 
         if (this._summaryBoxPointerState == State.HIDDEN) {
-            if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer)
+            if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer && !requestedNotificationStackIsEmpty)
                 this._showSummaryBoxPointer();
         } else if (this._summaryBoxPointerState == State.SHOWN) {
             if (!haveClickedSummaryItem || !canShowSummaryBoxPointer || wrongSummaryBoxPointer)
@@ -1900,33 +1985,28 @@ MessageTray.prototype = {
     },
 
     _showSummaryBoxPointer: function() {
+        this._summaryBoxPointerItem = this._clickedSummaryItem;
+        this._summaryBoxPointerContentUpdatedId = this._summaryBoxPointerItem.connect('content-updated',
+                                                                                      Lang.bind(this, this._adjustSummaryBoxPointerPosition));
+        this._summaryBoxPointerDoneDisplayingId = this._summaryBoxPointerItem.connect('done-displaying-content',
+                                                                                      Lang.bind(this, this._escapeTray));
         if (this._clickedSummaryItemMouseButton == 1) {
-            let clickedSummaryItemNotification = this._clickedSummaryItem.source.notification;
-            let index = this._notificationQueue.indexOf(clickedSummaryItemNotification);
-            if (index != -1)
-                this._notificationQueue.splice(index, 1);
-
-            this._summaryNotification = clickedSummaryItemNotification;
-            this._summaryNotificationClickedId = this._summaryNotification.connect('done-displaying',
-                                                                                   Lang.bind(this, this._escapeTray));
-            this._summaryBoxPointer.bin.child = this._summaryNotification.actor;
-            if (!this._summaryNotificationExpandedId)
-                this._summaryNotificationExpandedId = this._summaryNotification.connect('expanded',
-                                                                                        Lang.bind(this, this._onSummaryBoxPointerExpanded));
-            this._summaryNotification.expand(false);
+            this._notificationQueue = this._notificationQueue.filter( Lang.bind(this,
+                function(notification) {
+                    return this._summaryBoxPointerItem.source != notification.source;
+                }));
+            this._summaryBoxPointerItem.prepareNotificationStackForShowing();
+            this._summaryBoxPointer.bin.child = this._summaryBoxPointerItem.notificationStack;
         } else if (this._clickedSummaryItemMouseButton == 3) {
-            this._summaryRightClickMenuClickedId = this._clickedSummaryItem.connect('right-click-menu-done-displaying',
-                                                                                    Lang.bind(this, this._escapeTray));
             this._summaryBoxPointer.bin.child = this._clickedSummaryItem.rightClickMenu;
         }
 
         this._focusGrabber.grabFocus(this._summaryBoxPointer.bin.child);
 
-
         this._clickedSummaryItemAllocationChangedId =
             this._clickedSummaryItem.actor.connect('allocation-changed',
                                                    Lang.bind(this, this._adjustSummaryBoxPointerPosition));
-        // _clickedSummaryItem.actor can change absolute postiion without changing allocation
+        // _clickedSummaryItem.actor can change absolute position without changing allocation
         this._summaryMotionId = this._summary.connect('allocation-changed',
                                                       Lang.bind(this, this._adjustSummaryBoxPointerPosition));
 
@@ -1957,26 +2037,13 @@ MessageTray.prototype = {
             this._summaryMotionId = 0;
         }
 
-        if (this._summaryRightClickMenuClickedId) {
-            this._clickedSummaryItem.disconnect(this._summaryRightClickMenuClickedId);
-            this._summaryRightClickMenuClickedId = 0;
-        }
-
         if (this._clickedSummaryItem)
             this._clickedSummaryItem.actor.remove_style_pseudo_class('selected');
         this._clickedSummaryItem = null;
         this._clickedSummaryItemMouseButton = -1;
     },
 
-    _onSummaryBoxPointerExpanded: function() {
-        this._adjustSummaryBoxPointerPosition();
-    },
-
     _hideSummaryBoxPointer: function() {
-        if (this._summaryNotificationExpandedId) {
-            this._summaryNotification.disconnect(this._summaryNotificationExpandedId);
-            this._summaryNotificationExpandedId = 0;
-        }
         // Unset this._clickedSummaryItem if we are no longer showing the summary
         if (this._summaryState != State.SHOWN)
             this._unsetClickedSummaryItem();
@@ -1987,21 +2054,32 @@ MessageTray.prototype = {
     },
 
     _hideSummaryBoxPointerCompleted: function() {
+        let doneShowingNotificationStack = (this._summaryBoxPointer.bin.child == this._summaryBoxPointerItem.notificationStack);
+
         this._summaryBoxPointerState = State.HIDDEN;
         this._summaryBoxPointer.bin.child = null;
-        if (this._summaryNotification != null) {
-            this._summaryNotification.collapseCompleted();
-            this._summaryNotification.disconnect(this._summaryNotificationClickedId);
-            this._summaryNotificationClickedId = 0;
-            let summaryNotification = this._summaryNotification;
-            this._summaryNotification = null;
-            if (summaryNotification.isTransient && !this._reNotifyWithSummaryNotificationAfterHide)
-                summaryNotification.destroy(NotificationDestroyedReason.EXPIRED);
-            if (this._reNotifyWithSummaryNotificationAfterHide) {
-                this._onNotify(summaryNotification.source, summaryNotification);
-                this._reNotifyWithSummaryNotificationAfterHide = false;
+        this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerContentUpdatedId);
+        this._summaryBoxPointerContentUpdatedId = 0;
+        this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerDoneDisplayingId);
+        this._summaryBoxPointerDoneDisplayingId = 0;
+
+        let sourceNotificationStackDoneShowing = null;
+        if (doneShowingNotificationStack) {
+            this._summaryBoxPointerItem.doneShowingNotificationStack();
+            sourceNotificationStackDoneShowing = this._summaryBoxPointerItem.source;
+        }
+
+        this._summaryBoxPointerItem = null;
+
+        if (sourceNotificationStackDoneShowing) {
+            if (sourceNotificationStackDoneShowing.isTransient && !this._reNotifyAfterHideNotification)
+                sourceNotificationStackDoneShowing.destroy(NotificationDestroyedReason.EXPIRED);
+            if (this._reNotifyAfterHideNotification) {
+                this._onNotify(this._reNotifyAfterHideNotification.source, this._reNotifyAfterHideNotification);
+                this._reNotifyAfterHideNotification = null;
             }
         }
+
         if (this._clickedSummaryItem)
             this._updateState();
     }
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
index e807af4..f8f356e 100644
--- a/js/ui/notificationDaemon.js
+++ b/js/ui/notificationDaemon.js
@@ -390,8 +390,7 @@ NotificationDaemon.prototype = {
         for (let id in this._sources) {
             let source = this._sources[id];
             if (source.app == tracker.focus_app) {
-                if (source.notification && !source.notification.resident)
-                    source.notification.destroy();
+                source.destroyNonResidentNotifications();
                 return;
             }
         }
@@ -508,10 +507,11 @@ Source.prototype = {
     },
 
     open: function(notification) {
+        this.destroyNonResidentNotifications();
         this.openApp();
     },
 
-    _notificationRemoved: function() {
+    _lastNotificationRemoved: function() {
         if (!this._trayIcon)
             this.destroy();
     },
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 351ba7c..8fdd661 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -75,11 +75,13 @@ ShellInfo.prototype = {
             Main.messageTray.add(this._source);
         }
 
-        let notification = this._source.notification;
-        if (notification == null)
+        let notification = null;
+        if (this._source.notifications.length == 0) {
             notification = new MessageTray.Notification(this._source, text, null);
-        else
+        } else {
+            notification = this._source.notifications[0];
             notification.update(text, null, { clear: true });
+        }
 
         notification.setTransient(true);
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]