From 83689e494cd1e4f1e417718581d826ce9add153f Mon Sep 17 00:00:00 2001 From: Marina Zhurakhinskaya Date: Wed, 23 Jun 2010 15:20:39 -0400 Subject: [PATCH] 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 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; },