[gnome-shell] Show source title on hover, notification on click in the message tray



commit 83689e494cd1e4f1e417718581d826ce9add153f
Author: Marina Zhurakhinskaya <marinaz redhat com>
Date:   Wed Jun 23 15:20:39 2010 -0400

    Show source title on hover, notification on click in the message tray
    
    This is part of the design update for the message tray.
    
    Source now takes an extra argument called 'title'.
    
    All expanded message tray items are same width, which is determined by
    the width of the item with the longest title, up to MAX_SOURCE_TITLE_WIDTH.
    This is done so that items don't move around too much when one is expanded
    and another one is collapsed.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=617224

 data/theme/gnome-shell.css      |   21 +++-
 js/ui/messageTray.js            |  272 ++++++++++++++++++++++++++++++---------
 js/ui/notificationDaemon.js     |   20 ++-
 js/ui/telepathyClient.js        |    9 +-
 js/ui/windowAttentionHandler.js |    2 +-
 5 files changed, 250 insertions(+), 74 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index b3abbe0..6669845 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -891,12 +891,27 @@ StTooltip {
  * pseudo-class we could fix that...
  */
 #summary-mode {
-    spacing: 6px;
+    spacing: 4px;
     padding: 2px 0px 0px 4px;
 }
 
-.summary-icon  {
-    padding: 0px 4px 2px 0px;
+.summary-source-button  {
+    padding: 0px 2px 0px 2px;
+    border: 1px solid transparent;
+}
+
+.summary-source-button:hover {
+    border: 1px solid #ffffff;
+}
+
+.summary-source {
+    spacing: 4px;
+}
+
+.source-title {
+    font: 12px sans-serif;
+    font-weight: bold;
+    color: white;
 }
 
 .calendar-calendarweek {
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index aaba10b..90e9a78 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -20,6 +20,8 @@ const HIDE_TIMEOUT = 0.2;
 
 const ICON_SIZE = 24;
 
+const MAX_SOURCE_TITLE_WIDTH = 180;
+
 const State = {
     HIDDEN:  0,
     SHOWING: 1,
@@ -352,14 +354,14 @@ Notification.prototype = {
 };
 Signals.addSignalMethods(Notification.prototype);
 
-function Source(id, createIcon) {
-    this._init(id, createIcon);
+function Source(id, title, createIcon) {
+    this._init(id, title, createIcon);
 }
 
 Source.prototype = {
-    _init: function(id, createIcon) {
+    _init: function(id, title, createIcon) {
         this.id = id;
-        this.text = null;
+        this.title = title;
         if (createIcon)
             this.createIcon = createIcon;
     },
@@ -397,6 +399,144 @@ Source.prototype = {
 };
 Signals.addSignalMethods(Source.prototype);
 
+function SummaryItem(source, minTitleWidth) {
+    this._init(source, minTitleWidth);
+}
+
+SummaryItem.prototype = {
+    _init: function(source, minTitleWidth) {
+        this.source = source;
+        // The message tray items should all be the same width when expanded. Because the only variation is introduced by the width of the title,
+        // we pass in the desired minimum title width, which is the maximum title width of the items which are currently in the tray. If the width
+        // of the title of this item is greater (up to MAX_SOURCE_TITLE_WIDTH), then that width will be used, and the width of all the other items
+        // in the message tray will be readjusted.
+        this._minTitleWidth = minTitleWidth;
+        this.actor = new St.Button({ style_class: 'summary-source-button',
+                                     reactive: true,
+                                     track_hover: true });
+
+        this._sourceBox = new  Shell.GenericContainer({ style_class: 'summary-source',
+                                                        reactive: true });
+        this._sourceBox.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
+        this._sourceBox.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
+        this._sourceBox.connect('allocate', Lang.bind(this, this._allocate));
+
+        this._sourceIcon = source.createIcon(ICON_SIZE);
+        this._sourceTitleBin = new St.Bin({ y_align: St.Align.MIDDLE, x_fill: true });
+        this._sourceTitle = new St.Label({ style_class: 'source-title',
+                                           text: source.title });
+        this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._sourceTitleBin.child = this._sourceTitle;
+
+        this._sourceBox.add_actor(this._sourceIcon);
+        this._sourceBox.add_actor(this._sourceTitleBin);
+        this._widthFraction = 0;
+        this.actor.child = this._sourceBox;
+    },
+
+    getTitleNaturalWidth: function() {
+        let [sourceTitleBinMinWidth, sourceTitleBinNaturalWidth] =
+            this._sourceTitleBin.get_preferred_width(-1);
+        return Math.min(sourceTitleBinNaturalWidth, MAX_SOURCE_TITLE_WIDTH);
+    },
+
+    setMinTitleWidth: function(minTitleWidth) {
+        this._minTitleWidth = minTitleWidth;
+    },
+
+    _getPreferredWidth: function(actor, forHeight, alloc) {
+        let [found, spacing] = this._sourceBox.get_theme_node().get_length('spacing', false);
+        if (!found)
+            spacing = 0;
+        let [sourceIconMinWidth, sourceIconNaturalWidth] = this._sourceIcon.get_preferred_width(forHeight);
+        let [sourceTitleBinMinWidth, sourceTitleBinNaturalWidth] =
+            this._sourceTitleBin.get_preferred_width(forHeight);
+        let minWidth = sourceIconNaturalWidth +
+                       (this._widthFraction > 0 ? spacing : 0) +
+                       this._widthFraction * Math.min(Math.max(sourceTitleBinNaturalWidth, this._minTitleWidth),
+                                                      MAX_SOURCE_TITLE_WIDTH);
+        alloc.min_size = minWidth;
+        alloc.natural_size = minWidth;
+    },
+
+    _getPreferredHeight: function(actor, forWidth, alloc) {
+        let [sourceIconMinHeight, sourceIconNaturalHeight] = this._sourceIcon.get_preferred_height(forWidth);
+        alloc.min_size = sourceIconNaturalHeight;
+        alloc.natural_size = sourceIconNaturalHeight;
+    },
+
+    _allocate: function (actor, box, flags) {
+        let width = box.x2 - box.x1;
+        let height = box.y2 - box.y1;
+
+        let [sourceIconMinWidth, sourceIconNaturalWidth] = this._sourceIcon.get_preferred_width(-1);
+        let [sourceIconMinHeight, sourceIconNaturalHeight] = this._sourceIcon.get_preferred_height(-1);
+
+        let iconBox = new Clutter.ActorBox();
+        iconBox.x1 = 0;
+        iconBox.y1 = 0;
+        iconBox.x2 = sourceIconNaturalWidth;
+        iconBox.y2 = sourceIconNaturalHeight;
+
+        this._sourceIcon.allocate(iconBox, flags);
+
+        let [found, spacing] = this._sourceBox.get_theme_node().get_length('spacing', false);
+        if (!found)
+            spacing = 0;
+
+        let titleBox = new Clutter.ActorBox();
+        if (width > sourceIconNaturalWidth + spacing) {
+            titleBox.x1 = iconBox.x2 + spacing;
+            titleBox.x2 = width;
+        } else {
+            titleBox.x1 = iconBox.x2;
+            titleBox.x2 = iconBox.x2;
+        }
+        titleBox.y1 = 0;
+        titleBox.y2 = height;
+
+        this._sourceTitleBin.allocate(titleBox, flags);
+
+        this._sourceTitleBin.set_clip(0, 0, titleBox.x2 - titleBox.x1, height);
+    },
+
+    expand: function() {
+        // this._adjustEllipsization replaces some text with the dots at the end of the animation,
+        // and then we replace the dots with the text before we begin the animation to collapse
+        // the title. These changes are not noticeable at the speed with which we do the animation,
+        // while animating in the ellipsized mode does not look good.
+        Tweener.addTween(this,
+                         { widthFraction: 1,
+                           time: ANIMATION_TIME,
+                           transition: 'linear',
+                           onComplete: this._adjustEllipsization,
+                           onCompleteScope: this });
+    },
+
+    collapse: function() {
+        this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        Tweener.addTween(this,
+                         { widthFraction: 0,
+                           time: ANIMATION_TIME,
+                           transition: 'linear' });
+    },
+
+    _adjustEllipsization: function() {
+        let [sourceTitleBinMinWidth, sourceTitleBinNaturalWidth] = this._sourceTitleBin.get_preferred_width(-1);
+        if (sourceTitleBinNaturalWidth > MAX_SOURCE_TITLE_WIDTH)
+            this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+    },
+
+    set widthFraction(widthFraction) {
+        this._widthFraction = widthFraction;
+        this._sourceBox.queue_relayout();
+    },
+
+    get widthFraction() {
+        return this._widthFraction;
+    }
+};
+
 function MessageTray() {
     this._init();
 }
@@ -430,9 +570,8 @@ MessageTray.prototype = {
         this.actor.add_actor(this._summaryNotificationBin);
         this._summaryNotificationBin.lower_bottom();
         this._summaryNotificationBin.hide();
-        this._summaryNotificationBin.connect('notify::hover', Lang.bind(this, this._onSummaryNotificationHoverChanged));
         this._summaryNotification = null;
-        this._hoverSource = null;
+        this._clickedSummaryItem = null;
 
         this._trayState = State.HIDDEN;
         this._trayLeftTimeoutId = 0;
@@ -467,8 +606,8 @@ MessageTray.prototype = {
                 this._updateState();
             }));
 
-        this._sources = {};
-        this._icons = {};
+        this._summaryItems = {};
+        this._longestSummaryItem = null;
     },
 
     _setSizePosition: function() {
@@ -485,7 +624,7 @@ MessageTray.prototype = {
     },
 
     contains: function(source) {
-        return this._sources.hasOwnProperty(source.id);
+        return this._summaryItems.hasOwnProperty(source.id);
     },
 
     add: function(source) {
@@ -494,23 +633,33 @@ MessageTray.prototype = {
             return;
         }
 
-        let iconBox = new St.Clickable({ style_class: 'summary-icon',
-                                         reactive: true });
-        iconBox.child = source.createIcon(ICON_SIZE);
-        this._summary.insert_actor(iconBox, 0);
+        let minTitleWidth = (this._longestSummaryItem ? this._longestSummaryItem.getTitleNaturalWidth() : 0);
+        let summaryItem = new SummaryItem(source, minTitleWidth);
+
+        this._summary.insert_actor(summaryItem.actor, 0);
         this._summaryNeedsToBeShown = true;
-        this._icons[source.id] = iconBox;
-        this._sources[source.id] = source;
+
+        let newItemTitleWidth = summaryItem.getTitleNaturalWidth();
+        if (newItemTitleWidth > minTitleWidth) {
+            for (sourceId in this._summaryItems) {
+                this._summaryItems[sourceId].setMinTitleWidth(newItemTitleWidth);
+            }
+            summaryItem.setMinTitleWidth(newItemTitleWidth);
+            this._longestSummaryItem = summaryItem;
+        }
+
+        this._summaryItems[source.id] = summaryItem;
 
         source.connect('notify', Lang.bind(this, this._onNotify));
 
-        iconBox.connect('notify::hover', Lang.bind(this,
+        summaryItem.actor.connect('notify::hover', Lang.bind(this,
             function () {
-                this._onSourceHoverChanged(source, iconBox.hover);
+                this._onSummaryItemHoverChanged(summaryItem);
             }));
-        iconBox.connect('clicked', Lang.bind(this,
+
+        summaryItem.actor.connect('clicked', Lang.bind(this,
             function () {
-                source.clicked();
+                this._onSummaryItemClicked(summaryItem);
             }));
 
         source.connect('destroy', Lang.bind(this,
@@ -531,13 +680,28 @@ MessageTray.prototype = {
         }
         this._notificationQueue = newNotificationQueue;
 
-        this._summary.remove_actor(this._icons[source.id]);
+        this._summary.remove_actor(this._summaryItems[source.id].actor);
         if (this._summary.get_children().length > 0)
             this._summaryNeedsToBeShown = true;
         else
             this._summaryNeedsToBeShown = false;
-        delete this._icons[source.id];
-        delete this._sources[source.id];
+
+        delete this._summaryItems[source.id];
+        if (this._longestSummaryItem.source == source) {
+
+            let maxTitleWidth = 0;
+            this._longestSummaryItem = null;
+            for (sourceId in this._summaryItems) {
+                let summaryItem = this._summaryItems[sourceId];
+                if (summaryItem.getTitleNaturalWidth() > maxTitleWidth) {
+                    maxTitleWidth = summaryItem.getTitleNaturalWidth();
+                    this._longestSummaryItem = summaryItem;
+                }
+            }
+            for (sourceId in this._summaryItems) {
+                this._summaryItems[sourceId].setMinTitleWidth(maxTitleWidth);
+            }
+        }
 
         let needUpdate = false;
 
@@ -549,8 +713,8 @@ MessageTray.prototype = {
             this._notificationRemoved = true;
             needUpdate = true;
         }
-        if (this._hoverSource == source) {
-            this._hoverSource = null;
+        if (this._clickedSummaryItem && this._clickedSummaryItem.source == source) {
+            this._clickedSummaryItem = null;
             needUpdate = true;
         }
 
@@ -559,9 +723,9 @@ MessageTray.prototype = {
     },
 
     removeSourceByApp: function(app) {
-        for (let source in this._sources)
-            if (this._sources[source].app == app)
-                this.removeSource(this._sources[source]);
+        for (let sourceId in this._summaryItems)
+            if (this._summaryItems[sourceId].source.app == app)
+                this.removeSource(this._summaryItems[sourceId].source);
     },
 
     removeNotification: function(notification) {
@@ -581,7 +745,9 @@ MessageTray.prototype = {
     },
 
     getSource: function(id) {
-        return this._sources[id];
+        if (this._summaryItems[id])
+            return this._summaryItems[id].source;
+        return null;
     },
 
     _getNotification: function(id, source) {
@@ -626,37 +792,20 @@ MessageTray.prototype = {
         this._updateState();
     },
 
-    _onSourceHoverChanged: function(source, hover) {
-        if (!source.notification)
-            return;
-
-        if (this._summaryNotificationTimeoutId != 0) {
-            Mainloop.source_remove(this._summaryNotificationTimeoutId);
-            this._summaryNotificationTimeoutId = 0;
-        }
-
-        if (hover) {
-            this._hoverSource = source;
-            this._updateState();
-        } else if (this._hoverSource == source) {
-            let timeout = HIDE_TIMEOUT * 1000;
-            this._summaryNotificationTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._onSourceHoverChangedTimeout, source));
-        }
+    _onSummaryItemHoverChanged: function(summaryItem) {
+        if (summaryItem.actor.hover)
+            summaryItem.expand();
+        else
+            summaryItem.collapse();
     },
 
-    _onSourceHoverChangedTimeout: function(source) {
-        this._summaryNotificationTimeoutId = 0;
-        if (this._hoverSource == source) {
-            this._hoverSource = null;
-            this._updateState();
-        }
-    },
+    _onSummaryItemClicked: function(summaryItem) {
+        if (!this._clickedSummaryItem || this._clickedSummaryItem != summaryItem)
+            this._clickedSummaryItem = summaryItem
+        else
+            this._clickedSummaryItem = null;
 
-    _onSummaryNotificationHoverChanged: function() {
-        if (!this._summaryNotification)
-            return;
-        this._onSourceHoverChanged(this._summaryNotification.source,
-                                   this._summaryNotificationBin.hover);
+        this._updateState();
     },
 
     _onSummaryHoverChanged: function() {
@@ -731,12 +880,12 @@ MessageTray.prototype = {
         }
 
         // Summary notification
-        let haveSummaryNotification = this._hoverSource != null;
+        let haveSummaryNotification = this._clickedSummaryItem != null;
         let summaryNotificationIsMainNotification = (haveSummaryNotification &&
-                                                     this._hoverSource.notification == this._notification);
+                                                     this._clickedSummaryItem.source.notification == this._notification);
         let canShowSummaryNotification = this._summaryState == State.SHOWN;
         let wrongSummaryNotification = (haveSummaryNotification &&
-                                        this._summaryNotification != this._hoverSource.notification);
+                                        this._summaryNotification != this._clickedSummaryItem.source.notification);
 
         if (this._summaryNotificationState == State.HIDDEN) {
             if (haveSummaryNotification && !summaryNotificationIsMainNotification && canShowSummaryNotification)
@@ -929,7 +1078,7 @@ MessageTray.prototype = {
     },
 
     _showSummaryNotification: function() {
-        this._summaryNotification = this._hoverSource.notification;
+        this._summaryNotification = this._clickedSummaryItem.source.notification;
 
         let index = this._notificationQueue.indexOf(this._summaryNotification);
         if (index != -1)
@@ -962,6 +1111,9 @@ MessageTray.prototype = {
     },
 
     _hideSummaryNotification: function() {
+        // Unset this._clickedSummaryItem if we are no longer showing the summary
+        if (this._summaryState != State.SHOWN)
+            this._clickedSummaryItem = null;
         this._summaryNotification.popIn();
 
         this._tween(this._summaryNotificationBin, '_summaryNotificationState', State.HIDDEN,
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
index 300cdcc..da3c929 100644
--- a/js/ui/notificationDaemon.js
+++ b/js/ui/notificationDaemon.js
@@ -80,6 +80,15 @@ const rewriteRules = {
           replacement: '$2 &lt;$1&gt;' }
     ]
 };
+
+// The notification spec stipulates using formal names for the appName the applications
+// pass in. However, not all applications do that. Here is a list of the offenders we
+// encountered so far.
+const appNameMap = {
+    'evolution-mail-notification': 'Evolution Mail',
+    'rhythmbox': 'Rhythmbox'
+};
+
 function NotificationDaemon() {
     this._init();
 }
@@ -157,7 +166,8 @@ NotificationDaemon.prototype = {
         // from this app or if all notifications from this app have
         // been acknowledged.
         if (source == null) {
-            source = new Source(this._sourceId(appName), icon, hints);
+            let title = appNameMap[appName] || appName;
+            source = new Source(this._sourceId(appName), title, icon, hints);
             Main.messageTray.add(source);
 
             source.connect('clicked', Lang.bind(this,
@@ -278,15 +288,15 @@ NotificationDaemon.prototype = {
 
 DBus.conformExport(NotificationDaemon.prototype, NotificationDaemonIface);
 
-function Source(sourceId, icon, hints) {
-    this._init(sourceId, icon, hints);
+function Source(sourceId, title, icon, hints) {
+    this._init(sourceId, title, icon, hints);
 }
 
 Source.prototype = {
     __proto__:  MessageTray.Source.prototype,
 
-    _init: function(sourceId, icon, hints) {
-        MessageTray.Source.prototype._init.call(this, sourceId);
+    _init: function(sourceId, title, icon, hints) {
+        MessageTray.Source.prototype._init.call(this, sourceId, title);
 
         this.app = null;
         this._openAppRequested = false;
diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js
index 70f52ce..5f72f8a 100644
--- a/js/ui/telepathyClient.js
+++ b/js/ui/telepathyClient.js
@@ -447,7 +447,7 @@ Source.prototype = {
     __proto__:  MessageTray.Source.prototype,
 
     _init: function(accountPath, connPath, channelPath, targetHandle, targetHandleType, targetId) {
-        MessageTray.Source.prototype._init.call(this, targetId);
+        MessageTray.Source.prototype._init.call(this, targetId, targetId);
 
         this._accountPath = accountPath;
 
@@ -460,13 +460,12 @@ Source.prototype = {
         this._targetHandleType = targetHandleType;
         this._targetId = targetId;
 
-        this.name = this._targetId;
         if (targetHandleType == Telepathy.HandleType.CONTACT) {
             let aliasing = new Telepathy.ConnectionAliasing(DBus.session, connName, connPath);
             aliasing.RequestAliasesRemote([this._targetHandle], Lang.bind(this,
                 function (aliases, err) {
                     if (aliases && aliases.length)
-                        this.name = aliases[0];
+                        this.title = aliases[0];
                 }));
         }
 
@@ -579,7 +578,7 @@ Notification.prototype = {
     __proto__:  MessageTray.Notification.prototype,
 
     _init: function(id, source) {
-        MessageTray.Notification.prototype._init.call(this, id, source, source.name);
+        MessageTray.Notification.prototype._init.call(this, id, source, source.title);
         this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
 
         this._responseEntry = new St.Entry({ style_class: 'chat-response' });
@@ -594,7 +593,7 @@ Notification.prototype = {
         if (asTitle)
             this.update(text);
         else
-            this.update(this.source.name, text);
+            this.update(this.source.title, text);
         this._append(text, 'chat-received');
     },
 
diff --git a/js/ui/windowAttentionHandler.js b/js/ui/windowAttentionHandler.js
index c4aba46..d333c26 100644
--- a/js/ui/windowAttentionHandler.js
+++ b/js/ui/windowAttentionHandler.js
@@ -92,7 +92,7 @@ Source.prototype = {
     __proto__ : MessageTray.Source.prototype,
 
     _init: function(sourceId, app, window) {
-        MessageTray.Source.prototype._init.call(this, sourceId);
+        MessageTray.Source.prototype._init.call(this, sourceId, app.get_name());
         this._window = window;
         this._app = app;
     },



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