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:
parent
bd250e188b
commit
0215a449ad
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user