gnome-shell/js/ui/iconGrid.js

367 lines
12 KiB
JavaScript
Raw Normal View History

// -*- 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;
const BaseIcon = new Lang.Class({
Name: 'BaseIcon',
_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.actor.connect('destroy',
Lang.bind(this, this._onDestroy));
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({ x_align: St.Align.MIDDLE,
y_align: St.Align.MIDDLE });
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;
let cache = St.TextureCache.get_default();
this._iconThemeChangedId = cache.connect('icon-theme-changed', Lang.bind(this, this._onIconThemeChanged));
},
_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);
this._iconBin.child = this.icon;
// 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._iconBin.set_size(this.iconSize, this.iconSize);
},
_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;
}
if (this.iconSize == size && this._iconBin.child)
return;
this._createIconTexture(size);
},
_onDestroy: function() {
if (this._iconThemeChangedId > 0) {
let cache = St.TextureCache.get_default();
cache.disconnect(this._iconThemeChangedId);
this._iconThemeChangedId = 0;
}
},
_onIconThemeChanged: function() {
this._createIconTexture(this.iconSize);
}
});
const IconGrid = new Lang.Class({
Name: 'IconGrid',
_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._hItemSize = this._vItemSize = 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._hItemSize;
alloc.natural_size = nColumns * this._hItemSize + 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, spacing;
if (forWidth < 0) {
nColumns = children.length;
spacing = this._spacing;
} else {
[nColumns, , spacing] = 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) * spacing;
let height = nRows * this._vItemSize + 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, spacing] = 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._hItemSize, childNaturalWidth);
let childXSpacing = Math.max(0, width - childNaturalWidth) / 2;
let height = Math.min(this._vItemSize, childNaturalHeight);
let childYSpacing = Math.max(0, height - childNaturalHeight) / 2;
let childBox = new Clutter.ActorBox();
if (Clutter.get_default_text_direction() == Clutter.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._vItemSize + spacing;
x = box.x1 + leftPadding;
} else {
x += this._hItemSize + spacing;
}
}
},
childrenInRow: function(rowWidth) {
return this._computeLayout(rowWidth)[0];
},
getRowLimit: function() {
return this._rowLimit;
},
_computeLayout: function (forWidth) {
let nColumns = 0;
let usedWidth = 0;
let spacing = this._spacing;
if (this._colLimit) {
let itemWidth = this._hItemSize * this._colLimit;
let emptyArea = forWidth - itemWidth;
spacing = Math.max(this._spacing, emptyArea / (2 * this._colLimit));
spacing = Math.round(spacing);
}
while ((this._colLimit == null || nColumns < this._colLimit) &&
(usedWidth + this._hItemSize <= forWidth)) {
usedWidth += this._hItemSize + spacing;
nColumns += 1;
}
if (nColumns > 0)
usedWidth -= spacing;
return [nColumns, usedWidth, spacing];
},
_onStyleChanged: function() {
let themeNode = this.actor.get_theme_node();
this._spacing = themeNode.get_length('spacing');
this._hItemSize = themeNode.get_length('-shell-grid-horizontal-item-size') || ICON_SIZE;
this._vItemSize = themeNode.get_length('-shell-grid-vertical-item-size') || ICON_SIZE;
this._grid.queue_relayout();
},
removeAll: function() {
this._grid.destroy_all_children();
},
addItem: function(actor, index) {
if (index !== undefined)
this._grid.insert_child_at_index(actor, index);
else
this._grid.add_actor(actor);
},
getItemAtIndex: function(index) {
return this._grid.get_child_at_index(index);
},
visibleItemsCount: function() {
return this._grid.get_n_children() - this._grid.get_n_skip_paint();
}
});