1
0
Fork 0
gnome-shell-extensions-extra/extensions/47/vertical-workspaces/lib/appDisplay.js
Daniel Baumann af3a3f3a8f
Merging upstream version 20240916 (Closes: #1079257).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 23:20:06 +01:00

2014 lines
72 KiB
JavaScript

/**
* 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;
},
};