/** * V-Shell (Vertical Workspaces) * appDisplay.js * * @author GdH * @copyright 2022 - 2024 * @license GPL-3.0 * */ 'use strict'; import Clutter from 'gi://Clutter'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Graphene from 'gi://Graphene'; import Meta from 'gi://Meta'; import Pango from 'gi://Pango'; import Shell from 'gi://Shell'; import St from 'gi://St'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'; import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; import * as PageIndicators from 'resource:///org/gnome/shell/ui/pageIndicators.js'; import { IconSize } from './iconGrid.js'; let Me; let opt; // gettext let _; let _appDisplay; let _timeouts; const APP_ICON_TITLE_EXPAND_TIME = 200; const APP_ICON_TITLE_COLLAPSE_TIME = 100; const shellVersion46 = !Clutter.Container; // Container has been removed in 46 function _getCategories(info) { let categoriesStr = info.get_categories(); if (!categoriesStr) return []; return categoriesStr.split(';'); } function _listsIntersect(a, b) { for (let itemA of a) { if (b.includes(itemA)) return true; } return false; } export const AppDisplayModule = class { constructor(me) { Me = me; opt = Me.opt; _ = Me.gettext; _appDisplay = Main.overview._overview.controls._appDisplay; this._firstActivation = true; this.moduleEnabled = false; this._overrides = null; this._appSystemStateConId = 0; this._appGridLayoutConId = 0; this._origAppViewItemAcceptDrop = null; this._updateFolderIcons = 0; } cleanGlobals() { Me = null; opt = null; _ = null; _appDisplay = null; } update(reset) { this._removeTimeouts(); this.moduleEnabled = opt.get('appDisplayModule'); const conflict = false; reset = reset || !this.moduleEnabled || conflict; // don't touch the original code if module disabled if (reset && !this._firstActivation) { this._disableModule(); this.moduleEnabled = false; } else if (!reset) { this._firstActivation = false; this._activateModule(); } if (reset && this._firstActivation) { this.moduleEnabled = false; console.debug(' AppDisplayModule - Keeping untouched'); } } _activateModule() { Me.Modules.iconGridModule.update(); if (!this._overrides) this._overrides = new Me.Util.Overrides(); _timeouts = {}; this._applyOverrides(); this._updateAppDisplay(); _appDisplay.add_style_class_name('app-display-46'); console.debug(' AppDisplayModule - Activated'); } _disableModule() { Me.Modules.iconGridModule.update(true); if (this._overrides) this._overrides.removeAll(); this._overrides = null; const reset = true; this._updateAppDisplay(reset); this._restoreOverviewGroup(); _appDisplay.remove_style_class_name('app-display-46'); console.debug(' AppDisplayModule - Disabled'); } _removeTimeouts() { if (_timeouts) { Object.values(_timeouts).forEach(t => { if (t) GLib.source_remove(t); }); _timeouts = null; } } _applyOverrides() { // Common/appDisplay // this._overrides.addOverride('BaseAppViewCommon', AppDisplay.BaseAppView.prototype, BaseAppViewCommon); // instead of overriding inaccessible BaseAppView class, we override its subclasses - AppDisplay and FolderView this._overrides.addOverride('BaseAppViewCommonApp', AppDisplay.AppDisplay.prototype, BaseAppViewCommon); this._overrides.addOverride('AppDisplay', AppDisplay.AppDisplay.prototype, AppDisplayCommon); this._overrides.addOverride('AppViewItem', AppDisplay.AppViewItem.prototype, AppViewItemCommon); this._overrides.addOverride('AppGridCommon', AppDisplay.AppGrid.prototype, AppGridCommon); this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIcon); if (opt.ORIENTATION) { this._overrides.removeOverride('AppGridLayoutHorizontal'); this._overrides.addOverride('AppGridLayoutVertical', _appDisplay._appGridLayout, BaseAppViewGridLayoutVertical); } else { this._overrides.removeOverride('AppGridLayoutVertical'); this._overrides.addOverride('AppGridLayoutHorizontal', _appDisplay._appGridLayout, BaseAppViewGridLayoutHorizontal); } // Custom folders this._overrides.addOverride('BaseAppViewCommonFolder', AppDisplay.FolderView.prototype, BaseAppViewCommon); this._overrides.addOverride('FolderView', AppDisplay.FolderView.prototype, FolderView); this._overrides.addOverride('AppFolderDialog', AppDisplay.AppFolderDialog.prototype, AppFolderDialog); this._overrides.addOverride('FolderIcon', AppDisplay.FolderIcon.prototype, FolderIcon); // Prevent changing grid page size when showing/hiding _pageIndicators this._overrides.addOverride('PageIndicators', PageIndicators.PageIndicators.prototype, PageIndicatorsCommon); } _updateAppDisplay(reset) { const orientation = reset ? Clutter.Orientation.HORIZONTAL : opt.ORIENTATION; BaseAppViewCommon._adaptForOrientation.bind(_appDisplay)(orientation); this._updateFavoritesConnection(reset); _appDisplay.visible = true; if (reset) { _appDisplay._grid.layoutManager.fixedIconSize = -1; _appDisplay._grid.layoutManager.allow_incomplete_pages = true; _appDisplay._grid._currentMode = -1; _appDisplay._grid.setGridModes(); _appDisplay._grid.set_style(''); _appDisplay._prevPageArrow.set_scale(1, 1); _appDisplay._nextPageArrow.set_scale(1, 1); if (this._appGridLayoutConId) { global.settings.disconnect(this._appGridLayoutConId); this._appGridLayoutConId = 0; } this._repopulateAppDisplay(reset); } else { _appDisplay._grid._currentMode = -1; // update grid on layout reset if (!this._appGridLayoutConId) this._appGridLayoutConId = global.settings.connect('changed::app-picker-layout', this._updateLayout.bind(this)); // avoid resetting appDisplay before startup animation // x11 shell restart skips startup animation if (!Main.layoutManager._startingUp) { this._repopulateAppDisplay(); } else if (Main.layoutManager._startingUp && Meta.is_restart()) { _timeouts.three = GLib.idle_add(GLib.PRIORITY_LOW, () => { this._repopulateAppDisplay(); _timeouts.three = 0; return GLib.SOURCE_REMOVE; }); } } } _updateFavoritesConnection(reset) { if (!reset) { if (!this._appSystemStateConId && opt.APP_GRID_INCLUDE_DASH >= 3) { this._appSystemStateConId = Shell.AppSystem.get_default().connect( 'app-state-changed', () => { this._updateFolderIcons = true; _appDisplay._redisplay(); } ); } } else if (this._appSystemStateConId) { Shell.AppSystem.get_default().disconnect(this._appSystemStateConId); this._appSystemStateConId = 0; } } _restoreOverviewGroup() { Main.overview.dash.showAppsButton.checked = false; Main.layoutManager.overviewGroup.opacity = 255; Main.layoutManager.overviewGroup.scale_x = 1; Main.layoutManager.overviewGroup.scale_y = 1; Main.layoutManager.overviewGroup.hide(); _appDisplay.translation_x = 0; _appDisplay.translation_y = 0; _appDisplay.visible = true; _appDisplay.opacity = 255; } _updateLayout(settings, key) { // Reset the app grid only if the user layout has been completely removed if (!settings.get_value(key).deep_unpack().length) { this._repopulateAppDisplay(); } } _repopulateAppDisplay(reset = false, callback) { // Remove all icons so they can be re-created with the current configuration // Updating appGrid content while rebasing extensions when session is locked makes no sense (relevant for GS version < 46) if (!Main.sessionMode.isLocked) AppDisplayCommon.removeAllItems.bind(_appDisplay)(); // appDisplay disabled if (reset) { _appDisplay._redisplay(); return; } _appDisplay._readyToRedisplay = true; _appDisplay._redisplay(); // Setting OffscreenRedirect should improve performance when opacity transitions are used _appDisplay.offscreen_redirect = Clutter.OffscreenRedirect.ALWAYS; if (opt.APP_GRID_PERFORMANCE) this._realizeAppDisplay(callback); else if (callback) callback(); } _realizeAppDisplay(callback) { // Workaround - silently realize appDisplay // The realization takes some time and affects animations during the first use // If we do it invisibly before the user needs the app grid, it can improve the user's experience _appDisplay.opacity = 1; this._exposeAppGrid(); _appDisplay._redisplay(); this._exposeAppFolders(); // Let the main loop process our changes before we continue _timeouts.updateAppGrid = GLib.idle_add(GLib.PRIORITY_LOW, () => { this._restoreAppGrid(); Me._resetInProgress = false; if (callback) callback(); _timeouts.updateAppGrid = 0; return GLib.SOURCE_REMOVE; }); } _exposeAppGrid() { const overviewGroup = Main.layoutManager.overviewGroup; if (!overviewGroup.visible) { // scale down the overviewGroup so it don't cover uiGroup overviewGroup.scale_y = 0.001; // make it invisible to the eye, but visible for the renderer overviewGroup.opacity = 1; // if overview is hidden, show it overviewGroup.visible = true; } } _restoreAppGrid() { if (opt.APP_GRID_PERFORMANCE) this._hideAppFolders(); const overviewGroup = Main.layoutManager.overviewGroup; if (!Main.overview._shown) overviewGroup.hide(); overviewGroup.scale_y = 1; overviewGroup.opacity = 255; _appDisplay.opacity = 0; _appDisplay.visible = false; } _exposeAppFolders() { _appDisplay._folderIcons.forEach(d => { d._ensureFolderDialog(); d._dialog.scale_y = 0.0001; d._dialog.show(); d._dialog._updateFolderSize(); }); } _hideAppFolders() { _appDisplay._folderIcons.forEach(d => { if (d._dialog) { d._dialog.hide(); d._dialog.scale_y = 1; } }); } }; function _getViewFromIcon(icon) { icon = icon._sourceItem ? icon._sourceItem : icon; for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) { if (parent instanceof AppDisplay.AppDisplay || parent instanceof AppDisplay.FolderView) { return parent; } } return null; } const AppDisplayCommon = { _ensureDefaultFolders() { // disable creation of default folders if user deleted them }, removeAllItems() { this._orderedItems.slice().forEach(item => { if (item._dialog) Main.layoutManager.overviewGroup.remove_child(item._dialog); this._removeItem(item); item.destroy(); }); this._folderIcons = []; }, // apps load adapted for custom sorting and including dash items _loadApps() { let appIcons = []; const runningApps = Shell.AppSystem.get_default().get_running().map(a => a.id); this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => { try { appInfo.get_id(); // catch invalid file encodings } catch (e) { return false; } const appIsRunning = runningApps.includes(appInfo.get_id()); const appIsFavorite = this._appFavorites.isFavorite(appInfo.get_id()); const excludeApp = (opt.APP_GRID_EXCLUDE_RUNNING && appIsRunning) || (opt.APP_GRID_EXCLUDE_FAVORITES && appIsFavorite); return this._parentalControlsManager.shouldShowApp(appInfo) && !excludeApp; }); let apps = this._appInfoList.map(app => app.get_id()); let appSys = Shell.AppSystem.get_default(); const appsInsideFolders = new Set(); this._folderIcons = []; if (!opt.APP_GRID_USAGE) { let folders = this._folderSettings.get_strv('folder-children'); folders.forEach(id => { let path = `${this._folderSettings.path}folders/${id}/`; let icon = this._items.get(id); if (!icon) { icon = new AppDisplay.FolderIcon(id, path, this); icon.connect('apps-changed', () => { this._redisplay(); this._savePages(); }); icon.connect('notify::pressed', () => { if (icon.pressed) this.updateDragFocus(icon); }); } else if (this._updateFolderIcons && opt.APP_GRID_EXCLUDE_RUNNING) { // if any app changed its running state, update folder icon icon.icon.update(); } // remove empty folder icons if (!icon.visible) { icon.destroy(); return; } appIcons.push(icon); this._folderIcons.push(icon); icon.getAppIds().forEach(appId => appsInsideFolders.add(appId)); }); } // reset request to update active icon this._updateFolderIcons = false; // Allow dragging of the icon only if the Dash would accept a drop to // change favorite-apps. There are no other possible drop targets from // the app picker, so there's no other need for a drag to start, // at least on single-monitor setups. // This also disables drag-to-launch on multi-monitor setups, // but we hope that is not used much. const isDraggable = global.settings.is_writable('favorite-apps') || global.settings.is_writable('app-picker-layout'); apps.forEach(appId => { if (!opt.APP_GRID_USAGE && appsInsideFolders.has(appId)) return; let icon = this._items.get(appId); if (!icon) { let app = appSys.lookup_app(appId); icon = new AppDisplay.AppIcon(app, { isDraggable }); icon.connect('notify::pressed', () => { if (icon.pressed) this.updateDragFocus(icon); }); } appIcons.push(icon); }); // At last, if there's a placeholder available, add it if (this._placeholder) appIcons.push(this._placeholder); return appIcons; }, _onDragBegin(overview, source) { // let sourceId; // support active preview icons if (source._sourceItem) { // sourceId = source._sourceFolder._id; source = source._sourceItem; } /* else { sourceId = source.id; }*/ // Prevent switching page when an item on another page is selected // by removing the focus from all icons // This is an upstream bug // this.selectApp(sourceId); this.grab_key_focus(); this._dragMonitor = { dragMotion: this._onDragMotion.bind(this), dragDrop: this._onDragDrop.bind(this), }; DND.addDragMonitor(this._dragMonitor); this._appGridLayout.showPageIndicators(); this._dragFocus = null; this._swipeTracker.enabled = false; // When dragging from a folder dialog, the dragged app icon doesn't // exist in AppDisplay. We work around that by adding a placeholder // icon that is either destroyed on cancel, or becomes the effective // new icon when dropped. if (/* AppDisplay.*/_getViewFromIcon(source) instanceof AppDisplay.FolderView || (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(source.id))) this._ensurePlaceholder(source); }, _ensurePlaceholder(source) { if (this._placeholder) return; if (source._sourceItem) source = source._sourceItem; const appSys = Shell.AppSystem.get_default(); const app = appSys.lookup_app(source.id); const isDraggable = global.settings.is_writable('favorite-apps') || global.settings.is_writable('app-picker-layout'); this._placeholder = new AppDisplay.AppIcon(app, { isDraggable }); this._placeholder.connect('notify::pressed', () => { if (this._placeholder?.pressed) this.updateDragFocus(this._placeholder); }); this._placeholder.scaleAndFade(); this._redisplay(); }, // accept source from active folder preview acceptDrop(source) { if (opt.APP_GRID_USAGE) return false; if (source._sourceItem) source = source._sourceItem; if (!this._acceptDropCommon(source)) return false; this._savePages(); const view = /* AppDisplay.*/_getViewFromIcon(source); if (view instanceof AppDisplay.FolderView) view.removeApp(source.app); if (this._currentDialog) this._currentDialog.popdown(); if (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(source.id)) this._appFavorites.removeFavorite(source.id); return true; }, _savePages() { // Skip saving pages when search app grid mode is active // and the grid is showing search results if (Main.overview._overview.controls._origAppGridContent) return; const pages = []; for (let i = 0; i < this._grid.nPages; i++) { const pageItems = this._grid.getItemsAtPage(i).filter(c => c.visible); const pageData = {}; pageItems.forEach((item, index) => { pageData[item.id] = { position: GLib.Variant.new_int32(index), }; }); pages.push(pageData); } this._pageManager.pages = pages; }, }; const BaseAppViewCommon = { after__init() { // Only folders can run this init this._isFolder = true; this._adaptForOrientation(opt.ORIENTATION, true); // Because the original class prototype is not exported, we need to inject every instance const overrides = new Me.Util.Overrides(); if (opt.ORIENTATION) { overrides.addOverride('FolderGridLayoutVertical', this._appGridLayout, BaseAppViewGridLayoutVertical); this._pageIndicators.set_style('margin-right: 12px;'); } else { overrides.addOverride('FolderGridLayoutHorizontal', this._appGridLayout, BaseAppViewGridLayoutHorizontal); this._pageIndicators.set_style('margin-bottom: 12px;'); } }, _adaptForOrientation(orientation, folder) { const vertical = !!orientation; this._grid.layoutManager.fixedIconSize = folder ? opt.APP_GRID_FOLDER_ICON_SIZE : opt.APP_GRID_ICON_SIZE; this._grid.layoutManager._orientation = orientation; this._orientation = orientation; this._swipeTracker.orientation = orientation; this._swipeTracker._reset(); this._adjustment = vertical ? this._scrollView.get_vscroll_bar().adjustment : this._scrollView.get_hscroll_bar().adjustment; this._prevPageArrow.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 }); this._prevPageArrow.rotation_angle_z = vertical ? 90 : 0; this._nextPageArrow.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 }); this._nextPageArrow.rotation_angle_z = vertical ? 90 : 0; const pageIndicators = this._pageIndicators; pageIndicators.vertical = vertical; this._box.vertical = !vertical; pageIndicators.x_expand = !vertical; pageIndicators.y_align = vertical ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START; pageIndicators.x_align = vertical ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER; this._grid.layoutManager.allow_incomplete_pages = folder ? false : opt.APP_GRID_ALLOW_INCOMPLETE_PAGES; const spacing = folder ? opt.APP_GRID_FOLDER_SPACING : opt.APP_GRID_SPACING; this._grid.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`); if (vertical) { this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); if (!this._scrollConId) { this._scrollConId = this._adjustment.connect('notify::value', adj => { const value = adj.value / adj.page_size; this._pageIndicators.setCurrentPosition(value); }); } pageIndicators.remove_style_class_name('page-indicators-horizontal'); pageIndicators.add_style_class_name('page-indicators-vertical'); this._prevPageIndicator.add_style_class_name('prev-page-indicator'); this._nextPageIndicator.add_style_class_name('next-page-indicator'); this._nextPageArrow.translationY = 0; this._prevPageArrow.translationY = 0; this._nextPageIndicator.translationX = 0; this._prevPageIndicator.translationX = 0; } else { this._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER); if (this._scrollConId) { this._adjustment.disconnect(this._scrollConId); this._scrollConId = 0; } pageIndicators.remove_style_class_name('page-indicators-vertical'); pageIndicators.add_style_class_name('page-indicators-horizontal'); this._prevPageIndicator.remove_style_class_name('prev-page-indicator'); this._nextPageIndicator.remove_style_class_name('next-page-indicator'); this._nextPageArrow.translationX = 0; this._prevPageArrow.translationX = 0; this._nextPageIndicator.translationY = 0; this._prevPageIndicator.translationY = 0; } const scale = opt.APP_GRID_SHOW_PAGE_ARROWS ? 1 : 0; this._prevPageArrow.set_scale(scale, scale); this._nextPageArrow.set_scale(scale, scale); }, _sortItemsByName(items) { items.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); }, _updateItemPositions(icons, allowIncompletePages = false) { // Avoid recursion when relocating icons this._grid.layoutManager._skipRelocateSurplusItems = true; const { itemsPerPage } = this._grid; icons.slice().forEach((icon, index) => { const [currentPage, currentPosition] = this._grid.layoutManager.getItemPosition(icon); let page, position; if (allowIncompletePages) { [page, position] = this._getItemPosition(icon); } else { page = Math.floor(index / itemsPerPage); position = index % itemsPerPage; } if (currentPage !== page || currentPosition !== position) { this._moveItem(icon, page, position); } }); this._grid.layoutManager._skipRelocateSurplusItems = false; // Disable animating the icons to their new positions // since it can cause glitches when the app grid search mode is active // and many icons are repositioning at once this._grid.layoutManager._shouldEaseItems = false; }, // Adds sorting options _redisplay() { // different options for main app grid and app folders const thisIsFolder = this instanceof AppDisplay.FolderView; const thisIsAppDisplay = !thisIsFolder; // When an app was dragged from a folder and dropped to the main grid // folders (if exist) need to be redisplayed even if we temporary block it for the appDisplay this._folderIcons?.forEach(icon => { icon.view._redisplay(); }); // Avoid unwanted updates if (thisIsAppDisplay && !this._readyToRedisplay) return; const oldApps = this._orderedItems.slice(); const oldAppIds = oldApps.map(icon => icon.id); const newApps = this._loadApps(); const newAppIds = newApps.map(icon => icon.id); const addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id)); const removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id)); // Don't update folder without dialog if its content didn't change if (!addedApps.length && !removedApps.length && thisIsFolder && !this.get_parent()) return; // Remove old app icons removedApps.forEach(icon => { this._removeItem(icon); icon.destroy(); }); // For the main app grid only let allowIncompletePages = thisIsAppDisplay && opt.APP_GRID_ALLOW_INCOMPLETE_PAGES; const customOrder = !((opt.APP_GRID_ORDER && thisIsAppDisplay) || (opt.APP_FOLDER_ORDER && thisIsFolder)); if (!customOrder) { allowIncompletePages = false; // Sort by name this._sortItemsByName(newApps); // Sort by usage if ((opt.APP_GRID_USAGE && thisIsAppDisplay) || (opt.APP_FOLDER_USAGE && thisIsFolder)) { newApps.sort((a, b) => Shell.AppUsage.get_default().compare(a.app?.id, b.app?.id)); } // Sort favorites first if (!opt.APP_GRID_EXCLUDE_FAVORITES && opt.APP_GRID_DASH_FIRST) { const fav = Object.keys(this._appFavorites._favorites); newApps.sort((a, b) => { let aFav = fav.indexOf(a.id); if (aFav < 0) aFav = 999; let bFav = fav.indexOf(b.id); if (bFav < 0) bFav = 999; return bFav < aFav; }); } // Sort running first if (!opt.APP_GRID_EXCLUDE_RUNNING && opt.APP_GRID_DASH_FIRST) { newApps.sort((a, b) => a.app?.get_state() !== Shell.AppState.RUNNING && b.app?.get_state() === Shell.AppState.RUNNING); } // Sort folders first if (thisIsAppDisplay && opt.APP_GRID_FOLDERS_FIRST) newApps.sort((a, b) => b._folder && !a._folder); // Sort folders last else if (thisIsAppDisplay && opt.APP_GRID_FOLDERS_LAST) newApps.sort((a, b) => a._folder && !b._folder); } else { // Sort items according to the custom order stored in pageManager newApps.sort(this._compareItems.bind(this)); } // Add new app icons to the grid newApps.forEach(icon => { const [page, position] = this._grid.getItemPosition(icon); if (page === -1 && position === -1) this._addItem(icon, -1, -1); }); // When a placeholder icon was added to the custom sorted grid during DND from a folder // update its initial position on the page if (customOrder) newApps.sort(this._compareItems.bind(this)); this._orderedItems = newApps; // Update icon positions if needed this._updateItemPositions(this._orderedItems, allowIncompletePages); // Relocate items with invalid positions if (thisIsAppDisplay) { const nPages = this._grid.layoutManager.nPages; for (let pageIndex = 0; pageIndex < nPages; pageIndex++) this._grid.layoutManager._relocateSurplusItems(pageIndex); } this.emit('view-loaded'); }, _canAccept(source) { return source instanceof AppDisplay.AppViewItem; }, // this method is replacing BaseAppVew.acceptDrop which can't be overridden directly _acceptDropCommon(source) { const dropTarget = this._dropTarget; delete this._dropTarget; if (!this._canAccept(source)) return false; if (dropTarget === this._prevPageIndicator || dropTarget === this._nextPageIndicator) { let increment; increment = dropTarget === this._prevPageIndicator ? -1 : 1; const { currentPage, nPages } = this._grid; const page = Math.min(currentPage + increment, nPages); const position = page < nPages ? -1 : 0; this._moveItem(source, page, position); this.goToPage(page); } else if (this._delayedMoveData) { // Dropped before the icon was moved const { page, position } = this._delayedMoveData; try { this._moveItem(source, page, position); } catch (e) { console.warn(`Warning:${e}`); } this._removeDelayedMove(); } return true; }, // support active preview icons _onDragMotion(dragEvent) { if (!(dragEvent.source instanceof AppDisplay.AppViewItem)) return DND.DragMotionResult.CONTINUE; if (dragEvent.source._sourceItem) dragEvent.source = dragEvent.source._sourceItem; const appIcon = dragEvent.source; if (appIcon instanceof AppDisplay.AppViewItem) { if (!this._dragMaybeSwitchPageImmediately(dragEvent)) { // Two ways of switching pages during DND: // 1) When "bumping" the cursor against the monitor edge, we switch // page immediately. // 2) When hovering over the next-page indicator for a certain time, // we also switch page. const { targetActor } = dragEvent; if (targetActor === this._prevPageIndicator || targetActor === this._nextPageIndicator) this._maybeSetupDragPageSwitchInitialTimeout(dragEvent); else this._resetDragPageSwitch(); } } const thisIsFolder = this instanceof AppDisplay.FolderView; const thisIsAppDisplay = !thisIsFolder; // Prevent reorganizing the main app grid icons when an app folder is open and when sorting is not custom // For some reason in V-Shell the drag motion events propagate from folder to main grid, which is not a problem in default code - so test the open dialog if (!this._currentDialog && (!opt.APP_GRID_ORDER && thisIsAppDisplay) || (!opt.APP_FOLDER_ORDER && thisIsFolder)) this._maybeMoveItem(dragEvent); return DND.DragMotionResult.CONTINUE; }, }; const BaseAppViewGridLayoutHorizontal = { _getIndicatorsWidth(box) { const [width, height] = box.get_size(); const arrows = [ this._nextPageArrow, this._previousPageArrow, ]; let minArrowsWidth; minArrowsWidth = arrows.reduce( (previousWidth, accessory) => { const [min] = accessory.get_preferred_width(height); return Math.max(previousWidth, min); }, 0); minArrowsWidth = opt.APP_GRID_SHOW_PAGE_ARROWS ? minArrowsWidth : 0; const indicatorWidth = !this._grid._isFolder ? minArrowsWidth + ((width - minArrowsWidth) * (1 - opt.APP_GRID_PAGE_WIDTH_SCALE)) / 2 : minArrowsWidth + 6; return Math.round(indicatorWidth); }, vfunc_allocate(container, box) { const ltr = container.get_text_direction() !== Clutter.TextDirection.RTL; const indicatorsWidth = this._getIndicatorsWidth(box); const pageIndicatorsHeight = 20; // _appDisplay._pageIndicators.height is unstable, 20 is determined by the style const availHeight = box.get_height() - pageIndicatorsHeight; const vPadding = Math.round((availHeight - availHeight * opt.APP_GRID_PAGE_HEIGHT_SCALE) / 2); this._grid.indicatorsPadding = new Clutter.Margin({ left: indicatorsWidth, right: indicatorsWidth, top: vPadding + pageIndicatorsHeight, bottom: vPadding, }); this._scrollView.allocate(box); const leftBox = box.copy(); leftBox.x2 = leftBox.x1 + indicatorsWidth; const rightBox = box.copy(); rightBox.x1 = rightBox.x2 - indicatorsWidth; this._previousPageIndicator.allocate(ltr ? leftBox : rightBox); this._previousPageArrow.allocate_align_fill(ltr ? leftBox : rightBox, 0.5, 0.5, false, false); this._nextPageIndicator.allocate(ltr ? rightBox : leftBox); this._nextPageArrow.allocate_align_fill(ltr ? rightBox : leftBox, 0.5, 0.5, false, false); this._pageWidth = box.get_width(); // Center page arrow buttons this._previousPageArrow.translationY = pageIndicatorsHeight / 2; this._nextPageArrow.translationY = pageIndicatorsHeight / 2; // Reset page indicators vertical position this._nextPageIndicator.translationY = 0; this._previousPageIndicator.translationY = 0; }, }; const BaseAppViewGridLayoutVertical = { _getIndicatorsHeight(box) { const [width, height] = box.get_size(); const arrows = [ this._nextPageArrow, this._previousPageArrow, ]; let minArrowsHeight; minArrowsHeight = arrows.reduce( (previousHeight, accessory) => { const [min] = accessory.get_preferred_height(width); return Math.max(previousHeight, min); }, 0); minArrowsHeight = opt.APP_GRID_SHOW_PAGE_ARROWS ? minArrowsHeight : 0; const indicatorHeight = !this._grid._isFolder ? minArrowsHeight + ((height - minArrowsHeight) * (1 - opt.APP_GRID_PAGE_HEIGHT_SCALE)) / 2 : minArrowsHeight + 6; return Math.round(indicatorHeight); }, _syncPageIndicators() { if (!this._container) return; const { value } = this._pageIndicatorsAdjustment; const { top, bottom } = this._grid.indicatorsPadding; const topIndicatorOffset = -top * (1 - value); const bottomIndicatorOffset = bottom * (1 - value); this._previousPageIndicator.translationY = topIndicatorOffset; this._nextPageIndicator.translationY = bottomIndicatorOffset; const leftArrowOffset = -top * value; const rightArrowOffset = bottom * value; this._previousPageArrow.translationY = leftArrowOffset; this._nextPageArrow.translationY = rightArrowOffset; // Page icons this._translatePreviousPageIcons(value); this._translateNextPageIcons(value); if (this._grid.nPages > 0) { this._grid.getItemsAtPage(this._currentPage).forEach(icon => { icon.translationY = 0; }); } }, _translatePreviousPageIcons(value) { if (this._currentPage === 0) return; const pageHeight = this._grid.layoutManager._pageHeight; const previousPage = this._currentPage - 1; const icons = this._grid.getItemsAtPage(previousPage).filter(i => i.visible); if (icons.length === 0) return; const { top } = this._grid.indicatorsPadding; const { rowSpacing } = this._grid.layoutManager; const endIcon = icons[icons.length - 1]; let iconOffset; const currentPageOffset = pageHeight * this._currentPage; iconOffset = currentPageOffset - endIcon.allocation.y1 - endIcon.width + top - rowSpacing; for (const icon of icons) icon.translationY = iconOffset * value; }, _translateNextPageIcons(value) { if (this._currentPage >= this._grid.nPages - 1) return; const nextPage = this._currentPage + 1; const icons = this._grid.getItemsAtPage(nextPage).filter(i => i.visible); if (icons.length === 0) return; const { bottom } = this._grid.indicatorsPadding; const { rowSpacing } = this._grid.layoutManager; let iconOffset; const pageOffset = this._pageHeight * nextPage; iconOffset = pageOffset - icons[0].allocation.y1 - bottom + rowSpacing; for (const icon of icons) icon.translationY = iconOffset * value; }, vfunc_allocate(container, box) { const indicatorsHeight = this._getIndicatorsHeight(box); const pageIndicatorsWidth = 20; // _appDisplay._pageIndicators.width is not stable, 20 is determined by the style const availWidth = box.get_width() - pageIndicatorsWidth; const hPadding = Math.round((availWidth - availWidth * opt.APP_GRID_PAGE_WIDTH_SCALE) / 2); this._grid.indicatorsPadding = new Clutter.Margin({ top: indicatorsHeight, bottom: indicatorsHeight, left: hPadding + pageIndicatorsWidth, right: hPadding, }); this._scrollView.allocate(box); const topBox = box.copy(); topBox.y2 = topBox.y1 + indicatorsHeight; const bottomBox = box.copy(); bottomBox.y1 = bottomBox.y2 - indicatorsHeight; this._previousPageIndicator.allocate(topBox); this._previousPageArrow.allocate_align_fill(topBox, 0.5, 0.5, false, false); this._nextPageIndicator.allocate(bottomBox); this._nextPageArrow.allocate_align_fill(bottomBox, 0.5, 0.5, false, false); this._pageHeight = box.get_height(); // Center page arrow buttons this._previousPageArrow.translationX = pageIndicatorsWidth / 2; this._nextPageArrow.translationX = pageIndicatorsWidth / 2; // Reset page indicators vertical position this._nextPageIndicator.translationX = 0; this._previousPageIndicator.translationX = 0; }, }; const AppGridCommon = { _updatePadding() { const { rowSpacing, columnSpacing } = this.layoutManager; const padding = this._indicatorsPadding.copy(); padding.left += rowSpacing; padding.right += rowSpacing; padding.top += columnSpacing; padding.bottom += columnSpacing; this.layoutManager.pagePadding = padding; }, }; const FolderIcon = { after__init() { this.button_mask = St.ButtonMask.ONE | St.ButtonMask.TWO; if (shellVersion46) this.add_style_class_name('app-folder-46'); else this.add_style_class_name('app-folder-45'); }, open() { // Prevent switching page when an item on another page is selected GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { // Select folder icon to prevent switching page to the one with currently selected icon this._parentView._selectAppInternal(this._id); // Remove key focus from the selected icon to prevent switching page after dropping the removed folder icon on another page of the main grid this._parentView.grab_key_focus(); this._ensureFolderDialog(); this._dialog.popup(); }); }, vfunc_clicked() { this.open(); }, _canAccept(source) { if (!(source instanceof AppDisplay.AppIcon)) return false; const view = _getViewFromIcon(source); if (!view /* || !(view instanceof AppDisplay.AppDisplay)*/) return false; // Disable this test to allow the user to cancel the current DND by dropping the icon on its original source /* if (this._folder.get_strv('apps').includes(source.id)) return false;*/ return true; }, acceptDrop(source) { if (source._sourceItem) source = source._sourceItem; const accepted = AppViewItemCommon.acceptDrop.bind(this)(source); if (!accepted) return false; // If the icon is already in the folder (user dropped it back on the same folder), skip re-adding it if (this._folder.get_strv('apps').includes(source.id)) return true; this._onDragEnd(); this.view.addApp(source.app); return true; }, }; const FolderView = { _createGrid() { let grid = new FolderGrid(); grid._view = this; return grid; }, createFolderIcon(size) { const layout = new Clutter.GridLayout({ row_homogeneous: true, column_homogeneous: true, }); let icon = new St.Widget({ layout_manager: layout, x_align: Clutter.ActorAlign.CENTER, style: `width: ${size}px; height: ${size}px;`, }); const numItems = this._orderedItems.length; // decide what number of icons switch to 3x3 grid // APP_GRID_FOLDER_ICON_GRID: 3 -> more than 4 // : 4 -> more than 8 const threshold = opt.APP_GRID_FOLDER_ICON_GRID % 3 ? 8 : 4; const gridSize = opt.APP_GRID_FOLDER_ICON_GRID > 2 && numItems > threshold ? 3 : 2; const FOLDER_SUBICON_FRACTION = gridSize === 2 ? 0.4 : 0.27; let subSize = Math.floor(FOLDER_SUBICON_FRACTION * size); let rtl = icon.get_text_direction() === Clutter.TextDirection.RTL; for (let i = 0; i < gridSize * gridSize; i++) { const style = `width: ${subSize}px; height: ${subSize}px;`; let bin = new St.Bin({ style, reactive: true }); bin.pivot_point = new Graphene.Point({ x: 0.5, y: 0.5 }); if (i < numItems) { if (!opt.APP_GRID_ACTIVE_PREVIEW) { bin.child = this._orderedItems[i].app.create_icon_texture(subSize); } else { const app = this._orderedItems[i].app; const child = new AppDisplay.AppIcon(app, { setSizeManually: true, showLabel: false, }); child._sourceItem = this._orderedItems[i]; child._sourceFolder = this; child.icon.style_class = ''; child.set_style_class_name(''); child.icon.set_style('margin: 0; padding: 0;'); child._dot.set_style('margin-bottom: 1px;'); child.icon.setIconSize(subSize); child._canAccept = () => false; bin.child = child; bin.connect('enter-event', () => { bin.ease({ duration: 100, translation_y: -3, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); }); bin.connect('leave-event', () => { bin.ease({ duration: 100, translation_y: 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); }); } } layout.attach(bin, rtl ? (i + 1) % gridSize : i % gridSize, Math.floor(i / gridSize), 1, 1); } return icon; }, _loadApps() { this._apps = []; const excludedApps = this._folder.get_strv('excluded-apps'); const appSys = Shell.AppSystem.get_default(); const addAppId = appId => { if (excludedApps.includes(appId)) return; if (opt.APP_GRID_EXCLUDE_FAVORITES && this._appFavorites.isFavorite(appId)) return; const app = appSys.lookup_app(appId); if (!app) return; if (opt.APP_GRID_EXCLUDE_RUNNING) { const runningApps = Shell.AppSystem.get_default().get_running().map(a => a.id); if (runningApps.includes(appId)) return; } if (!this._parentalControlsManager.shouldShowApp(app.get_app_info())) return; if (this._apps.indexOf(app) !== -1) return; this._apps.push(app); }; const folderApps = this._folder.get_strv('apps'); folderApps.forEach(addAppId); const folderCategories = this._folder.get_strv('categories'); const appInfos = this._parentView.getAppInfos(); appInfos.forEach(appInfo => { let appCategories = /* AppDisplay.*/_getCategories(appInfo); if (!_listsIntersect(folderCategories, appCategories)) return; addAppId(appInfo.get_id()); }); let items = []; this._apps.forEach(app => { let icon = this._items.get(app.get_id()); if (!icon) icon = new AppDisplay.AppIcon(app); items.push(icon); }); return items; }, acceptDrop(source) { /* if (!BaseAppViewCommon.acceptDrop.bind(this)(source)) return false;*/ if (opt.APP_FOLDER_ORDER) return false; if (source._sourceItem) source = source._sourceItem; if (!this._acceptDropCommon(source)) return false; const folderApps = this._orderedItems.map(item => item.id); this._folder.set_strv('apps', folderApps); return true; }, }; const FolderGrid = GObject.registerClass({ // Registered name should be unique GTypeName: `FolderGrid${Math.floor(Math.random() * 1000)}`, }, class FolderGrid extends AppDisplay.AppGrid { _init() { super._init({ allow_incomplete_pages: false, // For adaptive size (0), set the numbers high enough to fit all the icons // to avoid splitting the icons to pages upon creating the grid columns_per_page: 20, rows_per_page: 20, page_halign: Clutter.ActorAlign.CENTER, page_valign: Clutter.ActorAlign.CENTER, }); this.layoutManager._isFolder = true; this._isFolder = true; const spacing = opt.APP_GRID_FOLDER_SPACING; this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`); this.layoutManager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE; this.setGridModes([ { columns: 20, rows: 20, }, ]); } _updatePadding() { const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); const padding = this._indicatorsPadding.copy(); const pageIndicatorSize = opt.ORIENTATION ? this._view._pageIndicators.get_preferred_width(1000)[1] / scaleFactor : this._view._pageIndicators.get_preferred_height(1000)[1] / scaleFactor; Math.round(Math.min(...this._view._pageIndicators.get_size()));// / scaleFactor);// ~28; padding.left = opt.ORIENTATION ? pageIndicatorSize : 0; padding.right = 0; padding.top = opt.ORIENTATION ? 0 : pageIndicatorSize; padding.bottom = 0; this.layoutManager.pagePadding = padding; } }); const FOLDER_DIALOG_ANIMATION_TIME = 200; // AppDisplay.FOLDER_DIALOG_ANIMATION_TIME const AppFolderDialog = { // injection to _init() after__init() { // GS 46 changed the aligning to CENTER which restricts max folder dialog size this._viewBox.set({ x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.FILL, }); // delegate this dialog to the FolderIcon._view // so its _createFolderIcon function can update the dialog if folder content changed this._view._dialog = this; // right click into the folder popup should close it this.child.reactive = true; const clickAction = new Clutter.ClickAction(); clickAction.connect('clicked', act => { if (act.get_button() === Clutter.BUTTON_PRIMARY) return Clutter.EVENT_STOP; const [x, y] = clickAction.get_coords(); const actor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y); // if it's not entry for editing folder title if (actor !== this._entry) this.popdown(); return Clutter.EVENT_STOP; }); this.child.add_action(clickAction); }, after__addFolderNameEntry() { // edit-folder-button class has been replaced with icon-button class which is not transparent in 46 this._editButton.add_style_class_name('edit-folder-button'); // Edit button this._removeButton = new St.Button({ style_class: 'icon-button edit-folder-button', button_mask: St.ButtonMask.ONE, toggle_mode: false, reactive: true, can_focus: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, child: new St.Icon({ icon_name: 'user-trash-symbolic', icon_size: 16, }), }); this._removeButton.connect('clicked', () => { if (Date.now() - this._removeButton._lastClick < Clutter.Settings.get_default().double_click_time) { // Close dialog to avoid crashes this._isOpen = false; this._grabHelper.ungrab({ actor: this }); this.emit('open-state-changed', false); this.hide(); this._popdownCallbacks.forEach(func => func()); this._popdownCallbacks = []; _appDisplay.ease({ opacity: 255, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); // Reset all keys to delete the relocatable schema this._view._deletingFolder = true; // Upstream property let keys = this._folder.settings_schema.list_keys(); for (const key of keys) this._folder.reset(key); let settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' }); let folders = settings.get_strv('folder-children'); folders.splice(folders.indexOf(this._view._id), 1); // remove all abandoned folders (usually my own garbage and unwanted default folders...) /* const appFolders = _appDisplay._folderIcons.map(icon => icon._id); folders.forEach(folder => { if (!appFolders.includes(folder)) { folders.splice(folders.indexOf(folder._id), 1); } });*/ settings.set_strv('folder-children', folders); this._view._deletingFolder = false; return; } this._removeButton._lastClick = Date.now(); }); this._entryBox.add_child(this._removeButton); this._entryBox.set_child_at_index(this._removeButton, 0); this._closeButton = new St.Button({ style_class: 'icon-button edit-folder-button', button_mask: St.ButtonMask.ONE, toggle_mode: false, reactive: true, can_focus: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, child: new St.Icon({ icon_name: 'window-close-symbolic', icon_size: 16, }), }); this._closeButton.connect('clicked', () => { this.popdown(); }); this._entryBox.add_child(this._closeButton); }, popup() { if (this._isOpen) return; this._isOpen = this._grabHelper.grab({ actor: this, focus: this._editButton, onUngrab: () => this.popdown(), }); if (!this._isOpen) return; this.get_parent().set_child_above_sibling(this, null); // _zoomAndFadeIn() is called from the dialog's allocate() this._needsZoomAndFade = true; this.show(); // force update folder size this._folderAreaBox = null; this._updateFolderSize(); this.emit('open-state-changed', true); }, _setupPopdownTimeout() { if (this._popdownTimeoutId > 0) return; // This timeout is handled in the original code and removed in _onDestroy() // All dialogs are destroyed on extension disable() this._popdownTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this._popdownTimeoutId = 0; // Following line fixes upstream bug // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6164 this._view._onDragEnd(); this.popdown(); return GLib.SOURCE_REMOVE; }); }, vfunc_allocate(box) { this._updateFolderSize(); // super.allocate(box) St.Bin.prototype.vfunc_allocate.bind(this)(box); // Override any attempt to resize the folder dialog, that happens when some child gets wild // Re-allocate the child only if necessary, because it terminates grid animations if (this._width && this._height && (this._width !== this.child.width || this._height !== this.child.height)) this._allocateChild(); // We can only start zooming after receiving an allocation if (this._needsZoomAndFade) this._zoomAndFadeIn(); }, _allocateChild() { const childBox = new Clutter.ActorBox(); childBox.set_size(this._width, this._height); this.child.allocate(childBox); }, // Note that the appDisplay may be off-screen so its coordinates may be shifted // However, for _updateFolderSize() it doesn't matter // and when _zoomAndFadeIn() is called, appDisplay is on the right place _getFolderAreaBox() { const appDisplay = this._source._parentView; const folderAreaBox = appDisplay.get_allocation_box().copy(); const searchEntryHeight = opt.SHOW_SEARCH_ENTRY ? Main.overview._overview.controls._searchEntryBin.height : 0; folderAreaBox.y1 -= searchEntryHeight; // _zoomAndFadeIn() needs an absolute position within a multi-monitor workspace const monitorGeometry = global.display.get_monitor_geometry(global.display.get_primary_monitor()); folderAreaBox.x1 += monitorGeometry.x; folderAreaBox.x2 += monitorGeometry.x; folderAreaBox.y1 += monitorGeometry.y; folderAreaBox.y2 += monitorGeometry.y; return folderAreaBox; }, _updateFolderSize() { const view = this._view; const nItems = view._orderedItems.length; const [firstItem] = view._grid.layoutManager._container; if (!firstItem) return; const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); const margin = 18; // see stylesheet .app-folder-dialog-container; const folderAreaBox = this._getFolderAreaBox(); const maxDialogWidth = folderAreaBox.get_width() / scaleFactor; const maxDialogHeight = folderAreaBox.get_height() / scaleFactor; // We can't build folder if the available space is not available if (!isFinite(maxDialogWidth) || !isFinite(maxDialogHeight) || !maxDialogWidth || !maxDialogHeight) return; // We don't need to recalculate grid if nothing changed if ( this._folderAreaBox?.get_width() === folderAreaBox.get_width() && this._folderAreaBox?.get_height() === folderAreaBox.get_height() && nItems === this._nItems ) return; const layoutManager = view._grid.layoutManager; const spacing = opt.APP_GRID_FOLDER_SPACING; const padding = 40; const titleBoxHeight = Math.round(this._entryBox.get_preferred_height(-1)[1] / scaleFactor); // ~75 const minDialogWidth = Math.max(640, Math.round(this._entryBox.get_preferred_width(-1)[1] / scaleFactor + 2 * margin)); const navigationArrowsSize = // padding + one arrow width is sufficient for both arrows Math.round(view._nextPageArrow.get_preferred_width(-1)[1] / scaleFactor); const pageIndicatorSize = Math.round(Math.min(...view._pageIndicators.get_size()) / scaleFactor);// ~28; const horizontalNavigation = opt.ORIENTATION ? pageIndicatorSize : navigationArrowsSize; // either add padding or arrows const verticalNavigation = opt.ORIENTATION ? navigationArrowsSize : pageIndicatorSize; // Horizontal size const baseWidth = horizontalNavigation + 3 * padding + 2 * margin; const maxGridPageWidth = maxDialogWidth - baseWidth; // Vertical size const baseHeight = titleBoxHeight + verticalNavigation + 2 * padding + 2 * margin; const maxGridPageHeight = maxDialogHeight - baseHeight; // Will be updated to the actual value later let itemPadding = 55; const minItemSize = 48 + itemPadding; let columns = opt.APP_GRID_FOLDER_COLUMNS; let rows = opt.APP_GRID_FOLDER_ROWS; const maxColumns = columns ? columns : 100; const maxRows = rows ? rows : 100; // Find best icon size let iconSize = opt.APP_GRID_FOLDER_ICON_SIZE < 0 ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT : opt.APP_GRID_FOLDER_ICON_SIZE; if (opt.APP_GRID_FOLDER_ICON_SIZE === -1) { let maxIconSize; if (columns) { const maxItemWidth = (maxGridPageWidth - (columns - 1) * opt.APP_GRID_FOLDER_SPACING) / columns; maxIconSize = maxItemWidth - itemPadding; } if (rows) { const maxItemHeight = (maxGridPageHeight - (rows - 1) * spacing) / rows; maxIconSize = Math.min(maxItemHeight - itemPadding, maxIconSize); } if (maxIconSize) { // We only need sizes from the default to the smallest let iconSizes = Object.values(IconSize).sort((a, b) => b - a); iconSizes = iconSizes.slice(iconSizes.indexOf(iconSize)); for (const size of iconSizes) { iconSize = size; if (iconSize <= maxIconSize) break; } } } if ((!columns && !rows) || opt.APP_GRID_FOLDER_ICON_SIZE !== -1) { columns = Math.ceil(Math.sqrt(nItems)); rows = columns; if (columns * (columns - 1) >= nItems) { rows = columns - 1; } else if ((columns + 1) * (columns - 1) >= nItems) { rows = columns - 1; columns += 1; } } else if (columns && !rows) { rows = Math.ceil(nItems / columns); } else if (rows && !columns) { columns = Math.ceil(nItems / rows); } columns = Math.clamp(columns, 1, maxColumns); columns = Math.min(nItems, columns); rows = Math.clamp(rows, 1, maxRows); let itemSize = iconSize + itemPadding; // First run sets the grid before we can read the real icon size // so we estimate the size from default properties // and correct it in the second run if (this.realized) { firstItem.icon.setIconSize(iconSize); // Item height is inconsistent because it depends on its label height const [, firstItemWidth] = firstItem.get_preferred_width(-1); const realSize = firstItemWidth / scaleFactor; itemSize = realSize; itemPadding = realSize - iconSize; } const gridWidth = columns * (itemSize + spacing); let width = gridWidth + baseWidth; const gridHeight = rows * (itemSize + spacing); let height = gridHeight + baseHeight; // Folder must fit the appDisplay area plus searchEntryBin if visible // reduce columns/rows if needed while (height > maxDialogHeight && rows > 1) { height -= itemSize + spacing; rows -= 1; } while (width > maxDialogWidth && columns > 1) { width -= itemSize + spacing; columns -= 1; } // Try to compensate for the previous reduction if there is a space while ((nItems > columns * rows) && ((width + (itemSize + spacing)) <= maxDialogWidth) && (columns < maxColumns)) { width += itemSize + spacing; columns += 1; } // remove columns that cannot be displayed if (((columns * minItemSize + (columns - 1) * spacing)) > maxDialogWidth) columns = Math.floor(maxDialogWidth / (minItemSize + spacing)); while ((nItems > columns * rows) && ((height + (itemSize + spacing)) <= maxDialogHeight) && (rows < maxRows)) { height += itemSize + spacing; rows += 1; } // remove rows that cannot be displayed if ((((rows * minItemSize + (rows - 1) * spacing))) > maxDialogHeight) rows = Math.floor(maxDialogWidth / (minItemSize + spacing)); // remove size for rows that are empty const rowsNeeded = Math.ceil(nItems / columns); if (rows > rowsNeeded) { height -= (rows - rowsNeeded) * (itemSize + spacing); rows -= rows - rowsNeeded; } // Remove space reserved for page controls and indicator if not used if (rows * columns >= nItems) { width -= horizontalNavigation; height -= verticalNavigation; } width = Math.clamp(width, minDialogWidth, maxDialogWidth); height = Math.min(height, maxDialogHeight); layoutManager.columns_per_page = columns; layoutManager.rows_per_page = rows; layoutManager.fixedIconSize = iconSize; // Store data for further use this._width = width * scaleFactor; this._height = height * scaleFactor; this._folderAreaBox = folderAreaBox; this._nItems = nItems; // Set fixed dialog size to prevent size instability this.child.set_size(this._width, this._height); this._viewBox.set_style(`width: ${this._width - 2 * margin}px; height: ${this._height - 2 * margin}px;`); this._viewBox.set_size(this._width - 2 * margin, this._height - 2 * margin); view._redisplay(); }, _zoomAndFadeIn() { let [sourceX, sourceY] = this._source.get_transformed_position(); let [dialogX, dialogY] = this.child.get_transformed_position(); const sourceCenterX = sourceX + this._source.width / 2; const sourceCenterY = sourceY + this._source.height / 2; // this. covers the whole screen let dialogTargetX = dialogX; let dialogTargetY = dialogY; const appDisplay = this._source._parentView; const folderAreaBox = this._getFolderAreaBox(); let folderAreaX = folderAreaBox.x1; let folderAreaY = folderAreaBox.y1; const folderAreaWidth = folderAreaBox.get_width(); const folderAreaHeight = folderAreaBox.get_height(); const folder = this.child; if (opt.APP_GRID_FOLDER_CENTER) { dialogTargetX = folderAreaX + folderAreaWidth / 2 - folder.width / 2; dialogTargetY = folderAreaY + (folderAreaHeight / 2 - folder.height / 2) / 2; } else { const { pagePadding } = appDisplay._grid.layoutManager; const hPadding = (pagePadding.left + pagePadding.right) / 2; const vPadding = (pagePadding.top + pagePadding.bottom) / 2; const minX = Math.min(folderAreaX + hPadding, folderAreaX + (folderAreaWidth - folder.width) / 2); const maxX = Math.max(folderAreaX + folderAreaWidth - hPadding - folder.width, folderAreaX + folderAreaWidth / 2 - folder.width / 2); const minY = Math.min(folderAreaY + vPadding, folderAreaY + (folderAreaHeight - folder.height) / 2); const maxY = Math.max(folderAreaY + folderAreaHeight - vPadding - folder.height, folderAreaY + folderAreaHeight / 2 - folder.height / 2); dialogTargetX = sourceCenterX - folder.width / 2; dialogTargetX = Math.clamp(dialogTargetX, minX, maxX); dialogTargetY = sourceCenterY - folder.height / 2; dialogTargetY = Math.clamp(dialogTargetY, minY, maxY); // keep the dialog in the appDisplay area dialogTargetX = Math.clamp( dialogTargetX, folderAreaX, folderAreaX + folderAreaWidth - folder.width ); dialogTargetY = Math.clamp( dialogTargetY, folderAreaY, folderAreaY + folderAreaHeight - folder.height ); } const dialogOffsetX = Math.round(dialogTargetX - dialogX); const dialogOffsetY = Math.round(dialogTargetY - dialogY); this.child.set({ translation_x: sourceX - dialogX, translation_y: sourceY - dialogY, scale_x: this._source.width / this.child.width, scale_y: this._source.height / this.child.height, opacity: 0, }); this.child.ease({ translation_x: dialogOffsetX, translation_y: dialogOffsetY, scale_x: 1, scale_y: 1, opacity: 255, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); appDisplay.ease({ opacity: 0, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); if (opt.SHOW_SEARCH_ENTRY) { Main.overview.searchEntry.ease({ opacity: 0, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } this._needsZoomAndFade = false; if (this._sourceMappedId === 0) { this._sourceMappedId = this._source.connect( 'notify::mapped', this._zoomAndFadeOut.bind(this)); } }, _zoomAndFadeOut() { if (!this._isOpen) return; if (!this._source.mapped) { this.hide(); return; } // if the dialog was shown silently, skip animation if (this.scale_y < 1) { this._needsZoomAndFade = false; this.hide(); this._popdownCallbacks.forEach(func => func()); this._popdownCallbacks = []; return; } let [sourceX, sourceY] = this._source.get_transformed_position(); let [dialogX, dialogY] = this.child.get_transformed_position(); this.child.ease({ translation_x: sourceX - dialogX + this.child.translation_x, translation_y: sourceY - dialogY + this.child.translation_y, scale_x: this._source.width / this.child.width, scale_y: this._source.height / this.child.height, opacity: 0, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_IN_QUAD, onComplete: () => { this.child.set({ translation_x: 0, translation_y: 0, scale_x: 1, scale_y: 1, opacity: 255, }); this.hide(); this._popdownCallbacks.forEach(func => func()); this._popdownCallbacks = []; }, }); const appDisplay = this._source._parentView; appDisplay.ease({ opacity: 255, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_IN_QUAD, }); if (opt.SHOW_SEARCH_ENTRY) { Main.overview.searchEntry.ease({ opacity: 255, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_IN_QUAD, }); } this._needsZoomAndFade = false; }, _setLighterBackground(lighter) { let opacity = 255; if (this._isOpen) opacity = lighter ? 20 : 0; _appDisplay.ease({ opacity, duration: FOLDER_DIALOG_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); }, vfunc_key_press_event(event) { if (global.focus_manager.navigate_from_event(event)) return Clutter.EVENT_STOP; return Clutter.EVENT_PROPAGATE; }, _showFolderLabel() { if (this._editButton.checked) this._editButton.checked = false; this._maybeUpdateFolderName(); this._switchActor(this._entry, this._folderNameLabel); // This line has been added in 47 to fix focus after editing the folder name this.navigate_focus(this, St.DirectionType.TAB_FORWARD, false); }, }; const AppIcon = { after__init() { // update the app label behavior this._updateMultiline(); }, // avoid accepting by placeholder when dragging active preview // and also by icon if usage sorting is used _canAccept(source) { if (source._sourceItem) source = source._sourceItem; // Folders in folder are not supported if (!(_getViewFromIcon(this) instanceof AppDisplay.AppDisplay) || !this.opacity) return false; const view = /* AppDisplay.*/_getViewFromIcon(source); return source !== this && (source instanceof this.constructor) && // Include drops from folders // (view instanceof AppDisplay.AppDisplay && (view && !opt.APP_GRID_USAGE); }, }; const AppViewItemCommon = { _updateMultiline() { const { label } = this.icon; if (label) label.opacity = 255; if (!this._expandTitleOnHover || !this.icon.label) return; const { clutterText } = label; const isHighlighted = this.has_key_focus() || this.hover || this._forcedHighlight; if (opt.APP_GRID_NAMES_MODE === 2 && this._expandTitleOnHover) { // !_expandTitleOnHover indicates search result icon label.opacity = isHighlighted || !this.app ? 255 : 0; } if (isHighlighted) this.get_parent()?.set_child_above_sibling(this, null); if (!opt.APP_GRID_NAMES_MODE) { const layout = clutterText.get_layout(); if (!layout.is_wrapped() && !layout.is_ellipsized()) return; } label.remove_transition('allocation'); const id = label.connect('notify::allocation', () => { label.restore_easing_state(); label.disconnect(id); }); const expand = opt.APP_GRID_NAMES_MODE === 1 || this._forcedHighlight || this.hover || this.has_key_focus(); label.save_easing_state(); label.set_easing_duration(expand ? APP_ICON_TITLE_EXPAND_TIME : APP_ICON_TITLE_COLLAPSE_TIME); clutterText.set({ line_wrap: expand, line_wrap_mode: expand ? Pango.WrapMode.WORD_CHAR : Pango.WrapMode.NONE, ellipsize: expand ? Pango.EllipsizeMode.NONE : Pango.EllipsizeMode.END, }); }, // support active preview icons acceptDrop(source, _actor, x) { if (opt.APP_GRID_USAGE) return DND.DragMotionResult.NO_DROP; this._setHoveringByDnd(false); if (!this._canAccept(source)) return false; if (this._withinLeeways(x)) return false; // added - remove app from the source folder after dnd to other folder let view = /* AppDisplay.*/_getViewFromIcon(source); if (view instanceof AppDisplay.FolderView) view.removeApp(source.app); return true; }, }; const PageIndicatorsCommon = { after_setNPages() { this.visible = true; this.opacity = this._nPages > 1 ? 255 : 0; }, };