MessageTray: fix wobbling during summaryitem animation

Redo the way that the summary item expand/collapse animation works so
that the items all resize in unison so that when moving from one to
another, the summary area as a whole stays a constant width rather
than wobbling slightly.

(Also rename all references to the "minimum" summary item title width,
since it's not a minimum, it's just the width.)

https://bugzilla.gnome.org/show_bug.cgi?id=630546
This commit is contained in:
Dan Winship 2010-09-24 16:04:56 -04:00
parent bd250e188b
commit 0215a449ad
2 changed files with 131 additions and 128 deletions

View File

@ -959,6 +959,10 @@ StTooltip {
* icons, because then the summary would be 0x0 when there were no * icons, because then the summary would be 0x0 when there were no
* icons in it, and so you wouldn't be able to hover over it to * icons in it, and so you wouldn't be able to hover over it to
* activate it. * activate it.
*
* Also, the spacing between a summary-source's icon and title is
* actually specified as padding-left in source-title, because we
* want the spacing to collapse along with the title.
*/ */
#summary-mode { #summary-mode {
padding: 2px 0px 0px 4px; padding: 2px 0px 0px 4px;
@ -966,7 +970,6 @@ StTooltip {
} }
.summary-source { .summary-source {
spacing: 4px;
} }
.summary-source-button { .summary-source-button {
@ -982,6 +985,7 @@ StTooltip {
font: 12px sans-serif; font: 12px sans-serif;
font-weight: bold; font-weight: bold;
color: white; color: white;
padding-left: 4px;
} }
.calendar-calendarweek { .calendar-calendarweek {

View File

@ -700,141 +700,56 @@ Source.prototype = {
}; };
Signals.addSignalMethods(Source.prototype); Signals.addSignalMethods(Source.prototype);
function SummaryItem(source, minTitleWidth) { function SummaryItem(source) {
this._init(source, minTitleWidth); this._init(source);
} }
SummaryItem.prototype = { SummaryItem.prototype = {
_init: function(source, minTitleWidth) { _init: function(source) {
this.source = source; 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', this.actor = new St.Button({ style_class: 'summary-source-button',
reactive: true, reactive: true,
track_hover: true }); track_hover: true });
this._sourceBox = new Shell.GenericContainer({ style_class: 'summary-source', this._sourceBox = new St.BoxLayout({ 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.getSummaryIcon(); this._sourceIcon = source.getSummaryIcon();
this._sourceTitleBin = new St.Bin({ y_align: St.Align.MIDDLE, x_fill: true }); this._sourceTitleBin = new St.Bin({ y_align: St.Align.MIDDLE,
x_fill: true,
clip_to_allocation: true });
this._sourceTitle = new St.Label({ style_class: 'source-title', this._sourceTitle = new St.Label({ style_class: 'source-title',
text: source.title }); text: source.title });
this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
this._sourceTitleBin.child = this._sourceTitle; this._sourceTitleBin.child = this._sourceTitle;
this._sourceTitleBin.width = 0;
this._sourceBox.add_actor(this._sourceIcon); this._sourceBox.add_actor(this._sourceIcon);
this._sourceBox.add_actor(this._sourceTitleBin); this._sourceBox.add_actor(this._sourceTitleBin, { expand: true });
this._widthFraction = 0;
this.actor.child = this._sourceBox; this.actor.child = this._sourceBox;
}, },
// getTitleNaturalWidth, getTitleWidth, and setTitleWidth include
// the spacing between the icon and title (which is actually
// _sourceTitle's padding-left) as part of the width.
getTitleNaturalWidth: function() { getTitleNaturalWidth: function() {
let [sourceTitleBinMinWidth, sourceTitleBinNaturalWidth] = let [minWidth, naturalWidth] = this._sourceTitle.get_preferred_width(-1);
this._sourceTitleBin.get_preferred_width(-1);
return Math.min(sourceTitleBinNaturalWidth, MAX_SOURCE_TITLE_WIDTH); return Math.min(naturalWidth, MAX_SOURCE_TITLE_WIDTH);
}, },
setMinTitleWidth: function(minTitleWidth) { getTitleWidth: function() {
this._minTitleWidth = minTitleWidth; return this._sourceTitleBin.width;
}, },
_getPreferredWidth: function(actor, forHeight, alloc) { setTitleWidth: function(width) {
let [found, spacing] = this._sourceBox.get_theme_node().get_length('spacing', false); width = Math.round(width);
if (!found) if (width != this._sourceTitleBin.width)
spacing = 0; this._sourceTitleBin.width = width;
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) { setEllipsization: function(mode) {
let [sourceIconMinHeight, sourceIconNaturalHeight] = this._sourceIcon.get_preferred_height(forWidth); this._sourceTitle.clutter_text.ellipsize = mode;
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;
} }
}; };
@ -873,6 +788,14 @@ MessageTray.prototype = {
this._summaryNotificationBin.hide(); this._summaryNotificationBin.hide();
this._summaryNotification = null; this._summaryNotification = null;
this._clickedSummaryItem = null; this._clickedSummaryItem = null;
this._expandedSummaryItem = null;
this._summaryItemTitleWidth = 0;
// To simplify the summary item animation code, we pretend
// that there's an invisible SummaryItem to the left of the
// leftmost real summary item, and that it's expanded when all
// of the other items are collapsed.
this._imaginarySummaryItemTitleWidth = 0;
this._trayState = State.HIDDEN; this._trayState = State.HIDDEN;
this._locked = false; this._locked = false;
@ -958,17 +881,15 @@ MessageTray.prototype = {
return; return;
} }
let minTitleWidth = (this._longestSummaryItem ? this._longestSummaryItem.getTitleNaturalWidth() : 0); let summaryItem = new SummaryItem(source);
let summaryItem = new SummaryItem(source, minTitleWidth);
this._summary.insert_actor(summaryItem.actor, 0); this._summary.insert_actor(summaryItem.actor, 0);
let newItemTitleWidth = summaryItem.getTitleNaturalWidth(); let titleWidth = summaryItem.getTitleNaturalWidth();
if (newItemTitleWidth > minTitleWidth) { if (titleWidth > this._summaryItemTitleWidth) {
for (let i = 0; i < this._summaryItems.length; i++) { this._summaryItemTitleWidth = titleWidth;
this._summaryItems[i].setMinTitleWidth(newItemTitleWidth); if (!this._expandedSummaryItem)
} this._imaginarySummaryItemTitleWidth = titleWidth;
summaryItem.setMinTitleWidth(newItemTitleWidth);
this._longestSummaryItem = summaryItem; this._longestSummaryItem = summaryItem;
} }
@ -1020,19 +941,20 @@ MessageTray.prototype = {
this._summaryItems.splice(index, 1); this._summaryItems.splice(index, 1);
if (this._longestSummaryItem.source == source) { if (this._longestSummaryItem.source == source) {
let newTitleWidth = 0;
let maxTitleWidth = 0;
this._longestSummaryItem = null; this._longestSummaryItem = null;
for (let i = 0; i < this._summaryItems.length; i++) { for (let i = 0; i < this._summaryItems.length; i++) {
let summaryItem = this._summaryItems[i]; let summaryItem = this._summaryItems[i];
if (summaryItem.getTitleNaturalWidth() > maxTitleWidth) { let titleWidth = summaryItem.getTitleNaturalWidth();
maxTitleWidth = summaryItem.getTitleNaturalWidth(); if (titleWidth > newTitleWidth) {
newTitleWidth = titleWidth;
this._longestSummaryItem = summaryItem; this._longestSummaryItem = summaryItem;
} }
} }
for (let i = 0; i < this._summaryItems.length; i++) {
this._summaryItems[i].setMinTitleWidth(maxTitleWidth); this._summaryItemTitleWidth = newTitleWidth;
} if (!this._expandedSummaryItem)
this._imaginarySummaryItemTitleWidth = newTitleWidth;
} }
let needUpdate = false; let needUpdate = false;
@ -1108,10 +1030,87 @@ MessageTray.prototype = {
}, },
_onSummaryItemHoverChanged: function(summaryItem) { _onSummaryItemHoverChanged: function(summaryItem) {
if (summaryItem.actor.hover) // We can't just animate individual summary items as the
summaryItem.expand(); // pointer moves in and out of them, because if they don't
// move in sync you get weird-looking wobbling. So whenever
// there's a change, we have to re-tween the entire summary
// area.
if (summaryItem.actor.hover) {
if (summaryItem == this._expandedSummaryItem)
return;
this._expandedSummaryItem = summaryItem;
} else {
if (summaryItem != this._expandedSummaryItem)
return;
this._expandedSummaryItem = null;
// Turn off ellipsization while collapsing; it looks better
summaryItem.setEllipsization(Pango.EllipsizeMode.NONE);
}
// We tween on a "_expandedSummaryItemTitleWidth" pseudo-property
// that represents the current title width of the
// expanded/expanding item, or the width of the imaginary
// invisible item if we're collapsing everything.
Tweener.addTween(this,
{ _expandedSummaryItemTitleWidth: this._summaryItemTitleWidth,
time: ANIMATION_TIME,
transition: 'easeOutQuad',
onComplete: this._expandSummaryItemCompleted,
onCompleteScope: this });
},
get _expandedSummaryItemTitleWidth() {
if (this._expandedSummaryItem)
return this._expandedSummaryItem.getTitleWidth();
else else
summaryItem.collapse(); return this._imaginarySummaryItemTitleWidth;
},
set _expandedSummaryItemTitleWidth(expansion) {
// Expand the expanding item to its new width
if (this._expandedSummaryItem)
this._expandedSummaryItem.setTitleWidth(expansion);
else
this._imaginarySummaryItemTitleWidth = expansion;
// Figure out how much space the other items are currently
// using, and how much they need to be shrunk to keep the
// total width (including the width of the imaginary item)
// constant.
let excess = this._summaryItemTitleWidth - expansion;
let oldExcess = 0, shrinkage;
if (excess) {
for (let i = 0; i < this._summaryItems.length; i++) {
if (this._summaryItems[i] != this._expandedSummaryItem)
oldExcess += this._summaryItems[i].getTitleWidth();
}
if (this._expandedSummaryItem)
oldExcess += this._imaginarySummaryItemTitleWidth;
}
if (excess && oldExcess)
shrinkage = excess / oldExcess;
else
shrinkage = 0;
// Now shrink each one proportionately
for (let i = 0; i < this._summaryItems.length; i++) {
if (this._summaryItems[i] == this._expandedSummaryItem)
continue;
let width = this._summaryItems[i].getTitleWidth();
this._summaryItems[i].setTitleWidth(width * shrinkage);
}
if (this._expandedSummaryItem)
this._imaginarySummaryItemTitleWidth *= shrinkage;
},
_expandSummaryItemCompleted: function() {
if (this._expandedSummaryItem)
this._expandedSummaryItem.setEllipsization(Pango.EllipsizeMode.END);
}, },
_onSummaryItemClicked: function(summaryItem) { _onSummaryItemClicked: function(summaryItem) {