/* DING: Desktop Icons New Generation for GNOME Shell
 *
 * Gtk4 Port Copyright (C) 2022-26 Sundeep Mediratta (smedius@gmail.com)
 * Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
 * Based on code original (C) Carlos Soriano
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import {
    AppChooser,
    AskRenamePopup,
    AutoAr,
    DesktopMenu,
    DesktopMonitor,
    DragManager,
    FileItemMenu,
    GnomeShellDragDrop,
    ShortcutManager,
    ShowErrorPopup,
    StackItem,
    TemplatesScriptsManager,
    WindowManager,
    WidgetManager
} from '../dependencies/localFiles.js';

import {Adw, Gtk, Gdk, Gio, GLib, GLibUnix} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';

export {DesktopManager};

const DesktopManager = class {
    constructor(Data, Utils, desktopList, codePath, asDesktop, primaryIndex) {
        // Inherit
        this.mainApp = Data.mainApp;
        this.codePath = codePath;
        this._asDesktop = asDesktop;
        if (asDesktop) {
            // Don't close the application if there are no desktops
            this.mainApp.hold();
            this._hold_active = true;
        }

        this.GnomeShellVersion = Data.gnomeversion;

        this.uuid = Data.uuid;

        // Init and import Scripts and classes
        this.DesktopIconsUtil = Utils.DesktopIconsUtil;
        this.FileUtils = Utils.FileUtils;
        this.Enums = Data.Enums;
        this.DBusUtils = Utils.DBusUtils;
        this.dbusManager = Utils.DBusUtils.dbusManagerObject;
        this.Prefs = Utils.Preferences;
        this.showErrorPopup = ShowErrorPopup;
        this.templatesScriptsManager = TemplatesScriptsManager;
        this.GnomeShellDragDrop = GnomeShellDragDrop;
        this.appChooser = AppChooser;
        this.ThumbnailLoader = Utils.ThumbnailLoader;

        // init methods
        this.dragManager = new DragManager.DragManager(this);
        this.windowManager = new WindowManager.WindowManager(this,
            desktopList,
            asDesktop,
            primaryIndex
        );
        this.desktopMonitor = new DesktopMonitor.DesktopMonitor(this);
        this.autoAr = new AutoAr.AutoAr(this);
        this.fileItemMenu = new FileItemMenu.FileItemMenu(this);
        this.fileItemActions = new FileItemMenu.FileItemActions(this);
        this.desktopActions = new DesktopMenu.DesktopActions(this);
        this.desktopMenuManager = new DesktopMenu.DesktopBackgroundMenu(this);
        this.Prefs.init(this);
        this.shortcutManager = new ShortcutManager(this);
        this.widgetManager = new WidgetManager.WidgetManager(this);
        this.mainApp.getDialogParentWindow =
            this.getDialogParentWindow.bind(this);

        // Init Variables
        this._clickX = null;
        this._clickY = null;
        this._compositeStackList = null;
        this._displayList = [];
        this._desktopUpdateQueue = Promise.resolve();
        // Track last seen keyboard modifier state so accelerators can query it
        this._lastModifierState = 0;
        this.ignoreKeys = this.Enums.IgnoreKeys.map(_k => Gdk[_k]);

        // setup gracefull termination
        if (this._asDesktop) {
            this._sigtermID = GLibUnix.signalAdd(
                GLib.PRIORITY_DEFAULT,
                15,
                () => {
                    GLib.source_remove(this._sigtermID);
                    this.terminateProgram();
                    if (this._hold_active) {
                        this.mainApp.release();
                        this._hold_active = false;
                    }
                    return false;
                }
            );
        }
        this._syncStartupDesktop().catch(e => logError(e));
    }

    getDialogParentWindow(window = null) {
        const activeWindow = window ?? this.mainApp.get_active_window();
        if (!activeWindow)
            return null;

        if (this.widgetManager) {
            return this.widgetManager.resolveSurfaceWindowFromActiveWindow(
                activeWindow
            );
        }

        return activeWindow;
    }

    runSerializedDesktopMutation(updateFn) {
        // Run desktop updates one at a time. Each call keeps its own turn, so
        // this serializes updates but does not merge them into a single call.
        //
        // Callers must pass a function with the correct `this` binding already
        // applied; `then()` invokes it directly.
        const nextUpdate = this._desktopUpdateQueue.then(updateFn);

        // Keep the queue chain alive even if one update fails; callers still
        // get the rejection from `nextUpdate`.
        this._desktopUpdateQueue = nextUpdate.catch(() => {});

        return nextUpdate;
    }

    _runSerializedDrawDesktop(fileList, opts) {
        return this.runSerializedDesktopMutation(
            this._drawDesktop.bind(this, fileList, opts)
        );
    }

    async _syncStartupDesktop() {
        // startup in a particular order
        // First create and make sure windows are created
        const windowscreated = new Promise(resolve => {
            this.windowsPromiseResolve = resolve;
            this.windowManager.createGridWindows().catch(e => logError(e));
            // If this desktop List is null, ask for a new one
            this.windowManager.requestGeometryUpdate();
        });

        // Monitor is attached, windows are created with proper geometry
        await windowscreated.catch(e => logError(e));

        // Now we can actually display errors, so check for them
        // Ensure that there is a 'Desktop' folder set and it exists
        // Verify if Gnome Files is available and executable, otherwise warn
        // Confirm Gnome Files is registered with xdg-utils
        // to handle inode/directory
        this._performSanityChecks().catch(e => logError(e));

        // The initialRead parameter insures tha grid positions are recalculated
        // and recaculated postions of all fileItems will be re-written to
        // disk with write mode 'OVERWRITE'
        const initialRead = true;

        // prior fileList, even if triggered through desktopdir changes
        // will not be displayed as windows were not there.
        const fileList = await this.desktopMonitor.getFileList();

        // This is no longer needed, if true it blocks and all updates.
        this.windowsPromiseResolve = null;

        await this._runSerializedDrawDesktop(
            fileList,
            {initialRead}
        ).catch(e => {
            logError(e);
        });
        // First intitiation complete, valid file read from
        // desktopdir, even if a prior fileList was read, the
        // forced new read will recalculate and resave new
        // normalized coordinates and monitor information.

        this._startWidgetDisplay();

        // force a queue draw of all windows now that we have drawn the desktop,
        // and poke mutter to map the meta window.
        this.windowManager.queue_draw();
    }

    async _performSanityChecks() {
        // show error if monitor frame buffer scaling is not enabled first,
        //  as windows may be awry
        if (this.windowManager.differentZooms &&
            !this.fractionalScaling &&
            !this._framebufferWarningDone) {
            const header = _('Monitor Frame Buffer Scaling is not enabled');
            const text = _('Multiple monitors with different zoom settings, recommend per monitor framebuffer scaling.\n\nPlease enable in Mutter Dconf Settings');
            // show notification as well as error dialog as windows may
            //  not be postioned correctly
            this.dbusManager.doNotify(header, text);
            this._framebufferWarningDone = true;

            const window = this.getDialogParentWindow();
            const dialog = new Adw.AlertDialog();
            dialog.set_body_use_markup(true);
            dialog.set_heading_use_markup(true);
            dialog.set_heading(header);
            const secondaryText = _('Multiple monitors with different zoom settings.\n\nEnable per monitor framebuffer scaling in Mutter Dconf Settings?');
            dialog.set_body(secondaryText);
            dialog.add_response('cancel', _('Cancel'));
            dialog.add_response('enable', _('Enable'));
            dialog.set_close_response('cancel');
            dialog.set_default_response('enable');
            dialog.set_response_appearance(
                'enable', Adw.ResponseAppearance.SUGGESTED);
            dialog.set_response_appearance(
                'cancel', Adw.ResponseAppearance.DEFAULT);
            dialog.set_prefer_wide_layout(true);
            const runDialog = new Promise(resolve => {
                dialog.choose(window, null, (actor, asyncResult) => {
                    const response = actor.choose_finish(asyncResult);
                    if (response === 'enable')
                        this.Prefs.fractionalScaling = true;
                    dialog.close();
                    resolve(response);
                });
            });
            await runDialog;
        }

        const fileType = this._desktopDir.query_file_type(
            Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
            null);
        if (fileType === Gio.FileType.SYMBOLIC_LINK) {
            const header = _('Can Not Show the Desktop');
            // TRANSLATORS: {desktop} folder path automatically inserted
            const text = _('The Desktop folder {desktop} is a Symbolic Link\n\nPlease set the Desktop Folder to a real Folder');
            const errorDialog = this.showError(
                header,
                text.replace('{desktop}', this._desktopDir.get_path())
            );
            await errorDialog.run();
            this._desktops.forEach(d => d.setErrorState());
        } else if (fileType !== Gio.FileType.DIRECTORY) {
            const header = _('Can Not Show the Desktop');
            // TRANSLATORS: {desktop} folder path automatically inserted
            const text = _('The Desktop folder {desktop} does not exist, or is not a Directory\n\nCheck your xdg-utils installation and set the correct Desktop Folder');
            const errorDialog = this.showError(
                header,
                text.replace('{desktop}', this._desktopDir.get_path())
            );
            await errorDialog.run();
            this._desktops.forEach(d => d.setErrorState());
        }

        const inodeHandlers = Gio.AppInfo.get_all_for_type('inode/directory');
        if (!GLib.find_program_in_path('nautilus')) {
            const header = _('GNOME Files not found');
            const text = _('The GNOME Files application is required by Gtk4 Desktop Icons NG.');
            const errorDialog = this.showError(header, text);
            await errorDialog.run();
        }

        if (!inodeHandlers.length) {
            const helpURL = 'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
            const header = _('There is no default File Manager');
            const text = _('There is no application that handles mimetype "inode/directory"');
            const errorDialog = this.showError(header, text, helpURL);
            await errorDialog.run();
        }

        if (!inodeHandlers.some(a => a.get_id() === 'org.gnome.Nautilus.desktop')) {
            const helpURL = 'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
            const header = _('Gnome Files is not registered as a File Manager');
            const text = _('The Gnome Files application is not programmed to open Folders!\nCheck your xdg-utils installation\nCheck Gnome Files .desktop File installation');
            const errorDialog = this.showError(header, text, helpURL);
            await errorDialog.run();
        }
    }

    showError(text, secondaryText, helpURL = null, timeout = 0,
        parentWindow = null) {
        const window = this.getDialogParentWindow(parentWindow);
        const errorDialog = new ShowErrorPopup.ShowErrorPopup(
            text,
            secondaryText,
            this.DesktopIconsUtil.waitDelayMs,
            helpURL,
            window
        );

        if (timeout)
            errorDialog.runAutoClose(timeout);

        return errorDialog;
    }

    closeFocusStealingWindows() {
        this.desktopActions?.preferencesWindow?.close();
        this.shortcutManager?.closeShortcutViewer();
    }

    terminateProgram() {
        this.desktopMonitor.stopMonitoring();

        if (this._dbusGeometryIface)
            this._dbusGeometryIface.unexport();

        if (this._compositeStackList && this._compositeStackList.length) {
            this._displayList.forEach(f => {
                if (f.isStackMarker)
                    f.onDestroy();
            });
            this._compositeStackList.forEach(f => f.onDestroy());
        } else {
            this._displayList.forEach(f => f.onDestroy());
        }

        this._stopWidgetDisplay();
        this.windowManager.destroyDesktops();
    }

    // Keyboard and Mouse Events

    onPressButton(X, Y, x, y, button, shiftPressed, controlPressed) {
        this._clickX = Math.floor(X);
        this._clickY = Math.floor(Y);

        // Left Click
        if (button === 1) {
            if (!shiftPressed && !controlPressed) {
                // clear selection
                this.unselectAll();
            }
            this.dragManager.startRubberband(X, Y);
        }
    }

    async onReleaseButton(X, Y, x, y, button, isShift, isCtrl, grid) {
        // Right Click
        if (button === 3) {
            await this.desktopMenuManager
                .showDesktopMenu(x, y, grid)
                .catch(e => logError(e));
        }
    }

    onLongPressButton(_X, _Y, _x, _y, button, _isShift, _isCtrl, _grid) {
        if (button === 3)
            this.mainApp.activate_action('displayShellBackgroundMenu', null);
    }

    onWidgetDisplayChanged() {
        if (this.Prefs.showDesktopWidgets)
            this._startWidgetDisplay();
        else
            this._stopWidgetDisplay();
    }

    _startWidgetDisplay() {
        this.widgetManager
        .startWidgetDisplay(this._desktops, {redisplay: true})
        .catch(e => logError(e));
    }

    _stopWidgetDisplay() {
        this.widgetManager.stopWidgetDisplay();
    }

    onKeyPress(keyval, keycode, state, grid) {
        this.keyEventGrid = grid;
        if (this.popupmenu || this.fileItemMenu.popupmenu)
            return true;

        if (this.ignoreKeys.includes(keyval))
            return true;

        let key = String.fromCharCode(Gdk.keyval_to_unicode(keyval));
        if (this.keypressTimeoutID && this.searchString)
            this.searchString = this.searchString.concat(key);
        else
            this.searchString = key;

        if (this.searchString !== '') {
            let found = this._scanForFiles(this.searchString, false);
            if (found) {
                if ((this.getNumberOfSelectedItems() >= 1) &&
                    !this.keypressTimeoutID) {
                    const secondaryText = null;
                    const helpURL = null;
                    const timeoutClose = 2000; // In ms
                    this.showError(
                        _('Clear current selection before new search'),
                        secondaryText,
                        helpURL,
                        timeoutClose
                    );
                    return true;
                }
                this.searchEventTime = GLib.get_monotonic_time();
                if (!this.keypressTimeoutID) {
                    this.keypressTimeoutID =
                        GLib.timeout_add(
                            GLib.PRIORITY_DEFAULT,
                            1000,
                            () => {
                                if (GLib.get_monotonic_time() -
                                    this.searchEventTime < 1500000)
                                    return true;

                                this.searchString = null;
                                this.keypressTimeoutID = null;

                                if (this._findFileWindow) {
                                    this._findFileWindow
                                        .response(Gtk.ResponseType.OK);
                                }

                                return false;
                            });
                }
                this.findFiles(this.searchString);
            }
            return true;
        } else {
            return false;
        }
    }

    updateModifierState(state) {
        // cache modifier state for use by accelerator-driven handlers
        this._lastModifierState = state;
    }

    clearModifierState() {
        // reset cached modifier state
        this._lastModifierState = 0;
    }

    closePopUps() {
        if (this._renameWindow) {
            this._renameWindow.close();
            return true;
        }
        if (this.dialogCancellable) {
            this.dialogCancellable.cancel();
            this.dialogCancellable = null;
            return true;
        }
        return false;
    }

    clearAllLayersFromGrids(layoutChange = {}) {
        // Icons: clear from all grids
        this._displayList.forEach(x => x.removeFromGrid());

        // Widgets: clear from all grids
        this.widgetManager.clearFromGrids(layoutChange);
    }

    async applyDesktopLayoutChange({
        redisplay,
        monitorschanged,
        gridschanged,
    } = {}) {
        this._performSanityChecks();

        // Icons first
        await this._reFrameDesktop({
            redisplay,
            monitorschanged,
            gridschanged,
        });

        await this.widgetManager.applyLayoutChange(this._desktops, {
            redisplay,
            monitorschanged,
            gridschanged,
        });
    }


    // Destktop Icon Placement and Display
    // ********************************************************************** */

    _removeAllFilesFromGrids() {
        for (let fileItem of this._displayList)
            fileItem.removeFromGrid({callOnDestroy: true});

        this._displayList = [];
    }

    _clearAllFilesFromGrids() {
        for (let fileItem of this._displayList)
            fileItem.removeFromGrid({callOnDestroy: false});

        this._displayList = [];
    }

    async _drawDesktop(fileList, opts = {initialRead: false}) {
        if (this.windowsPromiseResolve || !fileList)
            return;
        const selectedFiles = this.getCurrentSelectionAsUri();
        const displayList = [...fileList];

        //* Remove all files from the grids just before placing new files to
        // prevent flickering icons *//
        if (opts.initialRead)
            this._removeAllFilesFromGrids();
        else
            this._clearAllFilesFromGrids();
        this._displayList = displayList;

        this._placeAllFilesOnGrids(opts);

        const drawComplete = displayList.map(async fileItem => {
            // Start icon loading early so late loads settle before first paint.
            await fileItem.updateIcon();
            if (selectedFiles) {
                if (selectedFiles.includes(fileItem.uri))
                    fileItem.setSelected();
            }
            // Wait for either snapshot placement or the not-shown fallback.
            await fileItem.iconPlaced;
        });

        await Promise.all([...drawComplete]);

        //* Desktop draw/paint is now complete *//
        //* Reposition open Menus, renameFileItem pop up's **//
        //* Any task after complete desktop draw can now be done *//
        this._refreshMenus();
    }

    _refreshMenus() {
        if ((this.newItemDoRename && this.newItemDoRename.size) ||
            this.fileItemMenu.popupmenu ||
            this.activeFileItem) {
            let activeItem = false;
            let newItemDoRename = false;

            this._displayList.forEach(f => {
                if (this.activeFileItem &&
                    (f.fileName === this.activeFileItem.fileName))
                    this.activeFileItem = activeItem = f;

                if (this.newItemDoRename &&
                    this.newItemDoRename.has(f.fileName))
                    newItemDoRename = f;
            });

            if (this._renameWindow)
                this._renameWindow.close();

            if (newItemDoRename) {
                newItemDoRename.setSelected();
                const allowReturnOnSameName = true;
                this.doRename(newItemDoRename, allowReturnOnSameName)
                .catch(e => logError(e));
            }

            if (this.fileItemMenu.popupmenu) {
                if (!activeItem)
                    this.fileItemMenu.popupmenu.popdown();
            }

            if (!activeItem)
                this.activeFileItem = null;
        }
    }

    _placeAllFilesOnGrids(opts = {redisplay: false}) {
        if (this.Prefs.keepStacked) {
            this._doStacks(opts);
            return;
        }
        if (this.Prefs.keepArranged) {
            this._doSorts(opts);
            return;
        }
        let storeMode = this.Enums.StoredCoordinates.PRESERVE;
        if (opts.redisplay ||
            opts.initialRead) {
            // write the new recomputed positions to metadata when assigned
            storeMode = this.Enums.StoredCoordinates.OVERWRITE;
            this._sortByCurrentPosition();
            this._recomputeWindowPositions();
        }
        if (opts.gridschanged && !this.Prefs.freePositionIcons) {
            // if snap to grid, recompute column, row for fileItems
            // so they end up in the same relative grid, otherwise they keep
            // shifting postions. This keeps them in the same relative grid
            // position. For snap to grid this will apply the new  global x,y of
            // the grid assigned
            this._recomputeGridPositions();
        }
        this._addFilesToDesktop(this._displayList, storeMode);
    }

    _recomputeGridPositions(fileList) {
        if (!fileList)
            fileList = this._displayList;

        fileList.forEach(fileItem => {
            if (fileItem.savedCoordinates === null)
                return;

            if (fileItem._monitorIndex == null)
                return;

            const column = fileItem.column;
            const row = fileItem.row;

            if (column == null || row == null)
                return;

            const index = fileItem._monitorIndex;
            const [desktop] = this._desktops.filter(d => {
                return d.monitorIndex === index;
            });

            if (!desktop)
                return;

            const [newGlobalX, newGlobalY] =
                desktop.recomputeGridPosition(column, row);

            fileItem.temporarySavedPosition = [newGlobalX + 2, newGlobalY + 2];
        });
    }

    _recomputeWindowPositions(fileList) {
        if (!fileList)
            fileList = this._displayList;

        if (!this._desktops.length)
            return;

        fileList.forEach(fileItem => {
            if (fileItem.savedCoordinates == null)
                return;
            if (fileItem._normalCoordinates == null) {
                fileItem.savedCoordinates = null;
                return;
            }
            if (fileItem._monitorIndex == null)
                return;

            const itemMonitorIndex = fileItem._monitorIndex;
            let desktop;

            // reassign to monitors
            // if on primary monitor, reassign to new primary
            if (itemMonitorIndex === this._priorPrimaryMonitorIndex &&
                this._primaryMonitorIndex != null) {
                if (!this.Prefs.showOnSecondaryMonitor) {
                    [desktop] = this._desktops.filter(d => {
                        return d.monitorIndex === this._primaryMonitorIndex;
                    });
                } else {
                    desktop = this.preferredDisplayDesktop;
                }
            }

            // reassign not on primary monitor to prior monitor if
            // if the prior monitor is still in index
            if (!desktop) {
                [desktop] = this._desktops.filter(d => {
                    return d.monitorIndex === itemMonitorIndex;
                });
            }

            // reassingn to new monitor, prior monitor not available
            if (!desktop)
                desktop = this.preferredDisplayDesktop;

            // if any error, leave unmapped to new monitor, placement algorithm
            //  will find placement from the old global position
            if (!desktop)
                return;

            fileItem.temporaryMonitorIndex = desktop.monitorIndex;

            // recompute coordinates for the new monitor
            const x = fileItem._normalCoordinates[0];
            const y = fileItem._normalCoordinates[1];
            const [newlocalX, newlocalY] =
                desktop.setNormalizedCoordinates(x, y);
            const [newGlobalX, newGlobalY] =
                desktop.coordinatesLocalToGlobal(newlocalX, newlocalY);
            fileItem.temporarySavedPosition = [newGlobalX, newGlobalY];
        });
    }

    _addFilesToDesktop(fileList, storeMode) {
        let preferredDesktop = this.preferredDisplayDesktop;
        if (!preferredDesktop)
            return;
        let outOfDesktops = [];
        let notAssignedYet = [];
        let droppedFiles = [];

        // First, add those icons that have saved coordinates
        // and fit in the current desktops
        for (let fileItem of fileList) {
            if (fileItem.savedCoordinates === null) {
                if (fileItem.dropCoordinates !== null)
                    droppedFiles.push(fileItem);
                else
                    notAssignedYet.push(fileItem);
                continue;
            }

            if (fileItem.dropCoordinates !== null)
                fileItem.dropCoordinates = null;

            let [itemX, itemY] = fileItem.savedCoordinates;
            let addedToDesktop = false;

            for (let desktop of this._desktops) {
                if (desktop
                    .coordinatesBelongToThisGridWindow(itemX, itemY) &&
                    desktop.isAvailable()) {
                    addedToDesktop = true;
                    desktop
                    .addFileItemCloseTo(fileItem, itemX, itemY, storeMode);
                    break;
                }
            }

            if (!addedToDesktop)
                outOfDesktops.push(fileItem);
        }

        // Now, assign icons that have landed in changed margins, belong to
        // monitor and the window, however no longer fit on the grid as
        // they overlap margins.

        if (outOfDesktops.length) {
            this._addFilesCloseToAssignedDesktop(
                outOfDesktops,
                storeMode,
                preferredDesktop
            );
        }

        outOfDesktops = [];

        // Now assign those icons that have dropped coordinates
        for (let fileItem of droppedFiles) {
            let [x, y] = fileItem.dropCoordinates;
            storeMode = this.Enums.StoredCoordinates.OVERWRITE;
            let addedToDesktop = false;

            for (let desktop of this._desktops) {
                if (desktop.coordinatesBelongToThisGrid(x, y) &&
                    desktop.isAvailable()) {
                    fileItem.dropCoordinates = null;
                    desktop.addFileItemCloseTo(fileItem, x, y, storeMode);
                    addedToDesktop = true;
                    break;
                }
            }

            if (!addedToDesktop)
                outOfDesktops.push(fileItem);
        }

        // Now, try again assign those icons that had dropped coordinates and
        // did not fit on dropped desktop, to the preferred or closest desktop
        if (outOfDesktops.length) {
            this._addFilesCloseToAssignedDesktop(
                outOfDesktops,
                storeMode,
                preferredDesktop
            );
            outOfDesktops = [];
        }

        // Finally, assign coordinates of preferred desktop to those new icons
        // that still don't have coordinates and place on preferred desktop or
        // the next closest one
        for (let fileItem of notAssignedYet) {
            let x = preferredDesktop.gridGlobalRectangle.x;
            let y = preferredDesktop.gridGlobalRectangle.y;
            storeMode = this.Enums.StoredCoordinates.ASSIGN;

            // try first in the designated desktop
            let assigned = false;
            if (preferredDesktop.coordinatesBelongToThisGrid(x, y) &&
                preferredDesktop.isAvailable()) {
                preferredDesktop.addFileItemCloseTo(fileItem, x, y, storeMode);
                assigned = true;
            }

            if (!assigned)
                outOfDesktops.push(fileItem);
        }

        // if there was no space in the preferred desktop, place on the
        // desktop closest to preferred
        if (outOfDesktops.length) {
            this._addFilesCloseToAssignedDesktop(
                outOfDesktops,
                storeMode,
                preferredDesktop
            );
        }
    }

    _addFilesCloseToAssignedDesktop(fileList, storeMode, preferredDesktop) {
        for (let fileItem of fileList) {
            let desktopX;
            let x = desktopX = preferredDesktop.gridGlobalRectangle.x;
            let desktopY = preferredDesktop.gridGlobalRectangle.y;

            if (fileItem.savedCoordinates) {
                x = fileItem.savedCoordinates[0];
                storeMode = this.Enums.StoredCoordinates.ASSIGN;
            } else if (fileItem.droppedCoordinates) {
                x = fileItem.droppedCoordinates[0];
                storeMode = this.Enums.StoredCoordinates.OVERWRITE;
            }

            // Find the closest desktop to given position, is null
            // if not available
            const newDesktop = this.windowManager.getClosestDesktop(x);

            if (newDesktop) {
                desktopX = newDesktop.gridGlobalRectangle.x;
                desktopY = newDesktop.gridGlobalRectangle.y;

                if (fileItem.droppedCoordinates)
                    fileItem.droppedCoordinates = null;

                newDesktop
                .addFileItemCloseTo(fileItem, desktopX, desktopY, storeMode);
            } else {
                console.log('Not enough space to add icons');
                fileItem.iconCannotBeShown();
            }
        }
    }

    _unstack() {
        if (this.stackInitialCoordinates && this._compositeStackList) {
            this._displayList.forEach(f => {
                f.removeFromGrid();
                if (f.isStackMarker)
                    f.onDestroy();
            });
            this._restoreStackInitialCoordinates();
            this._displayList = this._compositeStackList;
            this._compositeStackList = null;

            if (this.sortingSubMenu && this.sortingMenu) {
                this.sortingSubMenu.prepend_item(
                    this.keepArrangedMenuItem
                );
                this.sortingMenu.prepend_item(this.cleanUpMenuItem);
            }

            if (this.Prefs.keepArranged) {
                this._doSorts();
            } else {
                this._addFilesToDesktop(
                    this._displayList,
                    this.Enums.StoredCoordinates.OVERWRITE
                );
            }
        }
    }

    _saveStackInitialCoordinates() {
        this.stackInitialCoordinates = [];
        for (let fileItem of this._displayList) {
            this.stackInitialCoordinates.push({
                fileName: fileItem.fileName,
                savedCoordinates: fileItem.savedCoordinates,
                _normalCoordinates: fileItem._normalCoordinates,
                _monitorIndex: fileItem._monitorIndex,
            });
        }
    }

    _transformSavedStackInitialCoordinates() {
        if (!this.stackInitialCoordinates &&
            this.stackInitialCoordinates.length)
            return;

        this._recomputeWindowPositions(this.stackInitialCoordinates);

        this.stackInitialCoordinates.forEach(o =>
            (o.savedCoordinates = o.temporarySavedPosition));
    }

    _restoreStackInitialCoordinates() {
        if (this.stackInitialCoordinates &&
            this.stackInitialCoordinates.length) {
            this._compositeStackList.forEach(fileItem => {
                this.stackInitialCoordinates.forEach(savedItem => {
                    if (savedItem.fileName === fileItem.fileName) {
                        fileItem.savedCoordinates = savedItem.savedCoordinates;
                        fileItem._normalCoordinates =
                            savedItem._normalCoordinates;
                        fileItem._monitorIndex = savedItem._monitorIndex;
                    }
                });
            });
        }

        this.stackInitialCoordinates = null;
    }

    _makeStackTopMarkerFolder(type, list) {
        let stackAttribute = type.split('/')[1];
        let fileItem = new StackItem.StackItem(
            this,
            stackAttribute,
            type,
            this.Enums.FileType.STACK_TOP
        );

        list.push(fileItem);
    }

    _sortAllFilesFromGridsByKindStacked(opts = {redisplay: false}) {
        /**
         * Looks through the generated fileItems
         */
        function determineStackTopSizeOrTime() {
            for (let item of otherFiles) {
                if (item.isStackMarker) {
                    for (let unstackitem of stackedFiles) {
                        if (item.attributeContentType ===
                            unstackitem.attributeContentType) {
                            item.size = unstackitem.fileSize;
                            item.time = unstackitem.modifiedTime;
                            break;
                        }
                    }
                }
            }
        }

        /**
         * Sorts fileItems by file size
         *
         * @param {integer} a the first file size
         * @param {integer} b the secondfile size
         */
        function bySize(a, b) {
            return a.fileSize - b.fileSize;
        }

        /**
         * Sorts fileItems by time
         *
         * @param {integer} a the first file timestamp
         * @param {integer} b the second file timestamp
         */
        function byTime(a, b) {
            return a._modifiedTime - b._modifiedTime;
        }

        let specialFiles = [];
        let directoryFiles = [];
        let validDesktopFiles = [];
        let otherFiles = [];
        let stackedFiles = [];
        let newFileList = [];
        let stackTopMarkerFolderList = [];
        let unstackList = this.Prefs.UnstackList;

        if (this._compositeStackList && opts.redisplay) {
            this._displayList.forEach(f => {
                if (f.isStackMarker)
                    f.onDestroy();
            });
            this._displayList = this._compositeStackList;
        }

        this._sortByName(this._displayList);

        for (let fileItem of this._displayList) {
            if (fileItem.isSpecial) {
                specialFiles.push(fileItem);
                continue;
            }

            if (fileItem.isDirectory) {
                directoryFiles.push(fileItem);
                continue;
            }

            if (fileItem._isValidDesktopFile) {
                validDesktopFiles.push(fileItem);
                continue;
            } else {
                let type = fileItem.attributeContentType;
                let stacked = false;

                for (let item of otherFiles) {
                    if (type === item.attributeContentType) {
                        stackedFiles.push(fileItem);
                        stacked = true;
                    }
                }

                if (!stacked) {
                    fileItem.isStackTop = true;
                    otherFiles.push(fileItem);
                }

                continue;
            }
        }

        for (let a of otherFiles) {
            let instack = false;

            for (let c of stackedFiles) {
                if (c.attributeContentType === a.attributeContentType) {
                    instack = true;
                    break;
                }
            }

            if (!instack)
                a.stackUnique = true;

            continue;
        }

        for (let item of otherFiles) {
            if (!item.stackUnique) {
                this._makeStackTopMarkerFolder(
                    item.attributeContentType,
                    stackTopMarkerFolderList
                );
                item.isStackTop = false;
                stackedFiles.push(item);
            }

            if (item.stackUnique)
                stackTopMarkerFolderList.push(item);

            item.updateIcon()
            .catch(e => console.error(e, 'Error loading stackMarker icon'));
        }

        otherFiles = [];
        this._sortByName(specialFiles);
        this._sortByName(directoryFiles);
        this._sortByName(validDesktopFiles);
        this._sortByKindByName(stackedFiles);
        this._sortByKindByName(stackTopMarkerFolderList);
        otherFiles.push(...specialFiles);
        otherFiles.push(...validDesktopFiles);
        otherFiles.push(...directoryFiles);
        otherFiles.push(...stackTopMarkerFolderList);

        switch (this.Prefs.sortOrder) {
        case this.Enums.SortOrder.NAME:
            this._sortByName(otherFiles);
            break;
        case this.Enums.SortOrder.DESCENDINGNAME:
            this._sortByName(otherFiles);
            otherFiles.reverse();
            this._sortByName(stackedFiles);
            stackedFiles.reverse();
            break;
        case this.Enums.SortOrder.MODIFIEDTIME:
            stackedFiles.sort(byTime);
            determineStackTopSizeOrTime();
            otherFiles.sort(byTime);
            break;
        case this.Enums.SortOrder.KIND:
            break;
        case this.Enums.SortOrder.SIZE:
            stackedFiles.sort(bySize);
            determineStackTopSizeOrTime();
            otherFiles.sort(bySize);
            break;
        default:
            break;
        }

        for (let item of otherFiles) {
            newFileList.push(item);
            let itemtype = item.attributeContentType;
            for (let unstackitem of stackedFiles) {
                if (unstackList.includes(unstackitem.attributeContentType) &&
                    (unstackitem.attributeContentType === itemtype))
                    newFileList.push(unstackitem);
            }
        }

        if (this._compositeStackList)
            this._compositeStackList = this._displayList;

        this._displayList = newFileList;
    }

    _sortByName(fileList) {
        /**
         * @param {string} a fileItem filename for A
         * @param {string} b fileItem filename for B
         */
        function byName(a, b) {
            // sort by label name instead of the the fileName or displayName so
            // that the "Home" folder is sorted in the correct order
            // alphabetical sort taking into account accent characters &
            // locale, natural language sort for numbers, ie 10.etc before 2.etc
            // other options for locale are best fit, or by specifying directly
            // in function below for translators
            return a.labelText
                .localeCompare(
                    b.labelText,
                    {
                        sensitivity: 'accent',
                        numeric: 'true',
                        localeMatcher: 'lookup',
                    }
                );
        }
        fileList.sort(byName);
    }

    _sortByKindByName(fileList) {
        /**
         * Sort by Kind, then by name
         *
         * @param {string} a fileItem
         * @param {string} b fileItem
         */
        function byKindByName(a, b) {
            return (
                a.attributeContentType
                    .localeCompare(b.attributeContentType) ||
                a.labelText
                    .localeCompare(
                        b.labelText,
                        {
                            sensitivity: 'accent',
                            numeric: 'true',
                            localeMatcher: 'lookup',
                        }
                    )
            );
        }
        fileList.sort(byKindByName);
    }

    _sortAllFilesFromGridsByName(order) {
        this._sortByName(this._displayList);
        if (order === this.Enums.SortOrder.DESCENDINGNAME)
            this._displayList.reverse();

        this._reassignFilesToDesktop();
    }

    _sortByOriginalPosition() {
        let cornerInversion = this.Prefs.StartCorner;
        if (!cornerInversion[0] && !cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.X < b.X)
                    return -1;
                if (a.X > b.X)
                    return 1;
                if (a.Y < b.Y)
                    return -1;
                if (a.Y > b.Y)
                    return 1;
                return 0;
            });
        }
        if (cornerInversion[0] && cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.X < b.X)
                    return 1;
                if (a.X > b.X)
                    return -1;
                if (a.Y < b.Y)
                    return 1;
                if (a.Y > b.Y)
                    return -1;
                return 0;
            });
        }
        if (cornerInversion[0] && !cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.X < b.X)
                    return 1;
                if (a.X > b.X)
                    return -1;
                if (a.Y < b.Y)
                    return -1;
                if (a.Y > b.Y)
                    return 1;
                return 0;
            });
        }
        if (!cornerInversion[0] && cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.X < b.X)
                    return -1;
                if (a.X > b.X)
                    return 1;
                if (a.Y < b.Y)
                    return 1;
                if (a.Y > b.Y)
                    return -1;
                return 0;
            });
        }
    }

    _sortByCurrentPosition() {
        let cornerInversion = this.Prefs.StartCorner;
        if (!cornerInversion[0] && !cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.x < b.x)
                    return -1;
                if (a.x > b.x)
                    return 1;
                if (a.y < b.y)
                    return -1;
                if (a.y > b.y)
                    return 1;
                return 0;
            });
        }
        if (cornerInversion[0] && cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.x < b.x)
                    return 1;
                if (a.x > b.x)
                    return -1;
                if (a.y < b.y)
                    return 1;
                if (a.y > b.y)
                    return -1;
                return 0;
            });
        }
        if (cornerInversion[0] && !cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.x < b.x)
                    return 1;
                if (a.x > b.x)
                    return -1;
                if (a.y < b.y)
                    return -1;
                if (a.y > b.y)
                    return 1;
                return 0;
            });
        }
        if (!cornerInversion[0] && cornerInversion[1]) {
            this._displayList.sort((a, b) => {
                if (a.x < b.x)
                    return -1;
                if (a.x > b.x)
                    return 1;
                if (a.y < b.y)
                    return 1;
                if (a.y > b.y)
                    return -1;
                return 0;
            });
        }
    }

    _reassignFilesToDesktop() {
        if (!this.Prefs.sortSpecialFolders) {
            this._reassignFilesToDesktopPreserveSpecialFiles();
            return;
        }

        for (let fileItem of this._displayList) {
            fileItem.temporarySavedPosition = null;
            fileItem.dropCoordinates = null;
        }

        this._addFilesToDesktop(
            this._displayList,
            this.Enums.StoredCoordinates.ASSIGN
        );
    }

    _reassignFilesToDesktopPreserveSpecialFiles() {
        let specialFiles = [];
        let otherFiles = [];
        let newFileList = [];

        for (let fileItem of this._displayList) {
            if (fileItem._isSpecial) {
                specialFiles.push(fileItem);
                continue;
            }

            if (!fileItem._isSpecial) {
                otherFiles.push(fileItem);
                fileItem.temporarySavedPosition = null;
                fileItem.dropCoordinates = null;
                continue;
            }
        }

        newFileList.push(...specialFiles);
        newFileList.push(...otherFiles);

        if (this._displayList.length === newFileList.length)
            this._displayList = newFileList;

        this._addFilesToDesktop(
            this._displayList,
            this.Enums.StoredCoordinates.PRESERVE
        );
    }

    // Desktop Manager Main methods
    // ********************************************************************** */


    findFiles(text) {
        if (this._findFileWindow) {
            this._findFileWindow.present();
            return;
        }

        const activeWindow = this.getDialogParentWindow();
        this._findFileWindow = new Gtk.Dialog({
            use_header_bar: true,
            resizable: false,
        });
        this._findFileButton =
            this._findFileWindow.add_button(_('OK'), Gtk.ResponseType.OK);
        this._findFileButton.sensitive = false;
        this._findFileWindow.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
        this._findFileWindow.set_modal(true);
        this._findFileWindow.set_title(_('Find Files on Desktop'));
        const modal = true;
        this.DesktopIconsUtil
        .windowHidePagerTaskbarModal(this._findFileWindow, modal);
        this._findFileWindow.set_transient_for(activeWindow);
        let contentArea = this._findFileWindow.get_content_area();
        this._findFileTextArea = new Gtk.Entry();
        this._findFileTextArea.set_margin_top(5);
        this._findFileTextArea.set_margin_bottom(5);
        this._findFileTextArea.set_margin_start(5);
        this._findFileTextArea.set_margin_end(5);
        contentArea.append(this._findFileTextArea);
        contentArea.set_homogeneous(true);
        contentArea.set_baseline_position(Gtk.BaselinePosition.CENTER);

        this._findFileTextArea.connect('activate', () => {
            if (this._findFileButton.sensitive)
                this._findFileWindow.response(Gtk.ResponseType.OK);
        });

        this._findFileTextArea.connect('changed', () => {
            let context = this._findFileTextArea.get_style_context();

            if (this._scanForFiles(this._findFileTextArea.text, true)) {
                this._findFileButton.sensitive = true;

                if (context.has_class('not-found'))
                    context.remove_class('not-found');
            } else {
                this._findFileButton.sensitive = false;
                this._findFileTextArea.error_bell();

                if (!context.has_class('not-found'))
                    context.add_class('not-found');
            }

            this.searchEventTime = GLib.get_monotonic_time();
        });

        this._findFileTextArea.grab_focus_without_selecting();

        if (text) {
            this._findFileTextArea.set_text(text);
            this._findFileTextArea.set_position(text.length);
        } else {
            this._scanForFiles(null);
        }

        this._findFileWindow.show();

        this.mainApp.activate_action('textEntryAccelsTurnOff', null);

        this._findFileWindow.connect('close', () => {
            this._findFileWindow.response(Gtk.ResponseType.CANCEL);
        });

        this._findFileWindow.connect('response', (actor, retval) => {
            if (retval === Gtk.ResponseType.CANCEL)
                this.unselectAll();

            this.mainApp.activate_action('textEntryAccelsTurnOn', null);
            this._displayList.forEach(f => (f.opacity = 1));
            this._findFileWindow.destroy();
            this._findFileWindow = null;
        });
    }

    _scanForFiles(text, setselected) {
        let found = [];
        if (text && (text !== '')) {
            const lowerCaseText = text.toLowerCase();
            found =
                this._displayList.filter(
                    f => {
                        const lowerCaseFilename = f.fileName.toLowerCase();
                        const lowerCaseLabel = f.labelText.toLowerCase();

                        return (
                            lowerCaseFilename.includes(lowerCaseText) ||
                            lowerCaseLabel.includes(lowerCaseText)
                        );
                    }
                );
        }

        if (found.length !== 0) {
            if (setselected) {
                this.unselectAll();
                const notfound = this._displayList.filter(
                    f => !found.includes(f)
                );
                found.forEach(f => {
                    f.setSelected();
                    f.opacity = 1;
                });
                notfound.forEach(f => (f.opacity = 0.2));
            }
            return true;
        } else {
            this.unselectAll();
            return false;
        }
    }

    async sortAllFilesFromGridsByPosition() {
        if (this.Prefs.keepArranged)
            return;

        await this.runSerializedDesktopMutation(
            this._sortAllFilesFromGridsByPosition.bind(this)
        );
    }

    _sortAllFilesFromGridsByPosition() {
        this._displayList.forEach(f => f.removeFromGrid({
            callOnDestroy: false,
        }));
        this._sortByCurrentPosition();
        this._reassignFilesToDesktop();
    }

    _sortAllFilesFromGridsByModifiedTime() {
        /**
         * @param {integer} a fileItem file modified time
         * @param {integer} b fileItem file modified time
         */
        function byTime(a, b) {
            return a._modifiedTime - b._modifiedTime;
        }
        this._displayList.sort(byTime);
        this._reassignFilesToDesktop();
    }

    _sortAllFilesFromGridsBySize() {
        /**
         * @param {integer} a fileItem fileSize
         * @param {integer} b fileItem fileSize
         */
        function bySize(a, b) {
            return a.fileSize - b.fileSize;
        }
        this._displayList.sort(bySize);
        this._reassignFilesToDesktop();
    }

    _sortAllFilesFromGridsByKind() {
        let specialFiles = [];
        let directoryFiles = [];
        let validDesktopFiles = [];
        let otherFiles = [];
        let newFileList = [];
        for (let fileItem of this._displayList) {
            if (fileItem._isSpecial) {
                specialFiles.push(fileItem);
                continue;
            }
            if (fileItem._isDirectory) {
                directoryFiles.push(fileItem);
                continue;
            }
            if (fileItem._isValidDesktopFile) {
                validDesktopFiles.push(fileItem);
                continue;
            } else {
                otherFiles.push(fileItem);
                continue;
            }
        }
        this._sortByName(specialFiles);
        this._sortByName(directoryFiles);
        this._sortByName(validDesktopFiles);
        this._sortByKindByName(otherFiles);
        newFileList.push(...specialFiles);
        newFileList.push(...validDesktopFiles);
        newFileList.push(...directoryFiles);
        newFileList.push(...otherFiles);
        if (this._displayList.length === newFileList.length)
            this._displayList = newFileList;

        this._reassignFilesToDesktop();
    }

    _doSorts(opts = {redisplay: false}) {
        if (opts.redisplay)
            this._displayList.forEach(f => f.removeFromGrid());

        switch (this.Prefs.sortOrder) {
        case this.Enums.SortOrder.NAME:
            this._sortAllFilesFromGridsByName();
            break;
        case this.Enums.SortOrder.DESCENDINGNAME:
            this._sortAllFilesFromGridsByName(
                this.Enums.SortOrder.DESCENDINGNAME);
            break;
        case this.Enums.SortOrder.MODIFIEDTIME:
            this._sortAllFilesFromGridsByModifiedTime();
            break;
        case this.Enums.SortOrder.KIND:
            this._sortAllFilesFromGridsByKind();
            break;
        case this.Enums.SortOrder.SIZE:
            this._sortAllFilesFromGridsBySize();
            break;
        default:
            this._addFilesToDesktop(
                this._displayList,
                this.Enums.StoredCoordinates.PRESERVE
            );
            break;
        }
    }

    _doStacks(opts = {redisplay: false}) {
        if (opts.redisplay) {
            for (let fileItem of this._displayList)
                fileItem.removeFromGrid();
        }

        if (!this.stackInitialCoordinates && !this._compositeStackList) {
            this._compositeStackList = [];
            this._saveStackInitialCoordinates();
            if (this.sortingSubMenu && this.sortingMenu) {
                this.sortingSubMenu.remove(0);
                this.sortingMenu.remove(0);
            }
            opts.redisplay = false;
        }

        if ((opts.monitorschanged ||
            opts.initialRead) &&
            this.stackInitialCoordinates)
            this._transformSavedStackInitialCoordinates();


        this._sortAllFilesFromGridsByKindStacked(opts);

        this._reassignFilesToDesktop();
    }

    unselectAll() {
        this._displayList.forEach(f => {
            f.unsetSelected();
            f.keyboardUnSelected();
            f.opacity = 1;
        });
        this.desktopActions.lastAnchorSelected = null;
        this.fileItemMenu.activeFileItem = null;
    }

    getCurrentSelection() {
        const selectedList = this._displayList.filter(f => f.isSelected);

        if (selectedList.length)
            return selectedList;

        return null;
    }

    getCurrentSelectionAsUri() {
        return this.getCurrentSelection()?.map(f => f.uri);
    }

    getNumberOfSelectedItems() {
        const count = this.getCurrentSelection();

        if (count)
            return count.length;

        return 0;
    }

    checkIfSpecialFilesAreSelected() {
        for (let item of this._displayList) {
            if (item.isSelected && item.isSpecial)
                return true;
        }

        return false;
    }

    checkIfDirectoryIsSelected() {
        for (let item of this._displayList) {
            if (item.isSelected && item.isDirectory)
                return true;
        }

        return false;
    }

    async doRename(fileItem, allowReturnOnSameName = false) {
        const selection = this.getCurrentSelection();

        if (!(selection && (selection.length === 1)))
            return;

        if (fileItem === null) {
            fileItem = selection[0];
            allowReturnOnSameName = false;
        }

        if (!fileItem.canRename)
            return;

        if (!this._renameWindow) {
            this.mainApp.activate_action('textEntryAccelsTurnOff', null);

            if (!this.newItemDoRename)
                this.newItemDoRename = new Set();

            this.newItemDoRename.add(fileItem.fileName);

            if (this.desktopMenuManager.popupmenu) {
                await this.desktopMenuManager.menuclosed()
                    .catch(e => logError(e));
            }

            if (this.fileItemMenu.popupmenu)
                await this.fileItemMenu.menuclosed().catch(e => logError(e));

            this._renameWindow = new AskRenamePopup.AskRenamePopup(
                fileItem,
                allowReturnOnSameName,
                () => {
                    const parentWindow = this.getDialogParentWindow();
                    if (parentWindow)
                        parentWindow.grab_focus();
                    this.mainApp.activate_action('textEntryAccelsTurnOn', null);
                    if (this.newItemDoRename)
                        this.newItemDoRename.delete(fileItem.fileName);
                    this._renameWindow = null;
                },
                this.dragManager.setPendingDropCoordinates.bind(this),
                {
                    FileUtils: this.FileUtils,
                    DesktopIconsUtil: this.DesktopIconsUtil,
                    DBusUtils: this.DBusUtils,
                }
            );
        }
    }

    async doNewFolder(
        position = null,
        suggestedName = null,
        opts = {rename: true}
    ) {
        this.unselectAll();

        if (!position)
            position = [this._clickX, this._clickY];


        const baseName = suggestedName ? suggestedName : _('New Folder');
        let newName = this.desktopMonitor.getDesktopUniqueFileName(baseName);

        if (newName) {
            const dir = this._desktopDir.get_child(newName);

            try {
                await dir.make_directory_async(GLib.PRIORITY_DEFAULT, null);

                const info = new Gio.FileInfo();
                info.set_attribute_string(
                    'metadata::nautilus-drop-position',
                    `${position.join(',')}`
                );
                info.set_attribute_string(
                    'metadata::desktop-icon-position',
                    ''
                );
                info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, 0o700);

                try {
                    await dir.set_attributes_async(info,
                        Gio.FileQueryInfoFlags.NONE,
                        GLib.PRIORITY_LOW,
                        null);
                } catch (e) {
                    console.error(
                        e, `Failed to set attributes to ${dir.get_path()}`);
                }
            } catch (e) {
                if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
                    this._performSanityChecks();
                else
                    console.error(e, `Failed to create folder ${e.message}`);
                const header = _('Folder Creation Failed');
                const text = _('Could not create folder');
                this.dbusManager.doNotify(header, text);
                if (position || suggestedName)
                    return null;

                return null;
            }

            if (opts.rename) {
                if (!this.newItemDoRename)
                    this.newItemDoRename = new Set();

                this.newItemDoRename.add(newName);
            }

            if (position || suggestedName)
                return dir.get_uri();
        }

        return null;
    }

    async redrawDesktop() {
        // fileList is not changed, we just need to render the desktop again
        // with changes in icon color, emblem, appearance, theme change etc.
        const opts = {initialRead: false, redisplay: true};
        const fileList = [...this.desktopMonitor.fileList];

        await this._runSerializedDrawDesktop(fileList, opts).catch(e => {
            console.error(
                `Error while redrawing desktop: ${e.message}\n${e.stack}`
            );
        });
    }

    async reLoadDesktop() {
        await this.desktopMonitor.reLoadFileList();
    }

    async refreshDesktop() {
        // fileList is changed, we need to render the desktop again
        // with latest fileList from the desktopMonitor. The position of the
        // icons is also recomputed from the normalized coordinates.
        const opts = {initialRead: true};
        const fileList = [...this.desktopMonitor.fileList];

        await this._runSerializedDrawDesktop(fileList, opts).catch(e => {
            console.error(`Error while refreshing desktop: ${e.message}`);
        });
    }

    async _reFrameDesktop(opts) {
        // fileList is not changed, grids changed, monitor added, removed,
        // monitor geometry, zoom, or index changed.
        // We need to recompute the position of the icons
        // from the normalized coordinates and redraw the desktop and reassign
        // the icons to the correct grid and monitors
        const fileList = [...this.desktopMonitor.fileList];

        await this._drawDesktop(fileList, opts).catch(e => {
            console.error(`Error while reframing desktop: ${e.message}`);
        });
    }

    onMutterSettingsChanged() {
        this.windowManager.requestGeometryUpdate();
    }

    async onGtkSettingsChanged() {
        // No serializer here. The actual desktop mutation is serialized later
        // when the reload reaches refreshDesktop().
        await this.reLoadDesktop();
        this.desktopMenuManager.updateTemplates();
    }

    async onKeepArrangedChanged() {
        if (this.Prefs.keepArranged) {
            await this.runSerializedDesktopMutation(
                this._doSorts.bind(this, {redisplay: true})
            );
        }
    }

    async onUnstackedTypesChanged() {
        if (this.Prefs.keepStacked) {
            await this.runSerializedDesktopMutation(
                this._doStacks.bind(this, {redisplay: true})
            );
        }
    }

    async onkeepStackedChanged() {
        await this.runSerializedDesktopMutation(
            this._applyKeepStackedChange.bind(this)
        );
    }

    async onSortOrderChanged() {
        await this.runSerializedDesktopMutation(
            this._applySortOrderChange.bind(this)
        );
    }

    async onIconSizeChanged() {
        await this.runSerializedDesktopMutation(
            this._applyIconSizeChange.bind(this)
        );

        await this.reLoadDesktop().catch(e => {
            console.log('Exception while reloading desktop after icon ' +
                `size change: ${e.message}\n${e.stack}`);
        });
    }

    _applyIconSizeChange() {
        this._displayList.forEach(x => x.removeFromGrid());
        for (let desktop of this._desktops)
            desktop.resizeGrid();
    }

    _applySortOrderChange() {
        if (this.Prefs.keepStacked)
            this._doStacks({redisplay: true});
        else
            this._doSorts({redisplay: true});
    }

    _applyKeepStackedChange() {
        if (this.Prefs.keepStacked)
            this._doStacks({redisplay: true});
        else
            this._unstack();
    }

    onDarkModeChanged() {
        this.widgetManager.onThemeChanged();
    }

    onAnimationChanged() {
        this.widgetManager.onAnimationChanged();
    }

    // Getters and Setters

    get _desktopDir() {
        return this.desktopMonitor.desktopDir;
    }

    get fractionalScaling() {
        return this.Prefs.fractionalScaling;
    }

    set fractionalScaling(boolean) {
        this.Prefs.fractionalScaling = boolean;
    }

    get _desktops() {
        return this.windowManager.desktops;
    }

    get _primaryMonitorIndex() {
        return this.windowManager.primaryMonitorIndex;
    }

    get _priorPrimaryMonitorIndex() {
        return this.windowManager.priorPrimaryMonitorIndex;
    }

    get preferredDisplayDesktop() {
        return this.windowManager.preferredDisplayDesktop;
    }

    get templatesMonitor() {
        return this.desktopActions.templatesMonitor;
    }

    get currentWorkingList() {
        let currentCompleteList;
        if (this._compositeStackList && (this._compositeStackList.length > 0))
            currentCompleteList = this._compositeStackList;
        else
            currentCompleteList = this._displayList;

        return currentCompleteList;
    }

    get activeFileItem() {
        return this.fileItemMenu.activeFileItem;
    }

    set activeFileItem(fileItem) {
        this.fileItemMenu.activeFileItem = fileItem;
    }

    get pendingDropFiles() {
        return this.dragManager.pendingDropFiles;
    }

    set pendingDropFiles(object) {
        this.dragManager.pendingDropFiles = object;
    }

    get pendingSelfCopyFiles() {
        return this.dragManager.pendingSelfCopyFiles;
    }

    set pendingSelfCopyFiles(object) {
        this.dragManager.pendingSelfCopyFiles = object;
    }

    get writableByOthers() {
        return this.desktopMonitor._writableByOthers;
    }

    get modifierMode() {
        return {
            state: this._lastModifierState,
            ctrl: (this._lastModifierState & Gdk.ModifierType.CONTROL_MASK) !== 0,
            shift: (this._lastModifierState & Gdk.ModifierType.SHIFT_MASK) !== 0,
            alt: (this._lastModifierState & Gdk.ModifierType.ALT_MASK) !== 0,
        };
    }
};
