a5324462c3
Add a short-circuit when the size doesn't change. Taken from part
of 26580f8f2c
(Giovanni Campagna). This greatly reduces the reentrancy
of recreating icons that seems to be responsible for many crashers,
though the exact mechanism for the crashes isn't clear. See:
https://bugzilla.gnome.org/show_bug.cgi?id=648450
https://bugzilla.redhat.com/show_bug.cgi?id=707652
https://bugzilla.redhat.com/show_bug.cgi?id=781780
333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
|
|
const Lang = imports.lang;
|
|
const Params = imports.misc.params;
|
|
|
|
const ICON_SIZE = 48;
|
|
|
|
|
|
function BaseIcon(label, createIcon) {
|
|
this._init(label, createIcon);
|
|
}
|
|
|
|
BaseIcon.prototype = {
|
|
_init : function(label, params) {
|
|
params = Params.parse(params, { createIcon: null,
|
|
setSizeManually: false,
|
|
showLabel: true });
|
|
this.actor = new St.Bin({ style_class: 'overview-icon',
|
|
x_fill: true,
|
|
y_fill: true });
|
|
this.actor._delegate = this;
|
|
this.actor.connect('style-changed',
|
|
Lang.bind(this, this._onStyleChanged));
|
|
|
|
this._spacing = 0;
|
|
|
|
let box = new Shell.GenericContainer();
|
|
box.connect('allocate', Lang.bind(this, this._allocate));
|
|
box.connect('get-preferred-width',
|
|
Lang.bind(this, this._getPreferredWidth));
|
|
box.connect('get-preferred-height',
|
|
Lang.bind(this, this._getPreferredHeight));
|
|
this.actor.set_child(box);
|
|
|
|
this.iconSize = ICON_SIZE;
|
|
this._iconBin = new St.Bin();
|
|
|
|
box.add_actor(this._iconBin);
|
|
|
|
if (params.showLabel) {
|
|
this.label = new St.Label({ text: label });
|
|
box.add_actor(this.label);
|
|
} else {
|
|
this.label = null;
|
|
}
|
|
|
|
if (params.createIcon)
|
|
this.createIcon = params.createIcon;
|
|
this._setSizeManually = params.setSizeManually;
|
|
|
|
this.icon = null;
|
|
},
|
|
|
|
_allocate: function(actor, box, flags) {
|
|
let availWidth = box.x2 - box.x1;
|
|
let availHeight = box.y2 - box.y1;
|
|
|
|
let iconSize = availHeight;
|
|
|
|
let [iconMinHeight, iconNatHeight] = this._iconBin.get_preferred_height(-1);
|
|
let [iconMinWidth, iconNatWidth] = this._iconBin.get_preferred_width(-1);
|
|
let preferredHeight = iconNatHeight;
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
|
|
if (this.label) {
|
|
let [labelMinHeight, labelNatHeight] = this.label.get_preferred_height(-1);
|
|
preferredHeight += this._spacing + labelNatHeight;
|
|
|
|
let labelHeight = availHeight >= preferredHeight ? labelNatHeight
|
|
: labelMinHeight;
|
|
iconSize -= this._spacing + labelHeight;
|
|
|
|
childBox.x1 = 0;
|
|
childBox.x2 = availWidth;
|
|
childBox.y1 = iconSize + this._spacing;
|
|
childBox.y2 = childBox.y1 + labelHeight;
|
|
this.label.allocate(childBox, flags);
|
|
}
|
|
|
|
childBox.x1 = Math.floor((availWidth - iconNatWidth) / 2);
|
|
childBox.y1 = Math.floor((iconSize - iconNatHeight) / 2);
|
|
childBox.x2 = childBox.x1 + iconNatWidth;
|
|
childBox.y2 = childBox.y1 + iconNatHeight;
|
|
this._iconBin.allocate(childBox, flags);
|
|
},
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
this._getPreferredHeight(actor, -1, alloc);
|
|
},
|
|
|
|
_getPreferredHeight: function(actor, forWidth, alloc) {
|
|
let [iconMinHeight, iconNatHeight] = this._iconBin.get_preferred_height(forWidth);
|
|
alloc.min_size = iconMinHeight;
|
|
alloc.natural_size = iconNatHeight;
|
|
|
|
if (this.label) {
|
|
let [labelMinHeight, labelNatHeight] = this.label.get_preferred_height(forWidth);
|
|
alloc.min_size += this._spacing + labelMinHeight;
|
|
alloc.natural_size += this._spacing + labelNatHeight;
|
|
}
|
|
},
|
|
|
|
// This can be overridden by a subclass, or by the createIcon
|
|
// parameter to _init()
|
|
createIcon: function(size) {
|
|
throw new Error('no implementation of createIcon in ' + this);
|
|
},
|
|
|
|
setIconSize: function(size) {
|
|
if (!this._setSizeManually)
|
|
throw new Error('setSizeManually has to be set to use setIconsize');
|
|
|
|
if (size == this.iconSize)
|
|
return;
|
|
|
|
this._createIconTexture(size);
|
|
},
|
|
|
|
_createIconTexture: function(size) {
|
|
if (this.icon)
|
|
this.icon.destroy();
|
|
this.iconSize = size;
|
|
this.icon = this.createIcon(this.iconSize);
|
|
|
|
// The icon returned by createIcon() might actually be smaller than
|
|
// the requested icon size (for instance StTextureCache does this
|
|
// for fallback icons), so set the size explicitly.
|
|
this.icon.set_size(this.iconSize, this.iconSize);
|
|
|
|
this._iconBin.child = this.icon;
|
|
},
|
|
|
|
_onStyleChanged: function() {
|
|
let node = this.actor.get_theme_node();
|
|
this._spacing = node.get_length('spacing');
|
|
|
|
let size;
|
|
if (this._setSizeManually) {
|
|
size = this.iconSize;
|
|
} else {
|
|
let [found, len] = node.lookup_length('icon-size', false);
|
|
size = found ? len : ICON_SIZE;
|
|
}
|
|
|
|
// don't create icons unnecessarily
|
|
if (size == this.iconSize &&
|
|
this._iconBin.child)
|
|
return;
|
|
|
|
this._createIconTexture(size);
|
|
}
|
|
};
|
|
|
|
function IconGrid(params) {
|
|
this._init(params);
|
|
}
|
|
|
|
IconGrid.prototype = {
|
|
_init: function(params) {
|
|
params = Params.parse(params, { rowLimit: null,
|
|
columnLimit: null,
|
|
xAlign: St.Align.MIDDLE });
|
|
this._rowLimit = params.rowLimit;
|
|
this._colLimit = params.columnLimit;
|
|
this._xAlign = params.xAlign;
|
|
|
|
this.actor = new St.BoxLayout({ style_class: 'icon-grid',
|
|
vertical: true });
|
|
// Pulled from CSS, but hardcode some defaults here
|
|
this._spacing = 0;
|
|
this._item_size = ICON_SIZE;
|
|
this._grid = new Shell.GenericContainer();
|
|
this.actor.add(this._grid, { expand: true, y_align: St.Align.START });
|
|
this.actor.connect('style-changed', Lang.bind(this, this._onStyleChanged));
|
|
|
|
this._grid.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
|
|
this._grid.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
|
|
this._grid.connect('allocate', Lang.bind(this, this._allocate));
|
|
},
|
|
|
|
_getPreferredWidth: function (grid, forHeight, alloc) {
|
|
let children = this._grid.get_children();
|
|
let nColumns = this._colLimit ? Math.min(this._colLimit,
|
|
children.length)
|
|
: children.length;
|
|
let totalSpacing = Math.max(0, nColumns - 1) * this._spacing;
|
|
// Kind of a lie, but not really an issue right now. If
|
|
// we wanted to support some sort of hidden/overflow that would
|
|
// need higher level design
|
|
alloc.min_size = this._item_size;
|
|
alloc.natural_size = nColumns * this._item_size + totalSpacing;
|
|
},
|
|
|
|
_getVisibleChildren: function() {
|
|
let children = this._grid.get_children();
|
|
children = children.filter(function(actor) {
|
|
return actor.visible;
|
|
});
|
|
return children;
|
|
},
|
|
|
|
_getPreferredHeight: function (grid, forWidth, alloc) {
|
|
let children = this._getVisibleChildren();
|
|
let [nColumns, usedWidth] = this._computeLayout(forWidth);
|
|
let nRows;
|
|
if (nColumns > 0)
|
|
nRows = Math.ceil(children.length / nColumns);
|
|
else
|
|
nRows = 0;
|
|
if (this._rowLimit)
|
|
nRows = Math.min(nRows, this._rowLimit);
|
|
let totalSpacing = Math.max(0, nRows - 1) * this._spacing;
|
|
let height = nRows * this._item_size + totalSpacing;
|
|
alloc.min_size = height;
|
|
alloc.natural_size = height;
|
|
},
|
|
|
|
_allocate: function (grid, box, flags) {
|
|
let children = this._getVisibleChildren();
|
|
let availWidth = box.x2 - box.x1;
|
|
let availHeight = box.y2 - box.y1;
|
|
|
|
let [nColumns, usedWidth] = this._computeLayout(availWidth);
|
|
|
|
let leftPadding;
|
|
switch(this._xAlign) {
|
|
case St.Align.START:
|
|
leftPadding = 0;
|
|
break;
|
|
case St.Align.MIDDLE:
|
|
leftPadding = Math.floor((availWidth - usedWidth) / 2);
|
|
break;
|
|
case St.Align.END:
|
|
leftPadding = availWidth - usedWidth;
|
|
}
|
|
|
|
let x = box.x1 + leftPadding;
|
|
let y = box.y1;
|
|
let columnIndex = 0;
|
|
let rowIndex = 0;
|
|
for (let i = 0; i < children.length; i++) {
|
|
let [childMinWidth, childMinHeight, childNaturalWidth, childNaturalHeight]
|
|
= children[i].get_preferred_size();
|
|
|
|
/* Center the item in its allocation horizontally */
|
|
let width = Math.min(this._item_size, childNaturalWidth);
|
|
let childXSpacing = Math.max(0, width - childNaturalWidth) / 2;
|
|
let height = Math.min(this._item_size, childNaturalHeight);
|
|
let childYSpacing = Math.max(0, height - childNaturalHeight) / 2;
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
if (St.Widget.get_default_direction() == St.TextDirection.RTL) {
|
|
let _x = box.x2 - (x + width);
|
|
childBox.x1 = Math.floor(_x - childXSpacing);
|
|
} else {
|
|
childBox.x1 = Math.floor(x + childXSpacing);
|
|
}
|
|
childBox.y1 = Math.floor(y + childYSpacing);
|
|
childBox.x2 = childBox.x1 + width;
|
|
childBox.y2 = childBox.y1 + height;
|
|
|
|
if (this._rowLimit && rowIndex >= this._rowLimit) {
|
|
this._grid.set_skip_paint(children[i], true);
|
|
} else {
|
|
children[i].allocate(childBox, flags);
|
|
this._grid.set_skip_paint(children[i], false);
|
|
}
|
|
|
|
columnIndex++;
|
|
if (columnIndex == nColumns) {
|
|
columnIndex = 0;
|
|
rowIndex++;
|
|
}
|
|
|
|
if (columnIndex == 0) {
|
|
y += this._item_size + this._spacing;
|
|
x = box.x1 + leftPadding;
|
|
} else {
|
|
x += this._item_size + this._spacing;
|
|
}
|
|
}
|
|
},
|
|
|
|
childrenInRow: function(rowWidth) {
|
|
return this._computeLayout(rowWidth)[0];
|
|
},
|
|
|
|
_computeLayout: function (forWidth) {
|
|
let nColumns = 0;
|
|
let usedWidth = 0;
|
|
while ((this._colLimit == null || nColumns < this._colLimit) &&
|
|
(usedWidth + this._item_size <= forWidth)) {
|
|
usedWidth += this._item_size + this._spacing;
|
|
nColumns += 1;
|
|
}
|
|
|
|
if (nColumns > 0)
|
|
usedWidth -= this._spacing;
|
|
|
|
return [nColumns, usedWidth];
|
|
},
|
|
|
|
_onStyleChanged: function() {
|
|
let themeNode = this.actor.get_theme_node();
|
|
this._spacing = themeNode.get_length('spacing');
|
|
this._item_size = themeNode.get_length('-shell-grid-item-size');
|
|
this._grid.queue_relayout();
|
|
},
|
|
|
|
removeAll: function () {
|
|
this._grid.get_children().forEach(Lang.bind(this, function (child) {
|
|
child.destroy();
|
|
}));
|
|
},
|
|
|
|
addItem: function(actor) {
|
|
this._grid.add_actor(actor);
|
|
},
|
|
|
|
getItemAtIndex: function(index) {
|
|
return this._grid.get_children()[index];
|
|
},
|
|
|
|
visibleItemsCount: function() {
|
|
return this._grid.get_children().length - this._grid.get_n_skip_paint();
|
|
}
|
|
};
|