/**
 * V-Shell (Vertical Workspaces)
 * appDisplay.js
 *
 * @author     GdH <G-dH@github.com>
 * @copyright  2022 - 2023
 * @license    GPL-3.0
 *
 */

'use strict';

const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Graphene = imports.gi.Graphene;
const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const St = imports.gi.St;

const AppDisplay = imports.ui.appDisplay;
const DND = imports.ui.dnd;
const IconGrid = imports.ui.iconGrid;
const Main = imports.ui.main;

let Me;
let opt;

let _timeouts;

// DIALOG_SHADE_NORMAL = Clutter.Color.from_pixel(0x00000022);
// DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000000);

var AppDisplayModule = class {
    constructor(me) {
        Me = me;
        opt = Me.opt;

        this._firstActivation = true;
        this.moduleEnabled = false;
        this._overrides = null;

        this._appGridLayoutSettings =  null;
        this._appDisplayScrollConId =  0;
        this._appSystemStateConId =  0;
        this._appGridLayoutConId =  0;
        this._origAppViewItemAcceptDrop =  null;
        this._updateFolderIcons =  0;
    }

    cleanGlobals() {
        Me = null;
        opt = 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 = {};

        // Common
        this._overrides.addOverride('FolderView', AppDisplay.FolderView.prototype, FolderView);
        this._overrides.addOverride('FolderIcon', AppDisplay.FolderIcon.prototype, FolderIcon);
        if (opt.APP_GRID_ACTIVE_PREVIEW)
            this._overrides.addOverride('ActiveFolderIcon', AppDisplay.FolderIcon, ActiveFolderIcon);
        this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIcon);
        this._overrides.addOverride('AppDisplay', AppDisplay.AppDisplay.prototype, AppDisplayCommon);
        this._overrides.addOverride('AppViewItem', AppDisplay.AppViewItem.prototype, AppViewItemCommon);
        this._overrides.addOverride('BaseAppViewCommon', AppDisplay.BaseAppView.prototype, BaseAppViewCommon);

        if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) {
            this._overrides.addOverride('AppDisplayVertical', AppDisplay.AppDisplay.prototype, AppDisplayVertical);
            this._overrides.addOverride('BaseAppViewVertical', AppDisplay.BaseAppView.prototype, BaseAppViewVertical);
        }

        // Custom App Grid
        this._overrides.addOverride('AppFolderDialog', AppDisplay.AppFolderDialog.prototype, AppFolderDialog);
        if (Me.shellVersion >= 43) {
            // const defined class needs to be touched before real access
            this._dummy = AppDisplay.AppGrid;
            delete this._dummy;
            // BaseAppViewGridLayout is not exported, we can only access current instance
            this._overrides.addOverride('BaseAppViewGridLayout', Main.overview._overview.controls._appDisplay._appGridLayout, BaseAppViewGridLayout);
            this._overrides.addOverride('FolderGrid', AppDisplay.FolderGrid.prototype, FolderGrid);
        } else {
            this._overrides.addOverride('FolderGrid', AppDisplay.FolderGrid.prototype, FolderGridLegacy);
        }

        this._setAppDisplayOrientation(opt.ORIENTATION === Clutter.Orientation.VERTICAL);
        this._updateDND();
        if (!Main.sessionMode.isGreeter)
            this._updateAppDisplayProperties();

        console.debug('  AppDisplayModule - Activated');
    }

    _disableModule() {
        Me.Modules.iconGridModule.update(true);

        if (this._overrides)
            this._overrides.removeAll();
        this._overrides = null;

        const reset = true;
        this._setAppDisplayOrientation(false);
        this._updateAppDisplayProperties(reset);
        this._updateDND(reset);
        this._restoreOverviewGroup();
        this._removeStatusMessage();

        console.debug('  AppDisplayModule - Disabled');
    }

    _removeTimeouts() {
        if (_timeouts) {
            Object.values(_timeouts).forEach(t => {
                if (t)
                    GLib.source_remove(t);
            });
            _timeouts = null;
        }
    }

    _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
            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 (Me.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 (this._appDisplayScrollConId) {
                appDisplay._adjustment.disconnect(this._appDisplayScrollConId);
                this._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
        this._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
    _updateAppDisplayProperties(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._currentMode = -1;
            appDisplay._grid.setGridModes();
            if (this._appGridLayoutSettings) {
                this._appGridLayoutSettings.disconnect(this._appGridLayoutConId);
                this._appGridLayoutConId = 0;
                this._appGridLayoutSettings = null;
            }
            appDisplay._redisplay();

            appDisplay._grid.set_style('');
            this._updateAppGrid(reset);
        } else {
            // update grid on layout reset
            if (!this._appGridLayoutSettings) {
                this._appGridLayoutSettings = new Gio.Settings({ schema_id: 'org.gnome.shell' });
                this._appGridLayoutConId = this._appGridLayoutSettings.connect('changed::app-picker-layout', this._updateLayout);
            }

            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;`);
            // APP_GRID_SPACING constant is used for grid dimensions calculation
            // but sometimes the actual grid spacing properties affect/change the calculated size, therefore we set it lower to avoid this problem
            // main app grid always use available space and the spacing is optimized for the grid dimensions
            appDisplay._grid.set_style('column-spacing: 5px; row-spacing: 5px;');

            // force redisplay
            appDisplay._grid._currentMode = -1;
            appDisplay._grid.setGridModes();
            appDisplay._grid.layoutManager.fixedIconSize = opt.APP_GRID_ICON_SIZE;
            // avoid resetting appDisplay before startup animation
            // x11 shell restart skips startup animation
            if (!Main.layoutManager._startingUp) {
                this._updateAppGrid();
            } else if (Main.layoutManager._startingUp && (Meta.is_restart() || Me.Util.dashIsDashToDock())) {
                _timeouts.three = GLib.idle_add(GLib.PRIORITY_LOW, () => {
                    this._updateAppGrid();
                    _timeouts.three = 0;
                    return GLib.SOURCE_REMOVE;
                });
            }
        }
    }

    _updateDND(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;
                        Main.overview._overview.controls._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();
        Main.overview._overview._controls._appDisplay.translation_x = 0;
        Main.overview._overview._controls._appDisplay.translation_y = 0;
        Main.overview._overview._controls._appDisplay.visible = true;
        Main.overview._overview._controls._appDisplay.opacity = 255;
    }

    // update all invalid positions that may be result of grid/icon size change
    _updateIconPositions() {
        const appDisplay = Main.overview._overview._controls._appDisplay;
        const layout = JSON.stringify(global.settings.get_value('app-picker-layout').recursiveUnpack());
        // if app grid layout is empty, sort source alphabetically to avoid misplacing
        if (layout === JSON.stringify([]) && appDisplay._sortOrderedItemsAlphabetically)
            appDisplay._sortOrderedItemsAlphabetically();
        const icons = [...appDisplay._orderedItems];
        for (let i = 0; i < icons.length; i++)
            appDisplay._moveItem(icons[i], -1, -1);
    }

    _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 = [];
    }

    _removeStatusMessage() {
        if (Me._vShellStatusMessage) {
            if (Me._vShellMessageTimeoutId) {
                GLib.source_remove(Me._vShellMessageTimeoutId);
                Me._vShellMessageTimeoutId = 0;
            }
            Me._vShellStatusMessage.destroy();
            Me._vShellStatusMessage = null;
        }
    }

    _updateLayout(settings, key) {
        const currentValue = JSON.stringify(settings.get_value(key).deep_unpack());
        const emptyValue = JSON.stringify([]);
        const customLayout = currentValue !== emptyValue;
        if (!customLayout) {
            this._updateAppGrid();
        }
    }

    _updateAppGrid(reset = false, callback) {
        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

        // 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);

        // don't delay the first screen lock on GS < 44, removing icons takes a time and with other 15 enabled extensions it can be multiplied by 15
        if (!Main.sessionMode.isLocked)
            this._removeIcons();

        appDisplay._redisplay();
        // don't realize appDisplay on disable, or at startup if disabled
        // always realize appDisplay otherwise to avoid errors while opening folders (that I was unable to trace)
        if (reset || (!opt.APP_GRID_PERFORMANCE && callback)) {
            this._removeStatusMessage();
            if (callback)
                callback();
            return;
        }

        // workaround - silently realize appDisplay
        // appDisplay and its content must be "visible" (opacity > 0) on the screen (within monitor geometry)
        // to realize its objects
        // this action takes some time and affects animations during the first use
        // if we do it invisibly before user needs it, it can improve the user's experience

        this._exposeAppGrid();

        // let the main loop process our changes before continuing
        _timeouts.one = GLib.idle_add(GLib.PRIORITY_LOW, () => {
            this._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);
            }

            appDisplay._redisplay();
            // realize also all app folders (by opening them) so the first popup is as smooth as the second one
            // let the main loop process our changes before continuing
            _timeouts.two = GLib.idle_add(GLib.PRIORITY_LOW, () => {
                this._restoreAppGrid();
                Me._resetInProgress = false;
                this._removeStatusMessage();

                if (callback)
                    callback();

                _timeouts.two = 0;
                return GLib.SOURCE_REMOVE;
            });
            _timeouts.one = 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;
        }

        const appDisplay = Main.overview._overview._controls._appDisplay;
        appDisplay.opacity = 1;

        // find usable value, sometimes it's one, sometime the other...
        let [x, y] = appDisplay.get_position();
        let { x1, y1 } = appDisplay.allocation;
        x = x === Infinity ? 0 : x;
        y = y === Infinity ? 0 : y;
        x1 = x1 === Infinity ? 0 : x1;
        y1 = y1 === Infinity ? 0 : y1;
        appDisplay.translation_x = -(x ? x : x1);
        appDisplay.translation_y = -(y ? y : y1);
        this._exposeAppFolders();
    }

    _exposeAppFolders() {
        const appDisplay = Main.overview._overview._controls._appDisplay;
        appDisplay._folderIcons.forEach(d => {
            d._ensureFolderDialog();
            d._dialog._updateFolderSize();
            d._dialog.scale_y = 0.0001;
            d._dialog.show();
        });
    }

    _restoreAppGrid() {
        const appDisplay = Main.overview._overview._controls._appDisplay;
        appDisplay.translation_x = 0;
        appDisplay.translation_y = 0;
        // appDisplay.opacity = 0;
        this._hideAppFolders();

        const overviewGroup = Main.layoutManager.overviewGroup;
        if (!Main.overview._shown)
            overviewGroup.hide();
        overviewGroup.scale_y = 1;
        overviewGroup.opacity = 255;

        this._removeStatusMessage();
    }

    _hideAppFolders() {
        const appDisplay = Main.overview._overview._controls._appDisplay;
        appDisplay._folderIcons.forEach(d => {
            if (d._dialog) {
                d._dialog._updateFolderSize();
                d._dialog.hide();
                d._dialog.scale_y = 1;
            }
        });
    }

    _getWindowApp(metaWin) {
        const tracker = Shell.WindowTracker.get_default();
        return tracker.get_window_app(metaWin);
    }

    _getAppLastUsedWindow(app) {
        let recentWin;
        global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => {
            const winApp = this._getWindowApp(metaWin);
            if (!recentWin && winApp === app)
                recentWin = metaWin;
        });
        return recentWin;
    }

    _getAppRecentWorkspace(app) {
        const recentWin = this._getAppLastUsedWindow(app);
        if (recentWin)
            return recentWin.get_workspace();

        return null;
    }
};

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_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;
    },

    // 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 (Me.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 folder preview
    acceptDrop(source) {
        if (opt.APP_GRID_USAGE)
            return false;
        if (source._sourceItem)
            source = source._sourceItem;

        if (!BaseAppViewCommon.acceptDrop.bind(this)(source))
            return false;

        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 (Me.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) {
                console.warn(`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
            }
        });

        // different options for root app grid and app folders
        const thisIsFolder = this instanceof AppDisplay.FolderView;
        const thisIsAppDisplay = !thisIsFolder;
        if ((opt.APP_GRID_ORDER && thisIsAppDisplay) ||
        (opt.APP_FOLDER_ORDER && thisIsFolder)) {
            // const { itemsPerPage } = this._grid;
            let appIcons = this._orderedItems;
            // sort all alphabetically
            this._sortOrderedItemsAlphabetically(appIcons);
            // appIcons.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
            // then sort used apps by usage
            if ((opt.APP_GRID_USAGE && thisIsAppDisplay) ||
                (opt.APP_FOLDER_USAGE && thisIsFolder))
                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 && thisIsAppDisplay)
                appIcons.sort((a, b) => a.app.get_state() !== Shell.AppState.RUNNING && b.app.get_state() === Shell.AppState.RUNNING);

            if (opt.APP_GRID_FOLDERS_FIRST)
                appIcons.sort((a, b) => b._folder && !a._folder);
            else if (opt.APP_GRID_FOLDERS_LAST)
                appIcons.sort((a, b) => a._folder && !b._folder);

            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 source instanceof AppDisplay.AppViewItem;
    },

    // support active preview icons
    acceptDrop(source) {
        if (!this._canAccept(source))
            return false;

        let dropTarget = null;
        if (Me.shellVersion >= 43) {
            dropTarget = this._dropTarget;
            delete this._dropTarget;
        }

        if (!this._canAccept(source))
            return false;

        if ((Me.shellVersion < 43 && this._dropPage) ||
                (Me.shellVersion >= 43 && (dropTarget === this._prevPageIndicator ||
                dropTarget === this._nextPageIndicator))) {
            let increment;

            if (Me.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) {
                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 (Me.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 (Me.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();
            }
        }

        const thisIsFolder = this instanceof AppDisplay.FolderView;
        const thisIsAppDisplay = !thisIsFolder;
        if ((!opt.APP_GRID_ORDER && thisIsAppDisplay) || (!opt.APP_FOLDER_ORDER  && thisIsFolder))
            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;
    },

    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 = new AppDisplay.FolderGrid();
        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.icon.set_style('margin: 0; padding: 0;');
                    child._dot.set_style('margin-bottom: 1px;');
                    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, but not if it's empty
        /* if (this._dialog && this._dialog._designCapacity !== this._orderedItems.length && 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);
        });

        if (opt.APP_FOLDER_ORDER)
            Main.overview._overview.controls._appDisplay._sortOrderedItemsAlphabetically(items);

        if (opt.APP_FOLDER_USAGE)
            items.sort((a, b) => Shell.AppUsage.get_default().compare(a.app.id, b.app.id));

        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 FolderGridLegacy = {
    _init() {
        IconGrid.IconGrid.prototype._init.bind(this)({
            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,
        });
        this.layout_manager._isFolder = true;
        // if (!opt.APP_GRID_FOLDER_DEFAULT)
        const spacing = opt.APP_GRID_SPACING;
        this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
        this.layoutManager.fixedIconSize = opt.APP_GRID_FOLDER_ICON_SIZE;
    },

    adaptToSize(width, height) {
        this.layout_manager.adaptToSize(width, height);
    },
};

const FolderGrid = {
    _init() {
        AppDisplay.AppGrid.prototype._init.bind(this)({
            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,
        });
        this.layout_manager._isFolder = true;
        const spacing = opt.APP_GRID_SPACING;
        this.set_style(`column-spacing: ${spacing}px; row-spacing: ${spacing}px;`);
        this.layoutManager.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() {
        this._viewBox.add_style_class_name('app-folder-dialog-vshell');

        // 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);

        // Adjust empty actor to center the title
        this._entryBox.get_first_child().width = 82;
    },

    after__addFolderNameEntry() {
        // Edit button
        this._removeButton = new St.Button({
            style_class: '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) {
                this._grabHelper.ungrab({ actor: this });
                // without hiding the dialog, Shell crashes (at least on X11)
                this.hide();
                this._view._deletingFolder = true;

                // Resetting all keys deletes the relocatable schema
                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 = this._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);
    },

    popup() {
        if (this._isOpen)
            return;

        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;

        // the first folder dialog realization needs size correction
        // so set the folder size, let it realize and then update the folder content
        if (!this.realized) {
            this._updateFolderSize();
            GLib.idle_add(
                GLib.PRIORITY_DEFAULT,
                () => {
                    this._updateFolderSize();
                }
            );
        }

        this.show();
        this.emit('open-state-changed', true);
    },

    _updateFolderSize() {
        const view = this._view;
        const [firstItem] = view._grid.layoutManager._container;
        if (!firstItem)
            return;
        // adapt folder size according to the settings and number of icons
        const appDisplay = this._source._parentView;
        if (!appDisplay.width || appDisplay.allocation.x2 === Infinity || appDisplay.allocation.x2 === -Infinity) {
            return;
        }

        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 itemPadding = 55; // default icon item padding on Fedora 44
        // const dialogMargin = 30;
        const nItems = view._orderedItems.length;
        let columns = opt.APP_GRID_FOLDER_COLUMNS;
        let rows = opt.APP_GRID_FOLDER_ROWS;
        const fullAdaptiveGrid = !columns && !rows;
        let spacing = opt.APP_GRID_SPACING;
        const minItemSize = 48 + itemPadding;

        if (fullAdaptiveGrid) {
            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;
        view._grid.layoutManager.fixedIconSize = iconSize;

        let itemSize = iconSize + 55; // 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.realized) {
            firstItem.icon.setIconSize(iconSize);
            const [firstItemWidth] = firstItem.get_preferred_size();
            const realSize = firstItemWidth / scaleFactor;
            // if the preferred item size is smaller than icon plus some padding, ignore it
            // (icons that are not yet realized are returning sizes like 45 or 53)
            if (realSize > (iconSize + 24))
                itemSize = realSize;
        }

        let width = columns * (itemSize + spacing) + /* padding for nav arrows*/64;
        width = Math.round(width + (opt.ORIENTATION ? 100 : 160/* space for navigation arrows*/));
        let height = rows * (itemSize + spacing) + /* header*/75 + /* padding*/ 2 * 30 + /* padding + ?page indicator*/(!opt.ORIENTATION || !opt.APP_GRID_FOLDER_COLUMNS ? 100 : 70);

        // allocation is more reliable than appDisplay width/height properties
        const appDisplayWidth = appDisplay.allocation.x2 - appDisplay.allocation.x1;
        const appDisplayHeight = appDisplay.allocation.y2 - appDisplay.allocation.y1 + (opt.SHOW_SEARCH_ENTRY ? Main.overview._overview.controls._searchEntryBin.height : 0);

        // folder must fit the appDisplay area
        // reduce columns/rows if needed and count with the scaled values
        if (!opt.APP_GRID_FOLDER_ROWS) {
            while ((height * scaleFactor) > appDisplayHeight) {
                height -= itemSize + spacing;
                rows -= 1;
            }
        }

        if (!opt.APP_GRID_FOLDER_COLUMNS) {
            while ((width * scaleFactor) > appDisplayWidth) {
                width -= itemSize + spacing;
                columns -= 1;
            }
        }
        // try to compensate for the previous reduction if there is a space
        if (!opt.APP_GRID_FOLDER_COLUMNS) {
            while ((nItems > columns * rows) && ((width * scaleFactor + itemSize + spacing) <= appDisplayWidth)) {
                width += itemSize + spacing;
                columns += 1;
            }
            // remove columns that cannot be displayed
            if ((columns * minItemSize  + (columns - 1) * spacing) > appDisplayWidth)
                columns = Math.floor(appDisplayWidth / (minItemSize + spacing));
        }
        if (!opt.APP_GRID_FOLDER_ROWS) {
            while ((nItems > columns * rows) && ((height * scaleFactor + itemSize + spacing) <= appDisplayHeight)) {
                height += itemSize + spacing;
                rows += 1;
            }
            // remove rows that cannot be displayed
            if ((rows * minItemSize  + (rows - 1) * spacing) > appDisplayHeight)
                rows = Math.floor(appDisplayWidth / (minItemSize + spacing));
        }

        width = Math.clamp(width, 640, appDisplayWidth);
        height = Math.min(height, appDisplayHeight);

        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._grid.layoutManager._pageWidth += 1;
        view._grid.layoutManager.adaptToSize(view._grid.layoutManager._pageWidth - 1, view._grid.layoutManager._pageHeight);
        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;

        const appDisplay = this._source._parentView;
        const [appDisplayX, appDisplayY] = this._source._parentView.get_transformed_position();

        if (!opt.APP_GRID_FOLDER_CENTER) {
            dialogTargetX = sourceCenterX - this.child.width / 2;
            dialogTargetY = sourceCenterY - this.child.height / 2;

            // keep the dialog in appDisplay area if possible
            dialogTargetX = Math.clamp(
                dialogTargetX,
                appDisplayX,
                appDisplayX + appDisplay.width - this.child.width
            );

            dialogTargetY = Math.clamp(
                dialogTargetY,
                appDisplayY,
                appDisplayY + appDisplay.height - this.child.height
            );
        } else {
            const searchEntryHeight = opt.SHOW_SEARCH_ENTRY ? Main.overview._overview.controls._searchEntryBin.height : 0;
            dialogTargetX = appDisplayX + appDisplay.width / 2 - this.child.width / 2;
            dialogTargetY = appDisplayY - searchEntryHeight + ((appDisplay.height + searchEntryHeight) / 2 - this.child.height / 2) / 2;
        }

        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_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 = [];
            },
        });

        const appDisplay = this._source._parentView;
        appDisplay.ease({
            opacity: 255,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        if (opt.SHOW_SEARCH_ENTRY) {
            Main.overview.searchEntry.ease({
                opacity: 255,
                duration: FOLDER_DIALOG_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        this._needsZoomAndFade = false;
    },

    _setLighterBackground(lighter) {
        if (this._isOpen) {
            const appDisplay = this._source._parentView;
            appDisplay.ease({
                opacity: lighter ? 20 : 0,
                duration: FOLDER_DIALOG_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        /* const backgroundColor = lighter
            ? DIALOG_SHADE_HIGHLIGHT
            : DIALOG_SHADE_NORMAL;

        this.ease({
            backgroundColor,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        }); */
    },
};

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_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
            ? 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_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
        if (source._sourceItem) {
            const app = source._sourceItem.app;
            source._sourceFolder.removeApp(app);
        }

        return true;
    },

};

const ActiveFolderIcon = {
    handleDragOver() {
        return DND.DragMotionResult.CONTINUE;
    },

    acceptDrop() {
        return false;
    },

    _onDragEnd() {
        this._dragging = false;
        this.undoScaleAndFade();
        Main.overview.endItemDrag(this._sourceItem.icon);
    },
};
