summaryrefslogtreecommitdiffstats
path: root/extensions/47/vertical-workspaces/lib/appDisplay.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/47/vertical-workspaces/lib/appDisplay.js')
-rw-r--r--extensions/47/vertical-workspaces/lib/appDisplay.js2014
1 files changed, 2014 insertions, 0 deletions
diff --git a/extensions/47/vertical-workspaces/lib/appDisplay.js b/extensions/47/vertical-workspaces/lib/appDisplay.js
new file mode 100644
index 0000000..d94f7df
--- /dev/null
+++ b/extensions/47/vertical-workspaces/lib/appDisplay.js
@@ -0,0 +1,2014 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * appDisplay.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @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;
+ },
+};