diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index b3abbe06a..66698457c 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 aaba10b23..90e9a78ed 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 300cdcc13..da3c9297c 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -80,6 +80,15 @@ const rewriteRules = { replacement: '$2 <$1>' } ] }; + +// 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 70f52ced7..5f72f8abd 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 c4aba46ff..d333c268e 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; },