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
This commit is contained in:
Marina Zhurakhinskaya 2010-06-23 15:20:39 -04:00
parent 7c4d4b8695
commit 83689e494c
5 changed files with 250 additions and 74 deletions

View File

@ -891,12 +891,27 @@ StTooltip {
* pseudo-class we could fix that... * pseudo-class we could fix that...
*/ */
#summary-mode { #summary-mode {
spacing: 6px; spacing: 4px;
padding: 2px 0px 0px 4px; padding: 2px 0px 0px 4px;
} }
.summary-icon { .summary-source-button {
padding: 0px 4px 2px 0px; 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 { .calendar-calendarweek {

View File

@ -20,6 +20,8 @@ const HIDE_TIMEOUT = 0.2;
const ICON_SIZE = 24; const ICON_SIZE = 24;
const MAX_SOURCE_TITLE_WIDTH = 180;
const State = { const State = {
HIDDEN: 0, HIDDEN: 0,
SHOWING: 1, SHOWING: 1,
@ -352,14 +354,14 @@ Notification.prototype = {
}; };
Signals.addSignalMethods(Notification.prototype); Signals.addSignalMethods(Notification.prototype);
function Source(id, createIcon) { function Source(id, title, createIcon) {
this._init(id, createIcon); this._init(id, title, createIcon);
} }
Source.prototype = { Source.prototype = {
_init: function(id, createIcon) { _init: function(id, title, createIcon) {
this.id = id; this.id = id;
this.text = null; this.title = title;
if (createIcon) if (createIcon)
this.createIcon = createIcon; this.createIcon = createIcon;
}, },
@ -397,6 +399,144 @@ Source.prototype = {
}; };
Signals.addSignalMethods(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() { function MessageTray() {
this._init(); this._init();
} }
@ -430,9 +570,8 @@ MessageTray.prototype = {
this.actor.add_actor(this._summaryNotificationBin); this.actor.add_actor(this._summaryNotificationBin);
this._summaryNotificationBin.lower_bottom(); this._summaryNotificationBin.lower_bottom();
this._summaryNotificationBin.hide(); this._summaryNotificationBin.hide();
this._summaryNotificationBin.connect('notify::hover', Lang.bind(this, this._onSummaryNotificationHoverChanged));
this._summaryNotification = null; this._summaryNotification = null;
this._hoverSource = null; this._clickedSummaryItem = null;
this._trayState = State.HIDDEN; this._trayState = State.HIDDEN;
this._trayLeftTimeoutId = 0; this._trayLeftTimeoutId = 0;
@ -467,8 +606,8 @@ MessageTray.prototype = {
this._updateState(); this._updateState();
})); }));
this._sources = {}; this._summaryItems = {};
this._icons = {}; this._longestSummaryItem = null;
}, },
_setSizePosition: function() { _setSizePosition: function() {
@ -485,7 +624,7 @@ MessageTray.prototype = {
}, },
contains: function(source) { contains: function(source) {
return this._sources.hasOwnProperty(source.id); return this._summaryItems.hasOwnProperty(source.id);
}, },
add: function(source) { add: function(source) {
@ -494,23 +633,33 @@ MessageTray.prototype = {
return; return;
} }
let iconBox = new St.Clickable({ style_class: 'summary-icon', let minTitleWidth = (this._longestSummaryItem ? this._longestSummaryItem.getTitleNaturalWidth() : 0);
reactive: true }); let summaryItem = new SummaryItem(source, minTitleWidth);
iconBox.child = source.createIcon(ICON_SIZE);
this._summary.insert_actor(iconBox, 0); this._summary.insert_actor(summaryItem.actor, 0);
this._summaryNeedsToBeShown = true; 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)); source.connect('notify', Lang.bind(this, this._onNotify));
iconBox.connect('notify::hover', Lang.bind(this, summaryItem.actor.connect('notify::hover', Lang.bind(this,
function () { function () {
this._onSourceHoverChanged(source, iconBox.hover); this._onSummaryItemHoverChanged(summaryItem);
})); }));
iconBox.connect('clicked', Lang.bind(this,
summaryItem.actor.connect('clicked', Lang.bind(this,
function () { function () {
source.clicked(); this._onSummaryItemClicked(summaryItem);
})); }));
source.connect('destroy', Lang.bind(this, source.connect('destroy', Lang.bind(this,
@ -531,13 +680,28 @@ MessageTray.prototype = {
} }
this._notificationQueue = newNotificationQueue; 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) if (this._summary.get_children().length > 0)
this._summaryNeedsToBeShown = true; this._summaryNeedsToBeShown = true;
else else
this._summaryNeedsToBeShown = false; 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; let needUpdate = false;
@ -549,8 +713,8 @@ MessageTray.prototype = {
this._notificationRemoved = true; this._notificationRemoved = true;
needUpdate = true; needUpdate = true;
} }
if (this._hoverSource == source) { if (this._clickedSummaryItem && this._clickedSummaryItem.source == source) {
this._hoverSource = null; this._clickedSummaryItem = null;
needUpdate = true; needUpdate = true;
} }
@ -559,9 +723,9 @@ MessageTray.prototype = {
}, },
removeSourceByApp: function(app) { removeSourceByApp: function(app) {
for (let source in this._sources) for (let sourceId in this._summaryItems)
if (this._sources[source].app == app) if (this._summaryItems[sourceId].source.app == app)
this.removeSource(this._sources[source]); this.removeSource(this._summaryItems[sourceId].source);
}, },
removeNotification: function(notification) { removeNotification: function(notification) {
@ -581,7 +745,9 @@ MessageTray.prototype = {
}, },
getSource: function(id) { getSource: function(id) {
return this._sources[id]; if (this._summaryItems[id])
return this._summaryItems[id].source;
return null;
}, },
_getNotification: function(id, source) { _getNotification: function(id, source) {
@ -626,37 +792,20 @@ MessageTray.prototype = {
this._updateState(); this._updateState();
}, },
_onSourceHoverChanged: function(source, hover) { _onSummaryItemHoverChanged: function(summaryItem) {
if (!source.notification) if (summaryItem.actor.hover)
return; summaryItem.expand();
else
if (this._summaryNotificationTimeoutId != 0) { summaryItem.collapse();
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));
}
}, },
_onSourceHoverChangedTimeout: function(source) { _onSummaryItemClicked: function(summaryItem) {
this._summaryNotificationTimeoutId = 0; if (!this._clickedSummaryItem || this._clickedSummaryItem != summaryItem)
if (this._hoverSource == source) { this._clickedSummaryItem = summaryItem
this._hoverSource = null; else
this._updateState(); this._clickedSummaryItem = null;
}
},
_onSummaryNotificationHoverChanged: function() { this._updateState();
if (!this._summaryNotification)
return;
this._onSourceHoverChanged(this._summaryNotification.source,
this._summaryNotificationBin.hover);
}, },
_onSummaryHoverChanged: function() { _onSummaryHoverChanged: function() {
@ -731,12 +880,12 @@ MessageTray.prototype = {
} }
// Summary notification // Summary notification
let haveSummaryNotification = this._hoverSource != null; let haveSummaryNotification = this._clickedSummaryItem != null;
let summaryNotificationIsMainNotification = (haveSummaryNotification && let summaryNotificationIsMainNotification = (haveSummaryNotification &&
this._hoverSource.notification == this._notification); this._clickedSummaryItem.source.notification == this._notification);
let canShowSummaryNotification = this._summaryState == State.SHOWN; let canShowSummaryNotification = this._summaryState == State.SHOWN;
let wrongSummaryNotification = (haveSummaryNotification && let wrongSummaryNotification = (haveSummaryNotification &&
this._summaryNotification != this._hoverSource.notification); this._summaryNotification != this._clickedSummaryItem.source.notification);
if (this._summaryNotificationState == State.HIDDEN) { if (this._summaryNotificationState == State.HIDDEN) {
if (haveSummaryNotification && !summaryNotificationIsMainNotification && canShowSummaryNotification) if (haveSummaryNotification && !summaryNotificationIsMainNotification && canShowSummaryNotification)
@ -929,7 +1078,7 @@ MessageTray.prototype = {
}, },
_showSummaryNotification: function() { _showSummaryNotification: function() {
this._summaryNotification = this._hoverSource.notification; this._summaryNotification = this._clickedSummaryItem.source.notification;
let index = this._notificationQueue.indexOf(this._summaryNotification); let index = this._notificationQueue.indexOf(this._summaryNotification);
if (index != -1) if (index != -1)
@ -962,6 +1111,9 @@ MessageTray.prototype = {
}, },
_hideSummaryNotification: function() { _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._summaryNotification.popIn();
this._tween(this._summaryNotificationBin, '_summaryNotificationState', State.HIDDEN, this._tween(this._summaryNotificationBin, '_summaryNotificationState', State.HIDDEN,

View File

@ -80,6 +80,15 @@ const rewriteRules = {
replacement: '$2 <$1>' } 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() { function NotificationDaemon() {
this._init(); this._init();
} }
@ -157,7 +166,8 @@ NotificationDaemon.prototype = {
// from this app or if all notifications from this app have // from this app or if all notifications from this app have
// been acknowledged. // been acknowledged.
if (source == null) { 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); Main.messageTray.add(source);
source.connect('clicked', Lang.bind(this, source.connect('clicked', Lang.bind(this,
@ -278,15 +288,15 @@ NotificationDaemon.prototype = {
DBus.conformExport(NotificationDaemon.prototype, NotificationDaemonIface); DBus.conformExport(NotificationDaemon.prototype, NotificationDaemonIface);
function Source(sourceId, icon, hints) { function Source(sourceId, title, icon, hints) {
this._init(sourceId, icon, hints); this._init(sourceId, title, icon, hints);
} }
Source.prototype = { Source.prototype = {
__proto__: MessageTray.Source.prototype, __proto__: MessageTray.Source.prototype,
_init: function(sourceId, icon, hints) { _init: function(sourceId, title, icon, hints) {
MessageTray.Source.prototype._init.call(this, sourceId); MessageTray.Source.prototype._init.call(this, sourceId, title);
this.app = null; this.app = null;
this._openAppRequested = false; this._openAppRequested = false;

View File

@ -447,7 +447,7 @@ Source.prototype = {
__proto__: MessageTray.Source.prototype, __proto__: MessageTray.Source.prototype,
_init: function(accountPath, connPath, channelPath, targetHandle, targetHandleType, targetId) { _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; this._accountPath = accountPath;
@ -460,13 +460,12 @@ Source.prototype = {
this._targetHandleType = targetHandleType; this._targetHandleType = targetHandleType;
this._targetId = targetId; this._targetId = targetId;
this.name = this._targetId;
if (targetHandleType == Telepathy.HandleType.CONTACT) { if (targetHandleType == Telepathy.HandleType.CONTACT) {
let aliasing = new Telepathy.ConnectionAliasing(DBus.session, connName, connPath); let aliasing = new Telepathy.ConnectionAliasing(DBus.session, connName, connPath);
aliasing.RequestAliasesRemote([this._targetHandle], Lang.bind(this, aliasing.RequestAliasesRemote([this._targetHandle], Lang.bind(this,
function (aliases, err) { function (aliases, err) {
if (aliases && aliases.length) if (aliases && aliases.length)
this.name = aliases[0]; this.title = aliases[0];
})); }));
} }
@ -579,7 +578,7 @@ Notification.prototype = {
__proto__: MessageTray.Notification.prototype, __proto__: MessageTray.Notification.prototype,
_init: function(id, source) { _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.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
this._responseEntry = new St.Entry({ style_class: 'chat-response' }); this._responseEntry = new St.Entry({ style_class: 'chat-response' });
@ -594,7 +593,7 @@ Notification.prototype = {
if (asTitle) if (asTitle)
this.update(text); this.update(text);
else else
this.update(this.source.name, text); this.update(this.source.title, text);
this._append(text, 'chat-received'); this._append(text, 'chat-received');
}, },

View File

@ -92,7 +92,7 @@ Source.prototype = {
__proto__ : MessageTray.Source.prototype, __proto__ : MessageTray.Source.prototype,
_init: function(sourceId, app, window) { _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._window = window;
this._app = app; this._app = app;
}, },