summaryrefslogtreecommitdiffstats
path: root/extensions/vertical-workspaces/lib/appDisplay.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/vertical-workspaces/lib/appDisplay.js')
-rw-r--r--extensions/vertical-workspaces/lib/appDisplay.js1474
1 files changed, 1474 insertions, 0 deletions
diff --git a/extensions/vertical-workspaces/lib/appDisplay.js b/extensions/vertical-workspaces/lib/appDisplay.js
new file mode 100644
index 0000000..2ac70b1
--- /dev/null
+++ b/extensions/vertical-workspaces/lib/appDisplay.js
@@ -0,0 +1,1474 @@
+/**
+ * V-Shell (Vertical Workspaces)
+ * appDisplay.js
+ *
+ * @author GdH <G-dH@github.com>
+ * @copyright 2022 - 2023
+ * @license GPL-3.0
+ *
+ */
+
+'use strict';
+
+const { Clutter, GLib, GObject, Meta, Shell, St, Graphene, Pango } = imports.gi;
+
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const AppDisplay = imports.ui.appDisplay;
+const IconGrid = imports.ui.iconGrid;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+const Me = ExtensionUtils.getCurrentExtension();
+const IconGridOverride = Me.imports.lib.iconGrid;
+const _Util = Me.imports.lib.util;
+
+const DIALOG_SHADE_NORMAL = Clutter.Color.from_pixel(0x00000022);
+const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000000);
+
+// gettext
+const _ = Me.imports.lib.settings._;
+
+let _overrides;
+
+let _appGridLayoutSettings;
+let _appDisplayScrollConId;
+let _appSystemStateConId;
+let _appGridLayoutConId;
+let _origAppViewItemAcceptDrop;
+let _updateFolderIcons;
+
+let opt;
+let shellVersion = _Util.shellVersion;
+let _firstRun = true;
+
+function update(reset = false) {
+ opt = Me.imports.lib.settings.opt;
+ const moduleEnabled = opt.get('appDisplayModule', true);
+ reset = reset || !moduleEnabled;
+
+ // don't even touch this module if disabled
+ if (_firstRun && reset)
+ return;
+
+ _firstRun = false;
+
+ if (_overrides)
+ _overrides.removeAll();
+
+ if (reset) {
+ _setAppDisplayOrientation(false);
+ _updateAppGridProperties(reset);
+ _updateAppGridDND(reset);
+ _restoreOverviewGroup();
+ _overrides = null;
+ opt = null;
+ return;
+ }
+
+ _overrides = new _Util.Overrides();
+
+ if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) {
+ _overrides.addOverride('AppDisplayVertical', AppDisplay.AppDisplay.prototype, AppDisplayVertical);
+ _overrides.addOverride('BaseAppViewVertical', AppDisplay.BaseAppView.prototype, BaseAppViewVertical);
+ }
+
+ // Custom App Grid
+ _overrides.addOverride('AppFolderDialog', AppDisplay.AppFolderDialog.prototype, AppFolderDialog);
+ if (shellVersion >= 43) {
+ // const defined class needs to be touched before real access
+ AppDisplay.BaseAppViewGridLayout;
+ _overrides.addOverride('BaseAppViewGridLayout', AppDisplay.BaseAppViewGridLayout.prototype, BaseAppViewGridLayout);
+ }
+ _overrides.addOverride('FolderView', AppDisplay.FolderView.prototype, FolderView);
+ _overrides.addOverride('FolderIcon', AppDisplay.FolderIcon.prototype, FolderIcon);
+ _overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIcon);
+ _overrides.addOverride('AppDisplay', AppDisplay.AppDisplay.prototype, AppDisplayCommon);
+ _overrides.addOverride('AppViewItem', AppDisplay.AppViewItem.prototype, AppViewItemCommon);
+ _overrides.addOverride('BaseAppViewCommon', AppDisplay.BaseAppView.prototype, BaseAppViewCommon);
+
+ _setAppDisplayOrientation(opt.ORIENTATION === Clutter.Orientation.VERTICAL);
+ _updateAppGridProperties();
+ _updateAppGridDND();
+ opt._appGridNeedsRedisplay = true;
+}
+
+function _setAppDisplayOrientation(vertical = false) {
+ const CLUTTER_ORIENTATION = vertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL;
+ const scroll = vertical ? 'vscroll' : 'hscroll';
+ // app display to vertical has issues - page indicator not working
+ // global appDisplay orientation switch is not built-in
+ let appDisplay = Main.overview._overview._controls._appDisplay;
+ // following line itself only changes in which axis will operate overshoot detection which switches appDisplay pages while dragging app icon to vertical
+ appDisplay._orientation = CLUTTER_ORIENTATION;
+ appDisplay._grid.layoutManager._orientation = CLUTTER_ORIENTATION;
+ appDisplay._swipeTracker.orientation = CLUTTER_ORIENTATION;
+ appDisplay._swipeTracker._reset();
+ if (vertical) {
+ appDisplay._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL);
+
+ // move and change orientation of page indicators
+ // needs corrections in appgrid page calculations, e.g. appDisplay.adaptToSize() fnc,
+ // which complicates use of super call inside the function
+ const pageIndicators = appDisplay._pageIndicators;
+ pageIndicators.vertical = true;
+ appDisplay._box.vertical = false;
+ pageIndicators.x_expand = false;
+ pageIndicators.y_align = Clutter.ActorAlign.CENTER;
+ pageIndicators.x_align = Clutter.ActorAlign.START;
+
+ const scrollContainer = appDisplay._scrollView.get_parent();
+ if (shellVersion < 43) {
+ // remove touch friendly side navigation bars / arrows
+ if (appDisplay._hintContainer && appDisplay._hintContainer.get_parent())
+ scrollContainer.remove_child(appDisplay._hintContainer);
+ } else {
+ // moving these bars needs more patching of the appDisplay's code
+ // for now we just change bars style to be more like vertically oriented arrows indicating direction to prev/next page
+ appDisplay._nextPageIndicator.add_style_class_name('nextPageIndicator');
+ appDisplay._prevPageIndicator.add_style_class_name('prevPageIndicator');
+ }
+
+ // setting their x_scale to 0 removes the arrows and avoid allocation issues compared to .hide() them
+ appDisplay._nextPageArrow.scale_x = 0;
+ appDisplay._prevPageArrow.scale_x = 0;
+ } else {
+ appDisplay._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER);
+ if (_appDisplayScrollConId) {
+ appDisplay._adjustment.disconnect(_appDisplayScrollConId);
+ _appDisplayScrollConId = 0;
+ }
+
+ // restore original page indicators
+ const pageIndicators = appDisplay._pageIndicators;
+ pageIndicators.vertical = false;
+ appDisplay._box.vertical = true;
+ pageIndicators.x_expand = true;
+ pageIndicators.y_align = Clutter.ActorAlign.END;
+ pageIndicators.x_align = Clutter.ActorAlign.CENTER;
+
+ // put back touch friendly navigation bars/buttons
+ const scrollContainer = appDisplay._scrollView.get_parent();
+ if (appDisplay._hintContainer && !appDisplay._hintContainer.get_parent()) {
+ scrollContainer.add_child(appDisplay._hintContainer);
+ // the hit container covers the entire app grid and added at the top of the stack blocks DND drops
+ // so it needs to be pushed below
+ scrollContainer.set_child_below_sibling(appDisplay._hintContainer, null);
+ }
+
+ appDisplay._nextPageArrow.scale_x = 1;
+ appDisplay._prevPageArrow.scale_x = 1;
+
+ appDisplay._nextPageIndicator.remove_style_class_name('nextPageIndicator');
+ appDisplay._prevPageIndicator.remove_style_class_name('prevPageIndicator');
+ }
+
+ // value for page indicator is calculated from scroll adjustment, horizontal needs to be replaced by vertical
+ appDisplay._adjustment = appDisplay._scrollView[scroll].adjustment;
+
+ // no need to connect already connected signal (wasn't removed the original one before)
+ if (!vertical) {
+ // reset used appDisplay properties
+ Main.overview._overview._controls._appDisplay.scale_y = 1;
+ Main.overview._overview._controls._appDisplay.scale_x = 1;
+ Main.overview._overview._controls._appDisplay.opacity = 255;
+ return;
+ }
+
+ // update appGrid dot pages indicators
+ _appDisplayScrollConId = appDisplay._adjustment.connect('notify::value', adj => {
+ const value = adj.value / adj.page_size;
+ appDisplay._pageIndicators.setCurrentPosition(value);
+ });
+}
+
+// Set App Grid columns, rows, icon size, incomplete pages
+function _updateAppGridProperties(reset = false) {
+ opt._appGridNeedsRedisplay = false;
+ // columns, rows, icon size
+ const appDisplay = Main.overview._overview._controls._appDisplay;
+ appDisplay.visible = true;
+
+ if (reset) {
+ appDisplay._grid.layoutManager.fixedIconSize = -1;
+ appDisplay._grid.layoutManager.allow_incomplete_pages = true;
+ appDisplay._grid.setGridModes();
+ if (_appGridLayoutSettings) {
+ _appGridLayoutSettings.disconnect(_appGridLayoutConId);
+ _appGridLayoutConId = 0;
+ _appGridLayoutSettings = null;
+ }
+ appDisplay._redisplay();
+
+ appDisplay._grid.set_style('');
+ _resetAppGrid();
+ } else {
+ // update grid on layout reset
+ if (!_appGridLayoutSettings) {
+ _appGridLayoutSettings = ExtensionUtils.getSettings('org.gnome.shell');
+ _appGridLayoutConId = _appGridLayoutSettings.connect('changed::app-picker-layout', _resetAppGrid);
+ }
+
+ appDisplay._grid.layoutManager.allow_incomplete_pages = opt.APP_GRID_ALLOW_INCOMPLETE_PAGES;
+ appDisplay._grid.set_style(`column-spacing: ${opt.APP_GRID_SPACING}px; row-spacing: ${opt.APP_GRID_SPACING}px;`);
+
+ // force redisplay
+ appDisplay._grid._currentMode = -1;
+ appDisplay._grid.setGridModes();
+ appDisplay._grid.layoutManager.fixedIconSize = opt.APP_GRID_ICON_SIZE;
+ // appDisplay._folderIcons.forEach(folder => folder._dialog?._updateFolderSize());
+ _resetAppGrid();
+ }
+}
+
+function _updateAppGridDND(reset) {
+ if (!reset) {
+ if (!_appSystemStateConId && opt.APP_GRID_INCLUDE_DASH >= 3) {
+ _appSystemStateConId = Shell.AppSystem.get_default().connect(
+ 'app-state-changed',
+ () => {
+ _updateFolderIcons = true;
+ Main.overview._overview._controls._appDisplay._redisplay();
+ }
+ );
+ }
+ } else if (_appSystemStateConId) {
+ Shell.AppSystem.get_default().disconnect(_appSystemStateConId);
+ _appSystemStateConId = 0;
+ }
+ if (opt.APP_GRID_ORDER && !reset) {
+ if (!_origAppViewItemAcceptDrop)
+ _origAppViewItemAcceptDrop = AppDisplay.AppViewItem.prototype.acceptDrop;
+ AppDisplay.AppViewItem.prototype.acceptDrop = () => false;
+ } else if (_origAppViewItemAcceptDrop) {
+ AppDisplay.AppViewItem.prototype.acceptDrop = _origAppViewItemAcceptDrop;
+ }
+}
+
+function _restoreOverviewGroup() {
+ Main.overview.dash.showAppsButton.checked = false;
+ Main.layoutManager.overviewGroup.opacity = 255;
+ Main.layoutManager.overviewGroup.scale_x = 1;
+ Main.layoutManager.overviewGroup.hide();
+}
+
+const AppDisplayVertical = {
+ // correction of the appGrid size when page indicators were moved from the bottom to the right
+ adaptToSize(width, height) {
+ const [, indicatorWidth] = this._pageIndicators.get_preferred_width(-1);
+ width -= indicatorWidth;
+
+ this._grid.findBestModeForSize(width, height);
+
+ const adaptToSize = AppDisplay.BaseAppView.prototype.adaptToSize.bind(this);
+ adaptToSize(width, height);
+ },
+};
+
+const AppDisplayCommon = {
+ _ensureDefaultFolders() {
+ // disable creation of default folders if user deleted them
+ },
+
+ _redisplay() {
+ this._folderIcons.forEach(icon => {
+ icon.view._redisplay();
+ });
+
+ BaseAppViewCommon._redisplay.bind(this)();
+ },
+
+ // 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_ORDER) {
+ 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 (_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
+ _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_ORDER && 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;
+ },
+
+ // support active preview icons
+ _onDragBegin(overview, source) {
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this),
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ if (shellVersion < 43)
+ this._slideSidePages(AppDisplay.SidePages.PREVIOUS | AppDisplay.SidePages.NEXT | AppDisplay.SidePages.DND);
+ else
+ 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 preview
+ acceptDrop(source) {
+ if (opt.APP_GRID_ORDER)
+ return false;
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+ let dropTarget = null;
+ if (shellVersion >= 43) {
+ dropTarget = this._dropTarget;
+ delete this._dropTarget;
+ }
+
+ if (!this._canAccept(source))
+ return false;
+
+ if ((shellVersion < 43 && this._dropPage) ||
+ (shellVersion >= 43 && (dropTarget === this._prevPageIndicator ||
+ dropTarget === this._nextPageIndicator))) {
+ let increment;
+
+ if (shellVersion < 43)
+ increment = this._dropPage === AppDisplay.SidePages.NEXT ? 1 : -1;
+ else
+ 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) {
+ log(`Warning:${e}`);
+ }
+ this._removeDelayedMove();
+ }
+
+ this._savePages();
+
+ let 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;
+ },
+};
+
+const BaseAppViewVertical = {
+ after__init() {
+ this._grid.layoutManager._orientation = Clutter.Orientation.VERTICAL;
+ this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL);
+ this._orientation = Clutter.Orientation.VERTICAL;
+ this._swipeTracker.orientation = Clutter.Orientation.VERTICAL;
+ this._swipeTracker._reset();
+ this._pageIndicators.vertical = true;
+ this._box.vertical = false;
+ this._pageIndicators.x_expand = false;
+ this._pageIndicators.y_align = Clutter.ActorAlign.CENTER;
+ this._pageIndicators.x_align = Clutter.ActorAlign.START;
+ this._pageIndicators.set_style('margin-right: 10px;');
+ const scrollContainer = this._scrollView.get_parent();
+ if (shellVersion < 43) {
+ // remove touch friendly side navigation bars / arrows
+ if (this._hintContainer && this._hintContainer.get_parent())
+ scrollContainer.remove_child(this._hintContainer);
+ } else {
+ // moving these bars needs more patching of the this's code
+ // for now we just change bars style to be more like vertically oriented arrows indicating direction to prev/next page
+ this._nextPageIndicator.add_style_class_name('nextPageIndicator');
+ this._prevPageIndicator.add_style_class_name('prevPageIndicator');
+ }
+
+ // setting their x_scale to 0 removes the arrows and avoid allocation issues compared to .hide() them
+ this._nextPageArrow.scale_x = 0;
+ this._prevPageArrow.scale_x = 0;
+
+ this._adjustment = this._scrollView.vscroll.adjustment;
+
+ this._adjustment.connect('notify::value', adj => {
+ const value = adj.value / adj.page_size;
+ this._pageIndicators.setCurrentPosition(value);
+ });
+ },
+ // <= 42 only, this fixes dnd from appDisplay to the workspace thumbnail on the left if appDisplay is on page 1 because of appgrid left overshoot
+ _pageForCoords() {
+ return AppDisplay.SidePages.NONE;
+ },
+};
+
+const BaseAppViewCommon = {
+ _sortOrderedItemsAlphabetically(icons = null) {
+ if (!icons)
+ icons = this._orderedItems;
+ icons.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ },
+
+ _setLinearPositions(icons) {
+ const { itemsPerPage } = this._grid;
+ icons.forEach((icon, i) => {
+ const page = Math.floor(i / itemsPerPage);
+ const position = i % itemsPerPage;
+ try {
+ this._moveItem(icon, page, position);
+ } catch (e) {
+ log(`Warning:${e}`);
+ }
+ });
+ },
+
+ // adds sorting options and option to add favorites and running apps
+ _redisplay() {
+ let oldApps = this._orderedItems.slice();
+ let oldAppIds = oldApps.map(icon => icon.id);
+
+ let newApps = this._loadApps().sort(this._compareItems.bind(this));
+ let newAppIds = newApps.map(icon => icon.id);
+
+ let addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id));
+ let removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id));
+
+ // Remove old app icons
+ removedApps.forEach(icon => {
+ this._removeItem(icon);
+ icon.destroy();
+ });
+
+ // Add new app icons, or move existing ones
+ newApps.forEach(icon => {
+ const [page, position] = this._getItemPosition(icon);
+ if (addedApps.includes(icon)) {
+ this._addItem(icon, page, position);
+ } else if (page !== -1 && position !== -1) {
+ this._moveItem(icon, page, position);
+ } else {
+ // App is part of a folder
+ }
+ });
+
+ // sort all alphabetically
+ if (opt.APP_GRID_ORDER > 0) {
+ // const { itemsPerPage } = this._grid;
+ let appIcons = this._orderedItems;
+ this._sortOrderedItemsAlphabetically(appIcons);
+ // appIcons.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ // then sort used apps by usage
+ if (opt.APP_GRID_ORDER === 2)
+ appIcons.sort((a, b) => Shell.AppUsage.get_default().compare(a.app.id, b.app.id));
+
+ // sort favorites first
+ if (opt.APP_GRID_DASH_FIRST) {
+ const fav = Object.keys(this._appFavorites._favorites);
+ appIcons.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_DASH_FIRST)
+ appIcons.sort((a, b) => a.app.get_state() !== Shell.AppState.RUNNING && b.app.get_state() === Shell.AppState.RUNNING);
+
+ this._setLinearPositions(appIcons);
+
+ this._orderedItems = appIcons;
+ }
+
+ this.emit('view-loaded');
+ if (!opt.APP_GRID_ALLOW_INCOMPLETE_PAGES) {
+ for (let i = 0; i < this._grid.nPages; i++)
+ this._grid.layoutManager._fillItemVacancies(i);
+ }
+ },
+
+ _canAccept(source) {
+ return opt.APP_GRID_ORDER ? false : source instanceof AppDisplay.AppViewItem;
+ },
+
+ // support active preview icons
+ acceptDrop(source) {
+ if (!this._canAccept(source))
+ return false;
+
+ if (source._sourceItem)
+ source = source._sourceItem;
+
+
+ if (this._dropPage) {
+ const increment = this._dropPage === AppDisplay.SidePages.NEXT ? 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;
+
+ this._moveItem(source, page, position);
+ 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 (shellVersion < 43) {
+ this._dropPage = this._pageForCoords(dragEvent.x, dragEvent.y);
+ if (this._dropPage &&
+ this._dropPage === AppDisplay.SidePages.PREVIOUS &&
+ this._grid.currentPage === 0) {
+ delete this._dropPage;
+ return DND.DragMotionResult.NO_DROP;
+ }
+ }
+
+ if (appIcon instanceof AppDisplay.AppViewItem) {
+ if (shellVersion < 44) {
+ // Handle the drag overshoot. When dragging to above the
+ // icon grid, move to the page above; when dragging below,
+ // move to the page below.
+ this._handleDragOvershoot(dragEvent);
+ } else 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();
+ }
+ }
+
+ this._maybeMoveItem(dragEvent);
+
+ return DND.DragMotionResult.CONTINUE;
+ },
+
+ // adjustable page width for GS <= 42
+ adaptToSize(width, height, isFolder = false) {
+ let box = new Clutter.ActorBox({
+ x2: width,
+ y2: height,
+ });
+ box = this.get_theme_node().get_content_box(box);
+ box = this._scrollView.get_theme_node().get_content_box(box);
+ box = this._grid.get_theme_node().get_content_box(box);
+
+ const availWidth = box.get_width();
+ const availHeight = box.get_height();
+
+ let pageWidth, pageHeight;
+
+ pageHeight = availHeight;
+ pageWidth = Math.ceil(availWidth * (isFolder ? 1 : opt.APP_GRID_PAGE_WIDTH_SCALE));
+ // subtract space for navigation arrows in horizontal mode
+ pageWidth -= opt.ORIENTATION ? 0 : 128;
+
+ this._grid.layout_manager.pagePadding.left =
+ Math.floor(availWidth * 0.02);
+ this._grid.layout_manager.pagePadding.right =
+ Math.ceil(availWidth * 0.02);
+
+ this._grid.adaptToSize(pageWidth, pageHeight);
+
+ const leftPadding = Math.floor(
+ (availWidth - this._grid.layout_manager.pageWidth) / 2);
+ const rightPadding = Math.ceil(
+ (availWidth - this._grid.layout_manager.pageWidth) / 2);
+ const topPadding = Math.floor(
+ (availHeight - this._grid.layout_manager.pageHeight) / 2);
+ const bottomPadding = Math.ceil(
+ (availHeight - this._grid.layout_manager.pageHeight) / 2);
+
+ this._scrollView.content_padding = new Clutter.Margin({
+ left: leftPadding,
+ right: rightPadding,
+ top: topPadding,
+ bottom: bottomPadding,
+ });
+
+ this._availWidth = availWidth;
+ this._availHeight = availHeight;
+
+ this._pageIndicatorOffset = leftPadding;
+ this._pageArrowOffset = Math.max(
+ leftPadding - 80, 0); // 80 is AppDisplay.PAGE_PREVIEW_MAX_ARROW_OFFSET
+ },
+};
+
+const BaseAppViewGridLayout = {
+ _getIndicatorsWidth(box) {
+ const [width, height] = box.get_size();
+ const arrows = [
+ this._nextPageArrow,
+ this._previousPageArrow,
+ ];
+
+ const minArrowsWidth = arrows.reduce(
+ (previousWidth, accessory) => {
+ const [min] = accessory.get_preferred_width(height);
+ return Math.max(previousWidth, min);
+ }, 0);
+
+ const idealIndicatorWidth = (width * 0.1/* PAGE_PREVIEW_RATIO*/) / 2;
+
+ return Math.max(idealIndicatorWidth, minArrowsWidth);
+ },
+};
+
+const FolderIcon = {
+ after__init() {
+ /* // If folder preview icons are clickable,
+ // disable opening the folder with primary mouse button and enable the secondary one
+ const buttonMask = opt.APP_GRID_ACTIVE_PREVIEW
+ ? St.ButtonMask.TWO | St.ButtonMask.THREE
+ : St.ButtonMask.ONE | St.ButtonMask.TWO;
+ this.button_mask = buttonMask;*/
+ this.button_mask = St.ButtonMask.ONE | St.ButtonMask.TWO;
+
+ // build the folders now to avoid node errors when dragging active folder preview icons
+ if (this.visible && opt.APP_GRID_ACTIVE_PREVIEW)
+ this._ensureFolderDialog();
+ },
+
+ open() {
+ this._ensureFolderDialog();
+ if (this._dialog._designCapacity !== this.view._orderedItems.length)
+ this._dialog._updateFolderSize();
+
+ this.view._scrollView.vscroll.adjustment.value = 0;
+ this._dialog.popup();
+ },
+};
+
+const FolderView = {
+ _createGrid() {
+ let grid;
+ if (shellVersion < 43)
+ grid = new FolderGrid();
+ else
+ grid = new FolderGrid43();
+
+ // IconGrid algorithm for adaptive icon size
+ // counts with different default(max) size for folders
+ grid.layoutManager._isFolder = true;
+ 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 ActiveFolderIcon(app);
+ child._sourceItem = this._orderedItems[i];
+ child._sourceFolder = this;
+ child.icon.style_class = '';
+ child.icon.set_style('margin: 0; padding: 0;');
+ child.icon.setIconSize(subSize);
+
+ bin.child = child;
+
+ bin.connect('enter-event', () => {
+ bin.ease({
+ duration: 100,
+ scale_x: 1.14,
+ scale_y: 1.14,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+ bin.connect('leave-event', () => {
+ bin.ease({
+ duration: 100,
+ scale_x: 1,
+ scale_y: 1,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+ }
+ }
+
+ layout.attach(bin, rtl ? (i + 1) % gridSize : i % gridSize, Math.floor(i / gridSize), 1, 1);
+ }
+
+ // if folder content changed, update folder size
+ if (this._dialog && this._dialog._designCapacity !== this._orderedItems.length)
+ this._dialog._updateFolderSize();
+
+ return icon;
+ },
+
+ // this just overrides _redisplay() for GS < 44
+ _redisplay() {
+ // super._redisplay(); - super doesn't work in my overrides
+ AppDisplay.BaseAppView.prototype._redisplay.bind(this)();
+ },
+
+ _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 (!AppDisplay._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);
+ });
+ this._appIds = this._apps.map(app => app.get_id());
+ return items;
+ },
+
+ // 42 only - don't apply appGrid scale on folders
+ adaptToSize(width, height) {
+ if (!opt.ORIENTATION) {
+ const [, indicatorHeight] = this._pageIndicators.get_preferred_height(-1);
+ height -= indicatorHeight;
+ }
+ BaseAppViewCommon.adaptToSize.bind(this)(width, height, true);
+ },
+};
+
+// folder columns and rows
+const FolderGrid = GObject.registerClass(
+class FolderGrid extends IconGrid.IconGrid {
+ _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
+ columns_per_page: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 20,
+ rows_per_page: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 20,
+ page_halign: Clutter.ActorAlign.CENTER,
+ page_valign: Clutter.ActorAlign.CENTER,
+ });
+
+ // if (!opt.APP_GRID_FOLDER_DEFAULT)
+ const spacing = opt.APP_GRID_SPACING;
+ this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
+ this.layout_manager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE;
+ }
+
+ adaptToSize(width, height) {
+ this.layout_manager.adaptToSize(width, height);
+ }
+});
+
+
+let FolderGrid43;
+// first reference to constant defined using const in other module returns undefined, the AppGrid const will remain empty and unused
+const AppGrid = AppDisplay.AppGrid;
+if (AppDisplay.AppGrid) {
+ FolderGrid43 = GObject.registerClass(
+ class FolderGrid43 extends AppDisplay.AppGrid {
+ _init() {
+ super._init({
+ allow_incomplete_pages: false,
+ columns_per_page: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 20,
+ rows_per_page: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 20,
+ page_halign: Clutter.ActorAlign.CENTER,
+ page_valign: Clutter.ActorAlign.CENTER,
+ });
+
+ const spacing = opt.APP_GRID_SPACING;
+ this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
+ this.layout_manager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE;
+
+ this.setGridModes([
+ {
+ columns: opt.APP_GRID_FOLDER_COLUMNS ? opt.APP_GRID_FOLDER_COLUMNS : 3,
+ rows: opt.APP_GRID_FOLDER_ROWS ? opt.APP_GRID_FOLDER_ROWS : 3,
+ },
+ ]);
+ }
+
+ adaptToSize(width, height) {
+ this.layout_manager.adaptToSize(width, height);
+ }
+ });
+}
+
+const FOLDER_DIALOG_ANIMATION_TIME = 200; // AppDisplay.FOLDER_DIALOG_ANIMATION_TIME
+const AppFolderDialog = {
+ // injection to _init()
+ after__init() {
+ // 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);
+ },
+
+ popup() {
+ if (this._isOpen)
+ return;
+
+ /* if (!this._correctSize) {
+ // update folder with the precise app item size when the dialog is realized
+ GLib.idle_add(0, () => this._updateFolderSize(true));
+ this._correctSize = true;
+ }*/
+
+ this._isOpen = this._grabHelper.grab({
+ actor: this,
+ onUngrab: () => this.popdown(),
+ });
+
+ if (!this._isOpen)
+ return;
+
+ this.get_parent().set_child_above_sibling(this, null);
+
+ this._needsZoomAndFade = true;
+ this.show();
+
+ this.emit('open-state-changed', true);
+ },
+
+ _updateFolderSize() {
+ // adapt folder size according to the settings and number of icons
+ const view = this._view;
+ view._grid.layoutManager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE;
+ view._grid.set_style(`column-spacing: ${opt.APP_GRID_SPACING}px; row-spacing: ${opt.APP_GRID_SPACING}px;`);
+
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const dialogMargin = 30;
+ const nItems = view._orderedItems.length;
+ let columns = opt.APP_GRID_FOLDER_COLUMNS;
+ let rows = opt.APP_GRID_FOLDER_ROWS;
+ let spacing = opt.APP_GRID_SPACING;
+ const monitor = global.display.get_monitor_geometry(global.display.get_primary_monitor());
+
+ if (!columns && !rows) {
+ 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) {
+ columns = Math.ceil(nItems / rows);
+ } else if (columns && !rows) {
+ rows = Math.ceil(nItems / columns);
+ }
+
+ const iconSize = opt.APP_GRID_FOLDER_ICON_SIZE < 0 ? opt.APP_GRID_FOLDER_ICON_SIZE_DEFAULT : opt.APP_GRID_FOLDER_ICON_SIZE;
+ let itemSize = iconSize + 53; // icon padding
+ // 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._notFirstRun) {
+ const [firstItem] = view._grid.layoutManager._container;
+ firstItem.icon.setIconSize(iconSize);
+ const [firstItemWidth] = firstItem.get_preferred_size();
+ const realSize = firstItemWidth / scaleFactor;
+ if (realSize > iconSize)
+ itemSize = realSize;
+ } else {
+ this._needsUpdateSize = true;
+ this._notFirstRun = true;
+ }
+
+
+ let width = columns * (itemSize + spacing) + /* padding for nav arrows*/64;
+ width = Math.round(width + (opt.ORIENTATION || !opt.APP_GRID_FOLDER_COLUMNS ? 100 : 160/* space for navigation arrows*/));
+ let height = rows * (itemSize + spacing) + /* header*/75 + /* padding*/100;
+
+ // folder must fit the primary monitor
+ // reduce columns/rows if needed and count with the scaled values
+ while (width * scaleFactor > monitor.width - 2 * dialogMargin) {
+ width -= itemSize + spacing;
+ columns -= 1;
+ }
+ while (height * scaleFactor > monitor.height - 2 * dialogMargin) {
+ height -= itemSize + spacing;
+ rows -= 1;
+ }
+ width = Math.max(540, width);
+
+ const layoutManager = view._grid.layoutManager;
+ layoutManager.rows_per_page = rows;
+ layoutManager.columns_per_page = columns;
+
+ // this line is required by GS 43
+ view._grid.setGridModes([{ columns, rows }]);
+
+ this.child.set_style(`
+ width: ${width}px;
+ height: ${height}px;
+ padding: 30px;
+ `);
+
+ view._redisplay();
+
+ // store original item count
+ this._designCapacity = nItems;
+ },
+
+ _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;
+ if (!opt.APP_GRID_FOLDER_CENTER) {
+ const appDisplay = this._source._parentView;
+
+ dialogTargetX = Math.round(sourceCenterX - this.child.width / 2);
+ dialogTargetY = Math.round(sourceCenterY - this.child.height / 2);
+
+ // keep the dialog in appDisplay area if possible
+ dialogTargetX = Math.clamp(
+ dialogTargetX,
+ this.x + appDisplay.x,
+ this.x + appDisplay.x + appDisplay.width - this.child.width
+ );
+
+ dialogTargetY = Math.clamp(
+ dialogTargetY,
+ this.y + appDisplay.y,
+ this.y + appDisplay.y + appDisplay.height - this.child.height
+ );
+ // or at least in the monitor area
+ const monitor = global.display.get_monitor_geometry(global.display.get_primary_monitor());
+ dialogTargetX = Math.clamp(
+ dialogTargetX,
+ this.x + monitor.x,
+ this.x + monitor.x + monitor.width - this.child.width
+ );
+
+ dialogTargetY = Math.clamp(
+ dialogTargetY,
+ this.y + monitor.y,
+ this.y + monitor.y + monitor.height - this.child.height
+ );
+ }
+ const dialogOffsetX = -dialogX + dialogTargetX;
+ const dialogOffsetY = -dialogY + dialogTargetY;
+
+ 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.ease({
+ background_color: DIALOG_SHADE_NORMAL,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ 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,
+ onComplete: () => {
+ // if the folder grid was build with the estimated icon item size because the real size wasn't available
+ // rebuild it with the real size now, after the folder was realized
+ if (this._needsUpdateSize) {
+ this._updateFolderSize();
+ this._view._redisplay();
+ this._needsUpdateSize = false;
+ }
+ },
+ });
+
+ 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;
+ }
+
+ let [sourceX, sourceY] =
+ this._source.get_transformed_position();
+ let [dialogX, dialogY] =
+ this.child.get_transformed_position();
+
+ this.ease({
+ background_color: Clutter.Color.from_pixel(0x00000000),
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ 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_OUT_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 = [];
+ },
+ });
+
+ this._needsZoomAndFade = false;
+ },
+
+ _setLighterBackground(lighter) {
+ const backgroundColor = lighter
+ ? DIALOG_SHADE_HIGHLIGHT
+ : DIALOG_SHADE_NORMAL;
+
+ this.ease({
+ backgroundColor,
+ duration: FOLDER_DIALOG_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ },
+};
+
+// just make app grid to update all invalid positions that may be result of grid/icon size change
+function _updateIconPositions() {
+ const appDisplay = Main.overview._overview._controls._appDisplay;
+ const icons = [...appDisplay._orderedItems];
+ for (let i = 0; i < icons.length; i++)
+ appDisplay._moveItem(icons[i], -1, -1);
+}
+
+function _removeIcons() {
+ const appDisplay = Main.overview._overview._controls._appDisplay;
+ const icons = [...appDisplay._orderedItems];
+ for (let i = 0; i < icons.length; i++) {
+ const icon = icons[i];
+ if (icon._dialog)
+ Main.layoutManager.overviewGroup.remove_child(icon._dialog);
+ appDisplay._removeItem(icon);
+ icon.destroy();
+ }
+ appDisplay._folderIcons = [];
+}
+
+function _resetAppGrid(settings) {
+ const appDisplay = Main.overview._overview._controls._appDisplay;
+ // reset the grid only if called directly without args or if all folders where removed by using reset button in Settings window
+ // otherwise this function is called every time a user moves icon to another position as a settings callback
+ if (settings) {
+ const currentValue = JSON.stringify(global.settings.get_value('app-picker-layout').deep_unpack());
+ const emptyValue = JSON.stringify([]);
+ const customLayout = currentValue !== emptyValue;
+ // appDisplay._customLayout = customLayout;
+ if (customLayout)
+ return;
+ else
+ opt._appGridNeedsRedisplay = true;
+ }
+
+ // force update icon size using adaptToSize(). the page size cannot be the same as the current one
+ appDisplay._grid.layoutManager._pageWidth += 1;
+ appDisplay._grid.layoutManager.adaptToSize(appDisplay._grid.layoutManager._pageWidth - 1, appDisplay._grid.layoutManager._pageHeight);
+ _removeIcons();
+ appDisplay._redisplay();
+ // force appDisplay to move all icons to proper positions and update all properties
+ GLib.idle_add(0, () => {
+ _updateIconPositions();
+ if (appDisplay._sortOrderedItemsAlphabetically) {
+ appDisplay._sortOrderedItemsAlphabetically();
+ appDisplay._grid.layoutManager._pageWidth += 1;
+ appDisplay._grid.layoutManager.adaptToSize(appDisplay._grid.layoutManager._pageWidth - 1, appDisplay._grid.layoutManager._pageHeight);
+ appDisplay._setLinearPositions(appDisplay._orderedItems);
+ } else {
+ appDisplay._removeItem(appDisplay._orderedItems[0]);
+ appDisplay._redisplay();
+ }
+
+ appDisplay._redisplay();
+ });
+}
+
+function _getWindowApp(metaWin) {
+ const tracker = Shell.WindowTracker.get_default();
+ return tracker.get_window_app(metaWin);
+}
+
+function _getAppLastUsedWindow(app) {
+ let recentWin;
+ global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => {
+ const winApp = _getWindowApp(metaWin);
+ if (!recentWin && winApp === app)
+ recentWin = metaWin;
+ });
+ return recentWin;
+}
+
+function _getAppRecentWorkspace(app) {
+ const recentWin = _getAppLastUsedWindow(app);
+ if (recentWin)
+ return recentWin.get_workspace();
+
+ return null;
+}
+
+const AppIcon = {
+ after__init() {
+ // update the app label behavior
+ this._updateMultiline();
+ },
+
+ // avoid accepting by placeholder when dragging active preview
+ // and also by icon if alphabet or usage sorting are used
+ _canAccept(source) {
+ if (source._sourceItem)
+ source = source._sourceItem;
+ let view = AppDisplay._getViewFromIcon(source);
+
+ return source !== this &&
+ (source instanceof this.constructor) &&
+ (view instanceof AppDisplay.AppDisplay &&
+ !opt.APP_GRID_ORDER);
+ },
+};
+
+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
+ ? AppDisplay.APP_ICON_TITLE_EXPAND_TIME
+ : AppDisplay.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_ORDER)
+ 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
+ if (source._sourceItem) {
+ const app = source._sourceItem.app;
+ source._sourceFolder.removeApp(app);
+ }
+
+ return true;
+ },
+
+};
+
+const ActiveFolderIcon = GObject.registerClass(
+class ActiveFolderIcon extends AppDisplay.AppIcon {
+ _init(app) {
+ super._init(app, {
+ setSizeManually: true,
+ showLabel: false,
+ });
+ }
+
+ handleDragOver() {
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ acceptDrop() {
+ return false;
+ }
+
+ _onDragEnd() {
+ this._dragging = false;
+ this.undoScaleAndFade();
+ Main.overview.endItemDrag(this._sourceItem.icon);
+ }
+});