diff --git a/data/theme/gnome-shell-sass/widgets/_app-grid.scss b/data/theme/gnome-shell-sass/widgets/_app-grid.scss index c2b8f7e84..f52a91663 100644 --- a/data/theme/gnome-shell-sass/widgets/_app-grid.scss +++ b/data/theme/gnome-shell-sass/widgets/_app-grid.scss @@ -1,17 +1,13 @@ /* App Grid */ $app_icon_size: 96px; -$app_icon_padding: 24px; // app icons .icon-grid { - -shell-grid-horizontal-item-size: $app_icon_size + $app_icon_padding * 2; - -shell-grid-vertical-item-size: $app_icon_size + $app_icon_padding * 2; - spacing: $base_spacing * 6; - - .overview-icon { - icon-size: $app_icon_size; - } + row-spacing: $base_spacing * 6; + column-spacing: $base_spacing * 6; + max-row-spacing: $base_spacing * 12; + max-column-spacing: $base_spacing * 12; } /* App Icons */ diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index c2c258f6e..6bd3b6145 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -20,16 +20,12 @@ const Util = imports.misc.util; const SystemActions = imports.misc.systemActions; var MENU_POPUP_TIMEOUT = 600; -var MAX_COLUMNS = 6; -var MIN_COLUMNS = 4; -var MIN_ROWS = 4; var FOLDER_SUBICON_FRACTION = .4; var VIEWS_SWITCH_TIME = 400; var VIEWS_SWITCH_ANIMATION_DELAY = 100; -var PAGE_SWITCH_TIME = 250; var SCROLL_TIMEOUT_TIME = 150; var APP_ICON_SCALE_IN_TIME = 500; @@ -121,17 +117,10 @@ var BaseAppView = GObject.registerClass({ 'view-loaded': {}, }, }, class BaseAppView extends St.Widget { - _init(params = {}, gridParams) { + _init(params = {}) { super._init(params); - gridParams = Params.parse(gridParams, { - columnLimit: MAX_COLUMNS, - minRows: MIN_ROWS, - minColumns: MIN_COLUMNS, - padWithSpacing: true, - }, true); - - this._grid = new IconGrid.IconGrid(gridParams); + this._grid = new IconGrid.IconGrid(); this._grid.connect('child-focused', (grid, actor) => { this._childFocused(actor); }); @@ -181,7 +170,7 @@ var BaseAppView = GObject.registerClass({ let iconIndex = newApps.indexOf(icon); this._orderedItems.splice(iconIndex, 0, icon); - this._grid.addItem(icon, iconIndex); + this._grid.addItem(icon); this._items.set(icon.id, icon); }); @@ -331,19 +320,13 @@ class AppDisplay extends BaseAppView { }); this.add_actor(this._stack); - let box = new St.BoxLayout({ - vertical: true, - y_align: Clutter.ActorAlign.START, - }); - box.add_child(this._grid); - this._scrollView = new St.ScrollView({ style_class: 'all-apps', x_expand: true, y_expand: true, reactive: true, }); - this._scrollView.add_actor(box); + this._scrollView.add_actor(this._grid); this._stack.add_actor(this._scrollView); this._scrollView.set_policy(St.PolicyType.NEVER, @@ -365,8 +348,6 @@ class AppDisplay extends BaseAppView { this._folderIcons = []; - this._grid.currentPage = 0; - this._scrollView.connect('scroll-event', this._onScroll.bind(this)); this._swipeTracker = new SwipeTracker.SwipeTracker( @@ -472,10 +453,20 @@ class AppDisplay extends BaseAppView { let newIdx = Util.insertSorted(this._orderedItems, item, this._compareItems); this._grid.removeItem(item); - this._grid.addItem(item, newIdx); + this._grid.addItem(item, -1, newIdx); this.selectApp(item.id); } + _isItemInFolder(itemId) { + for (const folder of this._folderIcons) { + const folderApps = folder.getAppIds(); + if (folderApps.some(appId => appId === itemId)) + return true; + } + + return false; + } + _refilterApps() { let filteredApps = this._orderedItems.filter(icon => !icon.visible); @@ -551,6 +542,7 @@ class AppDisplay extends BaseAppView { }); } + icon.visible = !this._isItemInFolder(appId); appIcons.push(icon); }); @@ -596,7 +588,7 @@ class AppDisplay extends BaseAppView { } goToPage(pageNumber, animate = true) { - pageNumber = Math.clamp(pageNumber, 0, this._grid.nPages() - 1); + pageNumber = Math.clamp(pageNumber, 0, this._grid.nPages - 1); if (this._grid.currentPage === pageNumber && this._displayingDialog && @@ -605,23 +597,7 @@ class AppDisplay extends BaseAppView { if (this._displayingDialog && this._currentDialog) this._currentDialog.popdown(); - if (!this.mapped) { - this._adjustment.value = this._grid.getPageY(pageNumber); - this._pageIndicators.setCurrentPosition(pageNumber); - this._grid.currentPage = pageNumber; - return; - } - - if (this._grid.currentPage === pageNumber) - return; - - this._grid.currentPage = pageNumber; - - // Animate the change between pages. - this._adjustment.ease(this._grid.getPageY(this._grid.currentPage), { - mode: Clutter.AnimationMode.EASE_OUT_CUBIC, - duration: animate ? PAGE_SWITCH_TIME : 0, - }); + this._grid.goToPage(pageNumber, animate); } _onScroll(actor, event) { @@ -661,7 +637,7 @@ class AppDisplay extends BaseAppView { adjustment.remove_transition('value'); let progress = adjustment.value / adjustment.page_size; - let points = Array.from({ length: this._grid.nPages() }, (v, i) => i); + let points = Array.from({ length: this._grid.nPages }, (v, i) => i); tracker.confirmSwipe(this._scrollView.height, points, progress, Math.round(progress)); @@ -738,21 +714,15 @@ class AppDisplay extends BaseAppView { box = this._grid.get_theme_node().get_content_box(box); let availWidth = box.x2 - box.x1; let availHeight = box.y2 - box.y1; - let oldNPages = this._grid.nPages(); + let oldNPages = this._grid.nPages; this._grid.adaptToSize(availWidth, availHeight); - let fadeOffset = Math.min(this._grid.topPadding, - this._grid.bottomPadding); - this._scrollView.update_fade_effect(fadeOffset, 0); - if (fadeOffset > 0) - this._scrollView.get_effect('fade').fade_edges = true; - - if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) { + if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages) { Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { this._adjustment.value = 0; this._grid.currentPage = 0; - this._pageIndicators.setNPages(this._grid.nPages()); + this._pageIndicators.setNPages(this._grid.nPages); this._pageIndicators.setCurrentPosition(0); return GLib.SOURCE_REMOVE; }); @@ -1006,8 +976,6 @@ class FolderView extends BaseAppView { layout_manager: new Clutter.BinLayout(), x_expand: true, y_expand: true, - }, { - minRows: 3, }); // If it not expand, the parent doesn't take into account its preferred_width when allocating @@ -1098,20 +1066,6 @@ class FolderView extends BaseAppView { this._parentAvailableHeight = height; this._grid.adaptToSize(width, height); - - // To avoid the fade effect being applied to the unscrolled grid, - // the offset would need to be applied after adjusting the padding; - // however the final padding is expected to be too small for the - // effect to look good, so use the unadjusted padding - let fadeOffset = Math.min(this._grid.topPadding, - this._grid.bottomPadding); - this._scrollView.update_fade_effect(fadeOffset, 0); - - // Set extra padding to avoid popup or close button being cut off - this._grid.topPadding = Math.max(this._grid.topPadding, 0); - this._grid.bottomPadding = Math.max(this._grid.bottomPadding, 0); - this._grid.leftPadding = Math.max(this._grid.leftPadding, 0); - this._grid.rightPadding = Math.max(this._grid.rightPadding, 0); } _loadApps() { diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index a746850cd..6a5b1dbbb 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -1,22 +1,20 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -/* exported BaseIcon, IconGrid */ +/* exported BaseIcon, IconGrid, IconGridLayout */ -const { Clutter, GLib, GObject, Graphene, Meta, St } = imports.gi; +const { Clutter, GLib, GObject, Meta, St } = imports.gi; const Params = imports.misc.params; const Main = imports.ui.main; var ICON_SIZE = 96; -var MIN_ICON_SIZE = 16; var ANIMATION_TIME_IN = 350; var ANIMATION_TIME_OUT = 1 / 2 * ANIMATION_TIME_IN; var ANIMATION_MAX_DELAY_FOR_ITEM = 2 / 3 * ANIMATION_TIME_IN; -var ANIMATION_BASE_DELAY_FOR_ITEM = 1 / 4 * ANIMATION_MAX_DELAY_FOR_ITEM; var ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 2 / 3 * ANIMATION_TIME_OUT; var ANIMATION_FADE_IN_TIME_FOR_ITEM = 1 / 4 * ANIMATION_TIME_IN; -var ANIMATION_BOUNCE_ICON_SCALE = 1.1; +var PAGE_SWITCH_TIME = 300; var AnimationDirection = { IN: 0, @@ -24,7 +22,6 @@ var AnimationDirection = { }; var IconSize = { - HUGE: 128, LARGE: 96, MEDIUM: 64, SMALL: 32, @@ -1124,72 +1121,64 @@ var IconGridLayout = GObject.registerClass({ }); var IconGrid = GObject.registerClass({ - Signals: { 'animation-done': {}, - 'child-focused': { param_types: [Clutter.Actor.$gtype] } }, -}, class IconGrid extends St.Widget { - _init(params) { - super._init({ style_class: 'icon-grid', - y_align: Clutter.ActorAlign.START }); + Signals: { + 'pages-changed': {}, + 'animation-done': {}, + 'child-focused': { param_types: [Clutter.Actor.$gtype] }, + }, +}, class IconGrid extends St.Viewport { + _init(layoutParams = {}) { + layoutParams = Params.parse(layoutParams, { + allow_incomplete_pages: false, + orientation: Clutter.Orientation.VERTICAL, + columns_per_page: 6, + rows_per_page: 4, + page_halign: Clutter.ActorAlign.CENTER, + page_valign: Clutter.ActorAlign.CENTER, + last_row_align: Clutter.ActorAlign.START, + column_spacing: 0, + row_spacing: 0, + }); + const layoutManager = new IconGridLayout(layoutParams); + layoutManager.connect('pages-changed', () => this.emit('pages-changed')); - params = Params.parse(params, { rowLimit: null, - columnLimit: null, - minRows: 1, - minColumns: 1, - xAlign: St.Align.MIDDLE, - padWithSpacing: false }); - this._rowLimit = params.rowLimit; - this._colLimit = params.columnLimit; - this._minRows = params.minRows; - this._minColumns = params.minColumns; - this._xAlign = params.xAlign; - this._padWithSpacing = params.padWithSpacing; + super._init({ + style_class: 'icon-grid', + layoutManager, + x_expand: true, + y_expand: true, + }); - this.topPadding = 0; - this.bottomPadding = 0; - this.rightPadding = 0; - this.leftPadding = 0; - - this._nPages = 0; - this.currentPage = 0; - this._rowsPerPage = 0; - this._spaceBetweenPages = 0; - this._childrenPerPage = 0; - - this._updateIconSizesLaterId = 0; - - this._items = []; + this._currentPage = 0; this._clonesAnimating = []; - // Pulled from CSS, but hardcode some defaults here - this._spacing = 0; - this._hItemSize = this._vItemSize = ICON_SIZE; - this._fixedHItemSize = this._fixedVItemSize = undefined; - this.connect('style-changed', this._onStyleChanged.bind(this)); this.connect('actor-added', this._childAdded.bind(this)); this.connect('actor-removed', this._childRemoved.bind(this)); - this.connect('destroy', this._onDestroy.bind(this)); } - vfunc_unmap() { - // Cancel animations when hiding the overview, to avoid icons - // swarming into the void ... + _getChildrenToAnimate() { + const layoutManager = this.layout_manager; + const children = layoutManager.getItemsAtPage(this._currentPage); + + return children.filter(c => c.visible); + } + + _resetAnimationActors() { + this._clonesAnimating.forEach(clone => { + clone.source.reactive = true; + clone.source.opacity = 255; + clone.destroy(); + }); + this._clonesAnimating = []; + } + + _animationDone() { this._resetAnimationActors(); - super.vfunc_unmap(); - } - - _onDestroy() { - if (this._updateIconSizesLaterId) { - Meta.later_remove(this._updateIconSizesLaterId); - this._updateIconSizesLaterId = 0; - } - } - - _keyFocusIn(actor) { - this.emit('child-focused', actor); + this.emit('animation-done'); } _childAdded(grid, child) { - child._iconGridKeyFocusInId = child.connect('key-focus-in', this._keyFocusIn.bind(this)); + child._iconGridKeyFocusInId = child.connect('key-focus-in', actor => this.emit('child-focused', actor)); child._paintVisible = child.opacity > 0; child._opacityChangedId = child.connect('notify::opacity', () => { @@ -1209,201 +1198,180 @@ var IconGrid = GObject.registerClass({ delete child._paintVisible; } - vfunc_get_preferred_width(_forHeight) { - let nChildren = this.get_n_children(); - let nColumns = this._colLimit - ? Math.min(this._colLimit, nChildren) - : nChildren; - let totalSpacing = Math.max(0, nColumns - 1) * this._getSpacing(); - // 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 - let minSize = this._getHItemSize() + this.leftPadding + this.rightPadding; - let natSize = nColumns * this._getHItemSize() + totalSpacing + this.leftPadding + this.rightPadding; - - return this.get_theme_node().adjust_preferred_width(minSize, natSize); + vfunc_unmap() { + // Cancel animations when hiding the overview, to avoid icons + // swarming into the void ... + this._resetAnimationActors(); + super.vfunc_unmap(); } - _getVisibleChildren() { - return this.get_children().filter(actor => actor.visible); + vfunc_style_changed() { + super.vfunc_style_changed(); + + const node = this.get_theme_node(); + this.layout_manager.column_spacing = node.get_length('column-spacing'); + this.layout_manager.row_spacing = node.get_length('row-spacing'); + + let [found, value] = node.lookup_length('max-column-spacing', false); + this.layout_manager.max_column_spacing = found ? value : -1; + + [found, value] = node.lookup_length('max-row-spacing', false); + this.layout_manager.max_row_spacing = found ? value : -1; } - _availableHeightPerPageForItems() { - return this.usedHeightForNRows(this._rowsPerPage) - (this.topPadding + this.bottomPadding); - } - - vfunc_get_preferred_height() { - const height = (this._availableHeightPerPageForItems() + this.bottomPadding + this.topPadding) * this._nPages + this._spaceBetweenPages * this._nPages; - return [height, height]; - } - - vfunc_allocate(box) { - if (this._childrenPerPage === 0) - log('computePages() must be called before allocate(); pagination will not work.'); - - this.set_allocation(box); - - let children = this._getVisibleChildren(); - let availWidth = box.x2 - box.x1; - let spacing = this._getSpacing(); - let [nColumns, usedWidth] = this._computeLayout(availWidth); - - let leftEmptySpace; - switch (this._xAlign) { - case St.Align.START: - leftEmptySpace = 0; - break; - case St.Align.MIDDLE: - leftEmptySpace = Math.floor((availWidth - usedWidth) / 2); - break; - case St.Align.END: - leftEmptySpace = availWidth - usedWidth; - } - - let x = box.x1 + leftEmptySpace + this.leftPadding; - let y = box.y1 + this.topPadding; - let columnIndex = 0; - - let nChangedIcons = 0; - for (let i = 0; i < children.length; i++) { - let childBox = this._calculateChildBox(children[i], x, y, box); - - if (animateIconPosition(children[i], childBox, nChangedIcons)) - nChangedIcons++; - - children[i].show(); - - columnIndex++; - if (columnIndex === nColumns) - columnIndex = 0; - - if (columnIndex == 0) { - y += this._getVItemSize() + spacing; - if ((i + 1) % this._childrenPerPage === 0) - y += this._spaceBetweenPages - spacing + this.bottomPadding + this.topPadding; - x = box.x1 + leftEmptySpace + this.leftPadding; - } else { - x += this._getHItemSize() + spacing; - } - } - } - - vfunc_get_paint_volume(paintVolume) { - // Setting the paint volume does not make sense when we don't have - // any allocation - if (!this.has_allocation()) - return false; - - let themeNode = this.get_theme_node(); - let allocationBox = this.get_allocation_box(); - let paintBox = themeNode.get_paint_box(allocationBox); - - let origin = new Graphene.Point3D(); - origin.x = paintBox.x1 - allocationBox.x1; - origin.y = paintBox.y1 - allocationBox.y1; - origin.z = 0.0; - - paintVolume.set_origin(origin); - paintVolume.set_width(paintBox.x2 - paintBox.x1); - paintVolume.set_height(paintBox.y2 - paintBox.y1); - - if (this.get_clip_to_allocation()) - return true; - - for (let child = this.get_first_child(); - child != null; - child = child.get_next_sibling()) { - - if (!child.visible || !child.opacity) - continue; - - let childVolume = child.get_transformed_paint_volume(this); - if (!childVolume) - return false; - - paintVolume.union(childVolume); - } - - return true; - } - - /* - * Intended to be override by subclasses if they need a different - * set of items to be animated. + /** + * addItem: + * @param {Clutter.Actor} item: item to append to the grid + * @param {int} page: page number + * @param {int} index: position in the page + * + * Adds @item to the grid. @item must not be part of the grid. + * + * If @index exceeds the number of items per page, @item will + * be added to the next page. + * + * @page must be a number between 0 and the number of pages. + * Adding to the page after next will create a new page. */ - _getChildrenToAnimate() { - const children = this._getVisibleChildren().filter(child => child.opacity > 0); - const firstIndex = this._childrenPerPage * this.currentPage; - const lastIndex = firstIndex + this._childrenPerPage; + addItem(item, page = -1, index = -1) { + if (!(item.icon instanceof BaseIcon)) + throw new Error('Only items with a BaseIcon icon property can be added to IconGrid'); - return children.slice(firstIndex, lastIndex); + this.layout_manager.addItem(item, page, index); } - _resetAnimationActors() { - this._clonesAnimating.forEach(clone => { - clone.source.reactive = true; - clone.source.opacity = 255; - clone.destroy(); + /** + * appendItem: + * @param {Clutter.Actor} item: item to append to the grid + * + * Appends @item to the grid. @item must not be part of the grid. + */ + appendItem(item) { + this.layout_manager.appendItem(item); + } + + /** + * removeItem: + * @param {Clutter.Actor} item: item to remove from the grid + * + * Removes @item to the grid. @item must be part of the grid. + */ + removeItem(item) { + if (!this.contains(item)) + throw new Error(`Item ${item} is not part of the IconGrid`); + + this.layout_manager.removeItem(item); + } + + /** + * goToPage: + * @param {int} pageIndex: page index + * @param {boolean} animate: animate the page transition + * + * Moves the current page to @pageIndex. @pageIndex must be a valid page + * number. + */ + goToPage(pageIndex, animate = true) { + if (pageIndex >= this.nPages) + throw new Error(`IconGrid does not have page ${pageIndex}`); + + let newValue; + let adjustment; + switch (this.layout_manager.orientation) { + case Clutter.Orientation.VERTICAL: + adjustment = this.vadjustment; + newValue = pageIndex * this.layout_manager.pageHeight; + break; + case Clutter.Orientation.HORIZONTAL: + adjustment = this.hadjustment; + newValue = pageIndex * this.layout_manager.pageWidth; + break; + } + + this._currentPage = pageIndex; + + if (!this.mapped) + animate = false; + + adjustment.ease(newValue, { + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + duration: animate ? PAGE_SWITCH_TIME : 0, }); - this._clonesAnimating = []; } - _animationDone() { - this._resetAnimationActors(); - this.emit('animation-done'); + /** + * getItemPage: + * @param {BaseIcon} item: the item + * + * Retrieves the page @item is in, or -1 if @item is not part of the grid. + * + * @returns {int} the page where @item is in + */ + getItemPage(item) { + return this.layout_manager.getItemPage(item); } - animatePulse(animationDirection) { - if (animationDirection != AnimationDirection.IN) { - throw new GObject.NotImplementedError("Pulse animation only implements " + - "'in' animation direction"); - } + /** + * getItemPosition: + * @param {BaseIcon} item: the item + * + * Retrieves the position of @item is its page, or -1 if @item is not + * part of the grid. + * + * @returns {[int, int]} the page and position of @item + */ + getItemPosition(item) { + if (!this.contains(item)) + return [-1, -1]; - this._resetAnimationActors(); + const layoutManager = this.layout_manager; + return layoutManager.getItemPosition(item); + } - let actors = this._getChildrenToAnimate(); - if (actors.length == 0) { - this._animationDone(); - return; - } + /** + * getItemAt: + * @param {int} page: the page + * @param {int} position: the position in page + * + * Retrieves the item at @page and @position. + * + * @returns {BaseItem} the item at @page and @position, or null + */ + getItemAt(page, position) { + const layoutManager = this.layout_manager; + return layoutManager.getItemAt(page, position); + } - // For few items the animation can be slow, so use a smaller - // delay when there are less than 4 items - // (ANIMATION_BASE_DELAY_FOR_ITEM = 1/4 * - // ANIMATION_MAX_DELAY_FOR_ITEM) - let maxDelay = Math.min(ANIMATION_BASE_DELAY_FOR_ITEM * actors.length, - ANIMATION_MAX_DELAY_FOR_ITEM); + /** + * getItemsAtPage: + * @param {int} page: the page index + * + * Retrieves the children at page @page, including invisible children. + * + * @returns {Array} an array of {Clutter.Actor}s + */ + getItemsAtPage(page) { + if (page < 0 || page > this.nPages) + throw new Error(`Page ${page} does not exist at IconGrid`); - for (let index = 0; index < actors.length; index++) { - let actor = actors[index]; - actor.set_scale(0, 0); - actor.set_pivot_point(0.5, 0.5); + const layoutManager = this.layout_manager; + return layoutManager.getItemsAtPage(page); + } - let delay = index / actors.length * maxDelay; - let bounceUpTime = ANIMATION_TIME_IN / 4; - let isLastItem = index == actors.length - 1; - actor.ease({ - scale_x: ANIMATION_BOUNCE_ICON_SCALE, - scale_y: ANIMATION_BOUNCE_ICON_SCALE, - duration: bounceUpTime, - mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD, - delay, - onComplete: () => { - let duration = ANIMATION_TIME_IN - bounceUpTime; - actor.ease({ - scale_x: 1, - scale_y: 1, - duration, - mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD, - onComplete: () => { - if (isLastItem) - this._animationDone(); - actor.reactive = true; - }, - }); - }, - }); - } + get currentPage() { + return this._currentPage; + } + + set currentPage(v) { + this.goToPage(v); + } + + get nPages() { + return this.layout_manager.nPages; + } + + adaptToSize(width, height) { + this.layout_manager.adaptToSize(width, height); } animateSpring(animationDirection, sourceActor) { @@ -1450,11 +1418,11 @@ var IconGrid = GObject.registerClass({ */ this._clonesAnimating.forEach(actorClone => { - let actor = actorClone.source; + const actor = actorClone.source; actor.opacity = 0; actor.reactive = false; - let [width, height] = this._getAllocatedChildSizeAndSpacing(actor); + let [width, height] = actor.get_size(); actorClone.set_size(width, height); let scaleX = sourceScaledWidth / width; let scaleY = sourceScaledHeight / height; @@ -1523,238 +1491,8 @@ var IconGrid = GObject.registerClass({ }); } - _getAllocatedChildSizeAndSpacing(child) { - let [,, natWidth, natHeight] = child.get_preferred_size(); - let width = Math.min(this._getHItemSize(), natWidth); - let xSpacing = Math.max(0, width - natWidth) / 2; - let height = Math.min(this._getVItemSize(), natHeight); - let ySpacing = Math.max(0, height - natHeight) / 2; - return [width, height, xSpacing, ySpacing]; - } - - _calculateChildBox(child, x, y, box) { - /* Center the item in its allocation horizontally */ - let [width, height, childXSpacing, childYSpacing] = - this._getAllocatedChildSizeAndSpacing(child); - - 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; - return childBox; - } - - columnsForWidth(rowWidth) { - return this._computeLayout(rowWidth)[0]; - } - - getRowLimit() { - return this._rowLimit; - } - - _computeLayout(forWidth) { - this.ensure_style(); - - let nColumns = 0; - let usedWidth = this.leftPadding + this.rightPadding; - let spacing = this._getSpacing(); - - while ((this._colLimit == null || nColumns < this._colLimit) && - (usedWidth + this._getHItemSize() <= forWidth)) { - usedWidth += this._getHItemSize() + spacing; - nColumns += 1; - } - - if (nColumns > 0) - usedWidth -= spacing; - - return [nColumns, usedWidth]; - } - - _onStyleChanged() { - let themeNode = this.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.queue_relayout(); - } - - nRows(forWidth) { - let children = this._getVisibleChildren(); - let nColumns = forWidth < 0 ? children.length : this._computeLayout(forWidth)[0]; - let nRows = nColumns > 0 ? Math.ceil(children.length / nColumns) : 0; - if (this._rowLimit) - nRows = Math.min(nRows, this._rowLimit); - return nRows; - } - - rowsForHeight(forHeight) { - return Math.floor((forHeight - (this.topPadding + this.bottomPadding) + this._getSpacing()) / (this._getVItemSize() + this._getSpacing())); - } - - usedHeightForNRows(nRows) { - return (this._getVItemSize() + this._getSpacing()) * nRows - this._getSpacing() + this.topPadding + this.bottomPadding; - } - - usedWidth(forWidth) { - return this.usedWidthForNColumns(this.columnsForWidth(forWidth)); - } - - usedWidthForNColumns(columns) { - let usedWidth = columns * (this._getHItemSize() + this._getSpacing()); - usedWidth -= this._getSpacing(); - return usedWidth + this.leftPadding + this.rightPadding; - } - - addItem(item, index) { - if (!(item.icon instanceof BaseIcon)) - throw new Error('Only items with a BaseIcon icon property can be added to IconGrid'); - - this._items.push(item); - if (index !== undefined) - this.insert_child_at_index(item, index); - else - this.add_actor(item); - } - - removeItem(item) { - this.remove_child(item); - } - - setSpacing(spacing) { - this._fixedSpacing = spacing; - } - - _getSpacing() { - return this._fixedSpacing ? this._fixedSpacing : this._spacing; - } - - _getHItemSize() { - return this._fixedHItemSize ? this._fixedHItemSize : this._hItemSize; - } - - _getVItemSize() { - return this._fixedVItemSize ? this._fixedVItemSize : this._vItemSize; - } - - _updateSpacingForSize(availWidth, availHeight) { - let maxEmptyVArea = availHeight - this._minRows * this._getVItemSize(); - let maxEmptyHArea = availWidth - this._minColumns * this._getHItemSize(); - let maxHSpacing, maxVSpacing; - - if (this._padWithSpacing) { - // minRows + 1 because we want to put spacing before the first row, so it is like we have one more row - // to divide the empty space - maxVSpacing = Math.floor(maxEmptyVArea / (this._minRows + 1)); - maxHSpacing = Math.floor(maxEmptyHArea / (this._minColumns + 1)); - } else { - if (this._minRows <= 1) - maxVSpacing = maxEmptyVArea; - else - maxVSpacing = Math.floor(maxEmptyVArea / (this._minRows - 1)); - - if (this._minColumns <= 1) - maxHSpacing = maxEmptyHArea; - else - maxHSpacing = Math.floor(maxEmptyHArea / (this._minColumns - 1)); - } - - let maxSpacing = Math.min(maxHSpacing, maxVSpacing); - // Limit spacing to the item size - maxSpacing = Math.min(maxSpacing, Math.min(this._getVItemSize(), this._getHItemSize())); - // The minimum spacing, regardless of whether it satisfies the row/columng minima, - // is the spacing we get from CSS. - let spacing = Math.max(this._spacing, maxSpacing); - this.setSpacing(spacing); - if (this._padWithSpacing) - this.topPadding = this.rightPadding = this.bottomPadding = this.leftPadding = spacing; - } - - _computePages(availWidthPerPage, availHeightPerPage) { - const [nColumns] = this._computeLayout(availWidthPerPage); - const children = this._getVisibleChildren(); - let nRows; - if (nColumns > 0) - nRows = Math.ceil(children.length / nColumns); - else - nRows = 0; - if (this._rowLimit) - nRows = Math.min(nRows, this._rowLimit); - - // We want to contain the grid inside the parent box with padding - this._rowsPerPage = this.rowsForHeight(availHeightPerPage); - this._nPages = Math.ceil(nRows / this._rowsPerPage); - this._spaceBetweenPages = availHeightPerPage - (this.topPadding + this.bottomPadding) - this._availableHeightPerPageForItems(); - this._childrenPerPage = nColumns * this._rowsPerPage; - } - - /* - * This function must to be called before iconGrid allocation, - * to know how much spacing can the grid has - */ - adaptToSize(availWidth, availHeight) { - this._fixedHItemSize = this._hItemSize; - this._fixedVItemSize = this._vItemSize; - this._updateSpacingForSize(availWidth, availHeight); - - if (this.columnsForWidth(availWidth) < this._minColumns || this.rowsForHeight(availHeight) < this._minRows) { - let neededWidth = this.usedWidthForNColumns(this._minColumns) - availWidth; - let neededHeight = this.usedHeightForNRows(this._minRows) - availHeight; - - let neededSpacePerItem = neededWidth > neededHeight - ? Math.ceil(neededWidth / this._minColumns) - : Math.ceil(neededHeight / this._minRows); - this._fixedHItemSize = Math.max(this._hItemSize - neededSpacePerItem, MIN_ICON_SIZE); - this._fixedVItemSize = Math.max(this._vItemSize - neededSpacePerItem, MIN_ICON_SIZE); - - this._updateSpacingForSize(availWidth, availHeight); - } - if (!this._updateIconSizesLaterId) { - this._updateIconSizesLaterId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, - this._updateIconSizes.bind(this)); - } - this._computePages(availWidth, availHeight); - } - - // Note that this is ICON_SIZE as used by BaseIcon, not elsewhere in IconGrid; it's a bit messed up - _updateIconSizes() { - this._updateIconSizesLaterId = 0; - let scale = Math.min(this._fixedHItemSize, this._fixedVItemSize) / Math.max(this._hItemSize, this._vItemSize); - let newIconSize = Math.floor(ICON_SIZE * scale); - for (let i in this._items) - this._items[i].icon.setIconSize(newIconSize); - - return GLib.SOURCE_REMOVE; - } - - nPages() { - return this._nPages; - } - - getPageHeight() { - return this._availableHeightPerPageForItems(); - } - - getPageY(pageNumber) { - if (!this._nPages) - return 0; - - let firstPageItem = pageNumber * this._childrenPerPage; - let childBox = this._getVisibleChildren()[firstPageItem].get_allocation_box(); - return childBox.y1 - this.topPadding; - } - - getItemPage(item) { - let children = this._getVisibleChildren(); - let index = children.indexOf(item); - if (index == -1) - throw new Error('Item not found.'); - return Math.floor(index / this._childrenPerPage); + get itemsPerPage() { + const layoutManager = this.layout_manager; + return layoutManager.rows_per_page * layoutManager.columns_per_page; } });