
/* DING: Desktop Icons New Generation for GNOME Shell
 *
 * Gtk4 Port Copyright (C) 2022- 2026 Sundeep Mediratta (smedius@gmail.com)
 * Copyright (C) 2021 Sundeep Mediratta (smedius@gmail.com)
 * Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
 * Based on code original (C) Carlos Soriano
 * SwitcherooControl code based on code original from Marsch84
 *
 * 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 {GObject, Gtk, Gdk, Gio, Graphene, Gsk, GLib, Pango, GdkPixbuf}
    from '../dependencies/gi.js';

import {_} from '../dependencies/gettext.js';

export {DesktopIconItem};

const PIXBUF_CONTENT_TYPES = new Set();

const DesktopIconPicture = GObject.registerClass(
class DesktopIconPicture extends Gtk.Picture {
    vfunc_snapshot(snapshot) {
        if (this.get_width() <= 0 || this.get_height() <= 0)
            return;

        super.vfunc_snapshot(snapshot);

        if (this._snapshotCallback)
            this._snapshotCallback();
    }
});

GdkPixbuf.Pixbuf
.get_formats()
.forEach(f => PIXBUF_CONTENT_TYPES.add(...f.get_mime_types()));

const DesktopIconItem = class {
    constructor(desktopManager, fileTypeEnum) {
        this._desktopManager = desktopManager;
        this._dragManager = desktopManager.dragManager;
        this.DesktopIconsUtil = desktopManager.DesktopIconsUtil;
        this.FileUtils = desktopManager.FileUtils;
        this.Prefs = desktopManager.Prefs;
        this.Enums = desktopManager.Enums;
        this.ThumbnailLoader = desktopManager.ThumbnailLoader;
        this._fileTypeEnum = fileTypeEnum;
        this._grid = null;
        this._column = null;
        this._row = null;
        this._lastClickTime = 0;
        this._lastClickButton = 0;
        this._clickCount = 0;
        this._isSelected = false;
        this._isSpecial = false;
        this._keyboardSelected = false;
        this._savedCoordinates = null;
        this._dropCoordinates = null;
        this._normalCoordinates = null;
        this._monitorIndex = null;
        this._destroying = false;
        this._updateIconCancellable = null;
        this._pendingIconUpdate = false;
        this._iconStateFlag = 0;
        this._labelStateFlag = 0;
        this._iconContainerEventController = null;
        this._iconContainerEventControllerEnterId = 0;
        this._iconContainerEventControllerLeaveId = 0;
        this.thumbnail = null;
        this.thumbnailFile = null;
        // Create the placement promise up front so snapshot and fallback
        // paths can resolve the same instance later.
        this.iconPlaced = new Promise(resolve => {
            this.iconPlacedPromiseResolve = resolve;
        });
    }

    /** *********************
     * Destroyers *
     ***********************/

    removeFromGrid(opts = {callOnDestroy: false}) {
        if (this._grid) {
            this._grid.removeItem(this);
            this._grid = null;
        }

        if (opts.callOnDestroy)
            this.onDestroy();
    }

    _destroy() {
        this._destroying = true;

        /* Icons update */
        if (this._updateIconCancellable)
            this._updateIconCancellable.cancel();
        this._updateIconCancellable = null;

        if (this._icon)
            this._icon.set_paintable(null);

        this.thumbnail = null;
        this.thumbnailFile = null;

        if (this._icon)
            this._icon._snapshotCallback = null;

        if (this._iconStateFlag) {
            this._iconContainer.disconnect(this._iconStateFlag);
            this._iconStateFlag = 0;
        }

        if (this._labelStateFlag) {
            this._labelContainer.disconnect(this._labelStateFlag);
            this._labelStateFlag = 0;
        }

        if (this._iconContainerEventController) {
            if (this._iconContainerEventControllerEnterId) {
                this._iconContainerEventController.disconnect(
                    this._iconContainerEventControllerEnterId
                );
                this._iconContainerEventControllerEnterId = 0;
            }

            if (this._iconContainerEventControllerLeaveId) {
                this._iconContainerEventController.disconnect(
                    this._iconContainerEventControllerLeaveId
                );
                this._iconContainerEventControllerLeaveId = 0;
            }

            if (this._icon)
                this._icon.remove_controller(this._iconContainerEventController);

            this._iconContainerEventController = null;
        }

        this._destroyToolTip();

        this._icon = null;
        this._iconContainer = null;
        this._label = null;
        this._labelContainer = null;
        this.container = null;
    }

    onDestroy() {
        this._resolveIconPlaced();
        this._destroy();
    }

    _isCancellationError(error) {
        return Boolean(
            error?.matches?.(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)
        );
    }

    /** *********************
     * Creators *
     ***********************/

    _createIconActor() {
        if (this.container)
            return;

        if (this.width == null || this.height == null)
            return;

        this.container =
            new Gtk.Box({
                orientation: Gtk.Orientation.VERTICAL,
                halign: Gtk.Align.CENTER,
                focusable: true,
                can_focus: true,
                accessible_role: Gtk.AccessibleRole.LABEL,
            });
        this.container.add_css_class('desktop-icon-container');
        this.container.set_size_request(this.width, this.height);
        this._icon = new DesktopIconPicture({
            can_shrink: false,
            keep_aspect_ratio: true,
            halign: Gtk.Align.CENTER,
        });
        this._icon._snapshotCallback = this._doIconSizeAllocated.bind(this);

        this._iconContainer = new Gtk.Box({
            orientation: Gtk.Orientation.HORIZONTAL,
            halign: Gtk.Align.CENTER,
            hexpand: false,
            baseline_position: Gtk.BaselinePosition.CENTER,
        });

        this._iconContainer.append(this._icon);

        this._label = new Gtk.Label({
            halign: Gtk.Align.CENTER,
            natural_wrap_mode: Gtk.NaturalWrapMode.WORD,
            ellipsize: Pango.EllipsizeMode.END,
            wrap: true,
            wrap_mode: Pango.WrapMode.WORD_CHAR,
            yalign: 0.0,
            xalign: 0.0,
            justify: Gtk.Justification.CENTER,
            lines: 2,
        });

        this._labelContainer = new Gtk.Box({
            orientation: Gtk.Orientation.VERTICAL,
            name: 'file-item',
        });

        this.iconRectangle = new Gdk.Rectangle();
        this.iconLocalWindowRectangle = new Gdk.Rectangle();
        this.labelRectangle = new Gdk.Rectangle();

        this._iconContainerEventController = new Gtk.EventControllerMotion({
            propagation_phase: Gtk.PropagationPhase.CAPTURE,
        });

        this._icon.add_controller(this._iconContainerEventController);

        this._iconContainerEventControllerEnterId =
            this._iconContainerEventController.connect('enter', () => {
                this._showToolTip();
            });

        this._iconContainerEventControllerLeaveId =
            this._iconContainerEventController.connect('leave', () => {
                this._destroyToolTip();
            });

        // This controls how the icons look - Rectangular or skinny trapezoid

        if (!this.Prefs.freePositionIcons) {
            this._labelContainer.append(this._iconContainer);
            this._label.add_css_class('file-label-vertical');
            this._labelContainer.append(this._label);
            this.container.append(this._labelContainer);
        } else {
            this._labelContainer.append(this._label);
            this.container.append(this._iconContainer);
            this.container.append(this._labelContainer);

            this._iconStateFlag =
                this._iconContainer.connect('state-flags-changed', () => {
                    if (this._checkHasHoveredPointer(this._iconContainer)) {
                        this._onEnter();
                        this._labelContainer.add_css_class('mimic-hovered');
                    } else {
                        this._onLeave();
                        this._labelContainer.remove_css_class('mimic-hovered');
                    }
                });

            this._iconContainer.set_name('file-item');
        }

        this._labelStateFlag =
            this._labelContainer.connect('state-flags-changed', () => {
                if (this._checkHasHoveredPointer(this._labelContainer)) {
                    this._onEnter();

                    if (this.Prefs.freePositionIcons)
                        this._iconContainer.add_css_class('mimic-hovered');
                } else {
                    this._onLeave();

                    if (this.Prefs.freePositionIcons)
                        this._iconContainer.remove_css_class('mimic-hovered');
                }
            });

        this.container.show();

        if (this._isSelected)
            this.setHighLighted();

        if (this._keyboardSelected)
            this.keyboardSelected();

        if (this._pendingIconUpdate) {
            this._pendingIconUpdate = false;
            void this.updateIcon();
        }

        this._onIconActorCreated();
    }

    _onIconActorCreated() {
    }

    _doIconSizeAllocated() {
        if (this._destroying || !this._grid)
            return;
        this._calculateIconRectangle();
        this._calculateLabelRectangle();
        this._resolveIconPlaced();
    }

    _resolveIconPlaced() {
        // Gtk snapshot is run, icon is painted for the first time.
        if (!this.iconPlacedPromiseResolve)
            return;

        this.iconPlacedPromiseResolve(true);
        this.iconPlacedPromiseResolve = null;
    }

    iconCannotBeShown() {
        // Resolve the placement promise even when the icon never gets a snapshot.
        // Resolve is done by desktopManager when it determines icon cannot be shown.
        this._resolveIconPlaced();
    }

    _calculateIconRectangle() {
        this.iconwidth = this._iconContainer.get_allocated_width();
        this.iconheight = this._iconContainer.get_allocated_height();
        const [x, y] =
            this._grid.coordinatesLocalToGlobal(0, 0, this._iconContainer);
        this.iconRectangle.x = x;
        this.iconRectangle.y = y;
        this.iconRectangle.width = this.iconwidth;
        this.iconRectangle.height = this.iconheight;
        this._calculateLocalWindowRectangle();
    }

    _calculateLocalWindowRectangle() {
        const [x, y] =
            this._grid.coordinatesLocalToWindow(0, 0, this._iconContainer);
        this.iconLocalWindowRectangle.x = x;
        this.iconLocalWindowRectangle.y = y;
        this.iconLocalWindowRectangle.width = this.iconwidth;
        this.iconLocalWindowRectangle.height = this.iconheight;
    }

    _calculateLabelRectangle() {
        this.labelwidth = this._labelContainer.get_allocated_width();
        this.labelheight = this._labelContainer.get_allocated_height();
        const [x, y] =
            this._grid.coordinatesLocalToGlobal(0, 0, this._labelContainer);
        this.labelRectangle.x = x;
        this.labelRectangle.y = y;
        this.labelRectangle.width = this.labelwidth;
        this.labelRectangle.height = this.labelheight;
    }

    setCoordinates(x, y, width, height, margin, grid) {
        this._x1 = x;
        this._y1 = y;
        this.width = width;
        this.height = height;
        this._grid = grid;
        this._createIconActor();
        this._label.margin_start = margin;
        this._label.margin_end = margin;
        this._label.margin_bottom = margin;
        this._iconContainer.margin_top = margin;
    }

    getCoordinates() {
        this._x2 = this._x1 + this.container.get_allocated_width() - 1;
        this._y2 = this._y1 + this.container.get_allocated_height() - 1;

        return [this._x1, this._y1, this._x2, this._y2, this._grid];
    }

    writeSavedCoordinates(pos) {
        this._parseSavedCoordinates(pos);
    }

    writeDropCoordinates(pos) {
        this._parseDropCoordinates(pos);
    }

    readSavedCoordinates() {
        this._parseSavedCoordinates([]);
    }

    readDropCoordinates() {
        this._parseDropCoordinates([]);
    }

    _parseDropCoordinates(pos) {
        if (!Array.isArray(pos) || pos.some(e => isNaN(e))) {
            this._dropCoordinates = null;
            return;
        }
        pos = pos.map(e => Number(e));
        if (pos?.length === 2)
            this._dropCoordinates = pos;
        else
            this._dropCoordinates = null;
    }

    _parseSavedCoordinates(pos) {
        if (!Array.isArray(pos) || pos.some(e => isNaN(e))) {
            this._savedCoordinates = null;
            this._normalCoordinates = null;
            this._monitorIndex = null;
            return;
        }
        pos = pos.map(e => Number(e));
        if (pos?.length === 2) {
            this._savedCoordinates = pos;
            this._normalCoordinates = null;
            this._monitorIndex = null;
        } else if (pos?.length === 5) {
            this._savedCoordinates = pos.slice(0, 2);
            this._normalCoordinates = pos.slice(2, 4);
            this._monitorIndex = pos[4];
        } else {
            this._savedCoordinates = null;
            this._normalCoordinates = null;
            this._monitorIndex = null;
        }
    }

    _setLabelName(text) {
        this._currentFileName = text;

        if (!this._label)
            return;

        this._label.label = text;
    }

    /** *********************
     * Button Clicks *
     ***********************/

    _checkHasHoveredPointer(widget) {
        let stateFlags = widget.get_state_flags();
        if ((stateFlags & Gtk.StateFlags.PRELIGHT) === Gtk.StateFlags.PRELIGHT)
            return true;
        else
            return false;
    }

    _onPressButton(
        actor,
        nPress,
        X, Y,
        x, y,
        shiftPressed,
        controlPressed,
        timestamp = Gdk.CURRENT_TIME
    ) {
        const button = actor.get_current_button();

        this._buttonPressInitialX = x - this._x1;
        this._buttonPressInitialY = y - this._y1;

        this._desktopManager.activeFileItem = this;

        if (button === 3) {
            this._doButtonThreePressed(
                button,
                nPress,
                X, Y,
                x, y,
                shiftPressed,
                controlPressed,
                timestamp
            );
        } else if (button === 1) {
            this._doButtonOnePressed(
                button,
                nPress,
                X, Y,
                x, y,
                shiftPressed,
                controlPressed
            );
        }
    }

    _onLongPressButton(
        _actor, _X, _Y, _x, _y, _button, _shiftPressed, _controlPressed) {
        // Handle long press events here if needed
    }

    _onReleaseButton(actor, X, Y, x, y, shiftPressed, controlPressed) {
        let button = actor.get_current_button();
        this._grid?.makeTopLayerOnGrid(this);

        if (button === 1) {
            this._doButtonOneReleased(
                button,
                X, Y,
                x, y,
                shiftPressed,
                controlPressed
            );
        }
    }

    _doButtonThreePressed(
        button, nPress, X, Y, x, y, shiftPressed, controlPressed, timestamp
    ) {
        if (!this._isSelected)
            this._dragManager.selected(this, this.Enums.Selection.RIGHT_BUTTON);

        this._destroyToolTip();

        this._desktopManager.fileItemMenu.showMenu(
            this,
            button,
            X, Y,
            x, y,
            shiftPressed,
            controlPressed,
            timestamp
        );
    }

    _doButtonOnePressed(
        button, nPress, X, Y, x, y, shiftPressed, controlPressed
    ) {
        if (nPress === 1) {
            if (shiftPressed || controlPressed) {
                this._dragManager.selected(
                    this,
                    this.Enums.Selection.WITH_SHIFT
                );
            } else {
                this._dragManager.selected(
                    this,
                    this.Enums.Selection.ALONE
                );
            }
        }
    }

    _doButtonOneReleased(
        // eslint-disable-next-line no-unused-vars
        button, nPressX, Y, x, y, shiftPressed, controlPressed) {
    }

    /** *********************
     * Drag and Drop *
     ***********************/

    _onEnter() {
        if (!this._grid)
            return true;

        if (this.Prefs.CLICK_POLICY_SINGLE) {
            let window = this._grid._window;

            if (window)
                window.set_cursor(Gdk.Cursor.new_from_name('hand', null));
        }

        return false;
    }

    _onLeave() {
        if (!this._grid)
            return true;

        if (this.Prefs.CLICK_POLICY_SINGLE) {
            let window = this._grid._window;

            if (window)
                window.set_cursor(Gdk.Cursor.new_from_name('default', null));
        }

        return false;
    }

    _showToolTip() {
        if (this._toolTipTimer)
            return;

        this._toolTipTimer =
            GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                this.Enums.TOOLTIP_HOVER_TIMEOUT,
                () => {
                    this._desktopManager.fileItemMenu.showToolTip(this);
                    this._toolTipTimer = 0;
                    return GLib.SOURCE_REMOVE;
                }
            );
    }

    _destroyToolTip() {
        if (this._toolTipTimer) {
            GLib.Source.remove(this._toolTipTimer);
            this._toolTipTimer = 0;
        }

        this._desktopManager.fileItemMenu.hideToolTip(this);
    }

    _hasToRouteDragToGrid() {
        if (this._grid)
            return true;
        else
            return false;
    }

    _updateDragStatus(context, time) {
        if (this.DesktopIconsUtil
            .getModifiersInDnD(context, Gdk.ModifierType.CONTROL_MASK)
        )
            Gdk.drag_status(context, Gdk.DragAction.COPY, time);
        else
            Gdk.drag_status(context, Gdk.DragAction.MOVE, time);
    }

    setHighLighted() {
        if (!this.container || !this._iconContainer || !this._labelContainer)
            return;

        if (!this._iconContainer
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this._iconContainer.add_css_class('desktop-icons-selected');

        if (!this._labelContainer
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this._labelContainer.add_css_class('desktop-icons-selected');

        if (!this.container
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this.container.add_css_class('desktop-icons-selected');
    }

    setUnHighLighted() {
        if (!this.container || !this._iconContainer || !this._labelContainer)
            return;

        if (this._iconContainer
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this._iconContainer.remove_css_class('desktop-icons-selected');

        if (this._labelContainer
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this._labelContainer.remove_css_class('desktop-icons-selected');

        if (this.container
            .get_css_classes()
            .includes('desktop-icons-selected')
        )
            this.container.remove_css_class('desktop-icons-selected');
    }

    highLightDropTarget() {
        if (this._hasToRouteDragToGrid()) {
            this._grid.receiveMotion(this._x1, this._y1, true);

            return;
        }

        this.setHighLighted();
        this._grid?.highLightGridAt(this._x1, this._y1);
    }

    unHighLightDropTarget() {
        if (this._hasToRouteDragToGrid()) {
            this._grid?.receiveLeave();
            return;
        }
        this.setUnHighLighted();
        this._grid?.unHighLightGrids();
    }

    setSelected() {
        this._isSelected = true;
        this._setSelectedStatus();
    }

    unsetSelected() {
        this._isSelected = false;
        this._setSelectedStatus();
    }

    toggleSelected() {
        this._isSelected = !this._isSelected;
        this._setSelectedStatus();
    }

    _setSelectedStatus() {
        if (!this.container || !this._iconContainer || !this._labelContainer)
            return;

        if (this._isSelected) {
            this.setHighLighted();
            this.container.grab_focus();
        }
        if (!this._isSelected)
            this.setUnHighLighted();
    }

    keyboardSelected() {
        this._keyboardSelected = true;

        if (!this.container || !this._iconContainer || !this._labelContainer)
            return;

        if (!this._iconContainer.get_css_classes().includes('mimic-hovered')) {
            this._iconContainer.add_css_class('mimic-hovered');
            this._labelContainer.add_css_class('mimic-hovered');
        }

        if (!this.container.get_css_classes().includes('keyboard-selected'))
            this.container.add_css_class('keyboard-selected');
    }

    keyboardUnSelected() {
        this._keyboardSelected = false;

        if (!this.container || !this._iconContainer || !this._labelContainer)
            return;

        if (this._iconContainer.get_css_classes().includes('mimic-hovered')) {
            this._iconContainer.remove_css_class('mimic-hovered');
            this._labelContainer.remove_css_class('mimic-hovered');
        }

        if (this.container.get_css_classes().includes('keyboard-selected'))
            this.container.remove_css_class('keyboard-selected');
    }

    get KeyboardSelected() {
        return this._keyboardSelected;
    }

    // eslint-disable-next-line no-unused-vars
    receiveDrop(x, y, selection, info) {
    }

    _dropCapable() {
        return false;
    }

    /** *********************
     * Icon Rendering *
     ***********************/

    async updateIcon() {
        if (this._destroying)
            return;

        if (!this._icon || !this._label) {
            this._pendingIconUpdate = true;
            return;
        }

        await this._updateIcon().catch(e => {
            if (!this._isCancellationError(e)) {
                console.error(e, `Exception while updating ${this._getVisibleName
                    ? this._getVisibleName() : 'an icon'}: ${e.message}`);
            }
        });
    }

    async _updateIcon(cancellable) {
        if (!cancellable)
            cancellable = new Gio.Cancellable();

        if (!this._icon || !this._label)
            return;

        if (this._updateIconCancellable)
            this._updateIconCancellable.cancel();

        this._updateIconCancellable = cancellable;

        try {
            if (this.Prefs.darkText) {
                this._label.remove_css_class('file-label');
                this._label.add_css_class('file-label-dark');
            } else {
                this._label.remove_css_class('file-label-dark');
                this._label.add_css_class('file-label');
            }

            try {
                const customIcon =
                    this._fileInfo.get_attribute_as_string('metadata::custom-icon');

                if (customIcon && (customIcon !== '')) {
                    const customIconFile = Gio.File.new_for_uri(customIcon);

                    if (await this._loadImageAsIcon(customIconFile, cancellable))
                        return;

                    if (cancellable.is_cancelled() || !this._icon)
                        return;
                }

                if (this.thumbnailFile && (this.thumbnailFile !== '')) {
                    const customIconFile = Gio.File.new_for_path(this.thumbnailFile);

                    if (await this.FileUtils.queryExists(
                        customIconFile,
                        cancellable
                    )) {
                        if (cancellable.is_cancelled() || !this._icon)
                            return;

                        const loadedImage =
                            await this._loadImageAsIcon(customIconFile, cancellable);

                        if (loadedImage || cancellable.is_cancelled())
                            return;
                    }
                }
            } catch (error) {
                if (this._isCancellationError(error))
                    throw error;

                console.error(error, `Error while updating icon: ${error.message}`);
            }

            if (this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_TRASH) {
                let pixbuf =
                    this._createEmblemedIcon(this._fileInfo.get_icon(), null);

                if (cancellable.is_cancelled() || !this._icon)
                    return;

                this._icon.set_paintable(null);
                this._icon.set_paintable(pixbuf);
                return;
            }

            let iconSet = false;

            if (this.Prefs.showImageThumbnails) {
                try {
                    if (!this.thumbnail) {
                        const thumbnail =
                            await this.ThumbnailLoader.getThumbnail(
                                this,
                                cancellable
                            );

                        if (cancellable.is_cancelled() || !this._icon)
                            return;

                        this.thumbnail = thumbnail;
                    }

                    if (this.thumbnail !== null) {
                        const thumbnailFile = Gio.File.new_for_path(this.thumbnail);
                        iconSet =
                            await this._loadImageAsIcon(thumbnailFile, cancellable);

                        if (cancellable.is_cancelled() || !this._icon)
                            return;
                    }
                } catch (e) {
                    if (this._isCancellationError(e))
                        throw e;

                    console.error(
                        e, `Error while generating thumbnail: ${e.message}`
                    );
                }
            }

            if (!iconSet) {
                let iconPaintable;

                if (this._isBrokenSymlink) {
                    iconPaintable =
                        this._createEmblemedIcon(null, 'text-x-generic');
                } else if (this._desktopFile && this._desktopFile.has_key('Icon')) {
                    iconPaintable =
                        this._createEmblemedIcon(
                            null,
                            this._desktopFile.get_string('Icon')
                        );
                } else {
                    iconPaintable =
                        this._createEmblemedIcon(this._getDefaultIcon(), null);
                }

                if (cancellable.is_cancelled() || !this._icon)
                    return;

                this._icon.set_paintable(null);
                this._icon.set_paintable(iconPaintable);
            }
        } finally {
            if (this._updateIconCancellable === cancellable)
                this._updateIconCancellable = null;
        }
    }

    _getDefaultIcon() {
        return this._fileInfo.get_icon();
    }

    async _loadImageAsIcon(imageFile, cancellable) {
        try {
            const [thumbnailData] =
                await imageFile.load_bytes_async(cancellable);

            if (cancellable?.is_cancelled() || !this._icon)
                return false;

            const iconTexture =
                Gdk.Texture.new_from_bytes(thumbnailData);

            let width = this.Prefs.DesiredWidth;
            let height = this.Prefs.IconSize;

            const aspectRatio = iconTexture.width / iconTexture.height;

            if ((width / height) > aspectRatio)
                width = height * aspectRatio;
            else
                height = width / aspectRatio;

            const iconPaintableSnapshot = Gtk.Snapshot.new();
            iconTexture.snapshot(
                iconPaintableSnapshot,
                Math.floor(width),
                Math.floor(height)
            );

            if (cancellable?.is_cancelled() || !this._icon)
                return false;

            let icon = iconPaintableSnapshot.to_paintable(null);
            icon = this._addEmblemsToIconIfNeeded(icon);

            if (cancellable?.is_cancelled() || !this._icon)
                return false;

            this._icon.set_paintable(null);
            this._icon.set_paintable(icon);

            return true;
        } catch (e) {
            if (this._isCancellationError(e))
                throw e;

            console
            .error(e, `Error while loading ${imageFile.get_uri()} as icon`);

            return false;
        }
    }

    _addEmblem(iconPaintable, emblem = null, position = 0) {
        if (!emblem)
            return iconPaintable;

        const scale = this._icon.get_scale_factor();

        let ratio;
        switch (this.Prefs.IconSize) {
        case 36: ratio = 3;
            break;
        case 48: ratio = 3;
            break;
        case 64: ratio = 4;
            break;
        case 96: ratio = 5;
        }

        const finalSize = Math.floor(this.Prefs.IconSize / ratio) * scale;
        const iconWidth = iconPaintable.get_intrinsic_width();
        const iconHeight = iconPaintable.get_intrinsic_height();

        const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());

        const emblemIcon =
            theme.lookup_by_gicon(
                emblem,
                finalSize / scale,
                scale,
                Gtk.TextDirection.NONE,
                Gtk.IconLookupFlags.FORCE_SIZE |
                Gtk.IconLookupFlags.FORCE_SYMBOLIC
            );

        const emblemWidth = emblemIcon.get_intrinsic_width();
        const emblemHeight = emblemIcon.get_intrinsic_height();


        const emblemSnapshot = Gtk.Snapshot.new();
        const rect = new Graphene.Rect();
        rect.init(2, 2, emblemWidth - 4, emblemHeight - 4);
        const rr = new Gsk.RoundedRect();
        rr.init_from_rect(rect, 5);
        emblemSnapshot.append_color(this.Prefs.hoverColor, rect);
        emblemIcon.snapshot(emblemSnapshot, emblemWidth, emblemHeight);
        const emblemNode = emblemSnapshot.to_node();

        const iconPaintableSnapshot = Gtk.Snapshot.new();
        iconPaintable.snapshot(iconPaintableSnapshot, iconWidth, iconHeight);
        iconPaintableSnapshot.save();

        if (!this._emblemX) {
            const desiredWidth = this.Prefs.DesiredWidth - 8;
            const estimatedWidth = iconWidth + 2 * emblemWidth;
            const finalWidth = Math.min(desiredWidth, estimatedWidth);
            this._emblemX =
                    Math.round((iconWidth + finalWidth) / 2 - emblemWidth);
        }

        const x = this._emblemX;
        const y = Math.max(0, position * (emblemHeight + 1));

        iconPaintableSnapshot.translate(new Graphene.Point({x, y}));
        iconPaintableSnapshot.append_node(emblemNode);
        iconPaintableSnapshot.restore();

        return iconPaintableSnapshot.to_paintable(null);
    }

    _addEmblemsToIconIfNeeded(iconPaintable) {
        return this._addEmblem(iconPaintable);
    }

    _createEmblemedIcon(icon, iconName) {
        if (icon === null) {
            if (GLib.path_is_absolute(iconName)) {
                try {
                    let iconFile = Gio.File.new_for_commandline_arg(iconName);
                    icon = new Gio.FileIcon({file: iconFile});
                } catch (e) {
                    icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName);
                }
            } else {
                try {
                    icon = Gio.Icon.new_for_string(iconName);
                } catch (e) {
                    icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName);
                }
            }
        }

        let theme =
            Gtk.IconTheme.get_for_display(Gdk.Display.get_default());

        const scale = this._icon.get_scale_factor();
        let iconPaintable = null;

        try {
            iconPaintable =
                theme.lookup_by_gicon(
                    icon,
                    this.Prefs.IconSize,
                    scale,
                    Gtk.TextDirection.NONE,
                    Gtk.IconLookupFlags.FORCE_SIZE
                );
        } catch (e) {
            iconPaintable =
                theme.lookup_icon(
                    'text-x-generic',
                    [],
                    this.Prefs.IconSize,
                    scale,
                    Gtk.TextDirection.NONE,
                    Gtk.IconLookupFlags.FORCE_SIZE
                );
        }

        return this._addEmblemsToIconIfNeeded(iconPaintable);
    }

    /** *********************
     * Getters and setters *
     ***********************/

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

    get state() {
        return this._state;
    }

    set state(state) {
        if (state === this._state)
            return;

        this._state = state;
    }

    set opacity(number) {
        this.container.set_opacity(number);
    }

    get dropCapable() {
        return this._dropCapable();
    }

    get isDrive() {
        return this._fileTypeEnum === this.Enums.FileType.EXTERNAL_DRIVE;
    }

    get isSelected() {
        return this._isSelected;
    }

    get isSpecial() {
        return this._isSpecial;
    }

    get savedCoordinates() {
        return this._savedCoordinates;
    }

    get normalCoordinates() {
        return this._normalCoordinates;
    }

    get monitorIndex() {
        return this._monitorIndex;
    }

    get dropCoordinates() {
        return this._dropCoordinates;
    }

    get column() {
        return this._column;
    }

    get row() {
        return this._row;
    }

    get labelText() {
        return this._currentFileName ?? this._displayName ?? this._file ?? '';
    }

    set column(num) {
        this._column = num;
    }

    set row(num) {
        this._row = num;
    }

    set dropCoordinates(pos) {
        this.writeDropCoordinates(pos);
    }

    set savedCoordinates(pos) {
        this.writeSavedCoordinates(pos);
    }

    set temporarySavedPosition(pos) {
        this._savedCoordinates = pos;
    }

    set temporaryMonitorIndex(index) {
        this._monitorIndex = index;
    }
};
