/** * V-Shell (Vertical Workspaces) * iconGrid.js * * @author GdH * @copyright 2022 - 2024 * @license GPL-3.0 * */ 'use strict'; import St from 'gi://St'; import GLib from 'gi://GLib'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as IconGrid from 'resource:///org/gnome/shell/ui/iconGrid.js'; let Me; let opt; // added sizes for better scaling export const IconSize = { LARGEST: 256, 224: 224, 208: 208, 192: 192, 176: 176, 160: 160, 144: 144, 128: 128, 112: 112, LARGE: 96, 80: 80, 64: 64, TINY: 48, }; export const IconGridModule = class { constructor(me) { Me = me; opt = Me.opt; this._firstActivation = true; this.moduleEnabled = false; this._overrides = null; } cleanGlobals() { Me = null; opt = null; } update(reset) { this.moduleEnabled = opt.get('appDisplayModule'); // if notifications are enabled no override is needed reset = reset || !this.moduleEnabled; // don't touch original code if module disabled if (reset && !this._firstActivation) { this._disableModule(); } else if (!reset) { this._firstActivation = false; this._activateModule(); } } _activateModule() { if (!this._overrides) this._overrides = new Me.Util.Overrides(); this._overrides.addOverride('IconGrid', IconGrid.IconGrid.prototype, IconGridCommon); this._overrides.addOverride('IconGridLayout', IconGrid.IconGridLayout.prototype, IconGridLayoutCommon); } _disableModule() { if (this._overrides) this._overrides.removeAll(); this._overrides = null; } }; const IconGridCommon = { getItemsAtPage(page) { if (page < 0 || page >= this.nPages) return []; // throw new Error(`Page ${page} does not exist at IconGrid`); const layoutManager = this.layout_manager; return layoutManager.getItemsAtPage(page); }, _shouldUpdateGrid(width, height) { if (this.layoutManager._isFolder) return false; else if (this._currentMode === -1) return true; // Update if page size changed // Page dimensions may change within a small range const range = 5; return (Math.abs(width - (this._gridForWidth ?? 0)) > range) || (Math.abs(height - (this._gridForHeight ?? 0)) > range); }, _findBestModeForSize(width, height) { // this function is for main grid only, folder grid calculation is in appDisplay.AppFolderDialog class if (!this._shouldUpdateGrid(width, height)) return; this._gridForWidth = width; this._gridForHeight = height; this._updateDefaultIconSize(); const { pagePadding } = this.layout_manager; const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); const itemPadding = 55; // pagePadding is already affected by the scaleFactor width -= pagePadding.left + pagePadding.right; height -= pagePadding.top + pagePadding.bottom; // Sync with _findBestIconSize() this.layoutManager._gridSizeChanged = true; this.layoutManager._gridWidth = width; this.layoutManager._gridHeight = height; // All widgets are affected by the scaleFactor so we need to apply it also on the page size width /= scaleFactor; height /= scaleFactor; const spacing = opt.APP_GRID_SPACING; const iconSize = opt.APP_GRID_ICON_SIZE > 0 ? opt.APP_GRID_ICON_SIZE : opt.APP_GRID_ICON_SIZE_DEFAULT; const itemSize = iconSize + itemPadding; let columns = opt.APP_GRID_COLUMNS; let rows = opt.APP_GRID_ROWS; // 0 means adaptive size let unusedSpaceH = -1; if (!columns) { // calculate #columns + 1 without spacing columns = Math.floor(width / itemSize) + 1; // check if columns with spacing fits the available width // and reduce the number until it fits while (unusedSpaceH < 0) { columns -= 1; unusedSpaceH = width - columns * itemSize - (columns - 1) * spacing; } } let unusedSpaceV = -1; if (!rows) { rows = Math.floor(height / itemSize) + 1; while (unusedSpaceV < 0) { rows -= 1; unusedSpaceV = height - rows * itemSize - ((rows - 1) * spacing); } } this._gridModes = [{ columns, rows }]; this._currentMode = -1; this._setGridMode(0); this.layoutManager.updateIconSize(); // Call _redisplay() from timeout to avoid allocation errors GLib.idle_add(GLib.PRIORITY_LOW, () => Main.overview._overview.controls.appDisplay._redisplay() ); }, _updateDefaultIconSize() { // Reduce default icon size for low resolution screens and high screen scales if (Me.Util.monitorHasLowResolution()) { opt.APP_GRID_ICON_SIZE_DEFAULT = opt.APP_GRID_ACTIVE_PREVIEW && !opt.APP_GRID_USAGE ? 128 : 64; opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT = 64; } else { opt.APP_GRID_ICON_SIZE_DEFAULT = opt.APP_GRID_ACTIVE_PREVIEW && !opt.APP_GRID_USAGE ? 192 : 96; } }, // Workaround for the upstream bug // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5753 // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5240 // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6892 // The appGridLayout._currentPage is not updated when the page is changed in the grid // For example, when user navigates app icons using a keyboard // Related issues open on GNOME's gitlab: after_goToPage() { if (this._delegate._appGridLayout._currentPage !== this._currentPage) this._delegate._appGridLayout.goToPage(this._currentPage); }, // Workaround for the upstream bug // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7700 // Return INVALID target if x or y is out of the grid view to prevent pages[page] undefined error (horizontal orientation only) getDropTarget(x, y) { if (x < 0 || y < 0) return [0, 0, 0]; // [0, 0, DragLocation.INVALID] const layoutManager = this.layout_manager; return layoutManager.getDropTarget(x, y, this._currentPage); }, }; const IconGridLayoutCommon = { _findBestIconSize() { if (this.fixedIconSize !== -1) return this.fixedIconSize; if (!this._isFolder && !this._gridSizeChanged) return this._iconSize; this._gridSizeChanged = false; const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); const nColumns = this.columnsPerPage; const nRows = this.rowsPerPage; // If grid is not defined, return default icon size if (nColumns < 1 && nRows < 1) { return this._isFolder ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT : opt.APP_GRID_ICON_SIZE_DEFAULT; } const spacing = this._isFolder ? opt.APP_GRID_FOLDER_SPACING : opt.APP_GRID_SPACING; const columnSpacingPerPage = spacing * (nColumns - 1); const rowSpacingPerPage = spacing * (nRows - 1); const itemPadding = 55; const width = (this._gridWidth ? this._gridWidth : this._pageWidth) / scaleFactor; let height = (this._gridHeight ? this._gridHeight : this._pageHeight) / scaleFactor; if (!width || !height) return opt.APP_GRID_ICON_SIZE_DEFAULT; const [firstItem] = this._container; let iconSizes = Object.values(IconSize).sort((a, b) => b - a); // Limit max icon size for folders and fully adaptive folder grids, the whole range is for the main grid with active folders if (this._isFolder && opt.APP_GRID_FOLDER_ICON_SIZE < 0) iconSizes = iconSizes.slice(iconSizes.indexOf(opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT), -1); else if (this._isFolder) iconSizes = iconSizes.slice(iconSizes.indexOf(IconSize.LARGE), -1); else if (opt.APP_GRID_ICON_SIZE < 0) iconSizes = iconSizes.slice(iconSizes.indexOf(opt.APP_GRID_ICON_SIZE_DEFAULT), -1); let sizeInvalid = false; for (const size of iconSizes) { let usedWidth, usedHeight; if (firstItem) { firstItem.icon.setIconSize(size); const [firstItemWidth] = firstItem.get_preferred_size(); const itemSize = firstItemWidth / scaleFactor; if (itemSize < size) sizeInvalid = true; usedWidth = itemSize * nColumns; usedHeight = itemSize * nRows; } if (!firstItem || sizeInvalid) { usedWidth = (size + itemPadding) * nColumns; usedHeight = (size + itemPadding) * nRows; } const emptyHSpace = width - usedWidth - columnSpacingPerPage; const emptyVSpace = height - usedHeight - rowSpacingPerPage; if (emptyHSpace >= 0 && emptyVSpace >= 0) return size; } return IconSize.TINY; }, removeItem(item) { if (!this._items.has(item)) { console.error(`iconGrid: Item ${item} is not part of the IconGridLayout`); return; // throw new Error(`Item ${item} is not part of the IconGridLayout`); } if (!this._container) return; this._shouldEaseItems = true; this._container.remove_child(item); this._removeItemData(item); }, addItem(item, page = -1, index = -1) { if (this._items.has(item)) { console.error(`iconGrid: Item ${item} already added to IconGridLayout`); return; // throw new Error(`Item ${item} already added to IconGridLayout`); } if (page > this._pages.length) { console.error(`iconGrid: Cannot add ${item} to page ${page}`); page = -1; index = -1; // throw new Error(`Cannot add ${item} to page ${page}`); } if (!this._container) return; if (page !== -1 && index === -1) page = this._findBestPageToAppend(page); this._shouldEaseItems = true; if (!this._container.get_children().includes(item)) this._container.add_child(item); this._addItemToPage(item, page, index); }, moveItem(item, newPage, newPosition) { if (!this._items.has(item)) { console.error(`iconGrid: Item ${item} is not part of the IconGridLayout`); return; // throw new Error(`Item ${item} is not part of the IconGridLayout`); } this._shouldEaseItems = true; this._removeItemData(item); if (newPage !== -1 && newPosition === -1) newPage = this._findBestPageToAppend(newPage); this._addItemToPage(item, newPage, newPosition); }, _addItemToPage(item, pageIndex, index) { // Ensure we have at least one page if (this._pages.length === 0) this._appendPage(); // Append a new page if necessary if (pageIndex === this._pages.length) this._appendPage(); if (pageIndex >= this._pages.length) { pageIndex = -1; index = -1; } if (pageIndex === -1) pageIndex = this._pages.length - 1; if (index === -1) index = this._pages[pageIndex].children.length; this._items.set(item, { actor: item, pageIndex, destroyId: item.connect('destroy', () => this._removeItemData(item)), visibleId: item.connect('notify::visible', () => { const itemData = this._items.get(item); this._updateVisibleChildrenForPage(itemData.pageIndex); if (item.visible) this._relocateSurplusItems(itemData.pageIndex); else if (!this.allowIncompletePages) this._fillItemVacancies(itemData.pageIndex); }), queueRelayoutId: item.connect('queue-relayout', () => { this._childrenMaxSize = -1; }), }); item.icon.setIconSize(this._iconSize); this._pages[pageIndex].children.splice(index, 0, item); this._updateVisibleChildrenForPage(pageIndex); this._relocateSurplusItems(pageIndex); }, _relocateSurplusItems(pageIndex) { // Avoid recursion during relocations in _redisplay() if (this._skipRelocateSurplusItems) return; const visiblePageItems = this._pages[pageIndex].visibleChildren; const itemsPerPage = this.columnsPerPage * this.rowsPerPage; // No overflow if (visiblePageItems.length <= itemsPerPage) return; const nExtraItems = visiblePageItems.length - itemsPerPage; for (let i = 0; i < nExtraItems; i++) { const overflowIndex = visiblePageItems.length - i - 1; const overflowItem = visiblePageItems[overflowIndex]; this._removeItemData(overflowItem); this._addItemToPage(overflowItem, pageIndex + 1, 0); } }, _findBestPageToAppend(startPage) { const itemsPerPage = this.columnsPerPage * this.rowsPerPage; for (let i = startPage; i < this._pages.length; i++) { const visibleItems = this._pages[i].visibleChildren; if (visibleItems.length < itemsPerPage) return i; } return this._pages.length; }, updateIconSize() { const iconSize = this._findBestIconSize(); if (this._iconSize !== iconSize) { this._iconSize = iconSize; for (const child of this._container) child.icon.setIconSize(iconSize); this.notify('icon-size'); } }, };