/* Desktop Icons GNOME Shell extension
 *
 * Copyright (C) 2017 Carlos Soriano <csoriano@redhat.com>
 *
 * 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, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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/>.
 */

const Gtk = imports.gi.Gtk;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const St = imports.gi.St;
const Pango = imports.gi.Pango;
const Meta = imports.gi.Meta;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Cogl = imports.gi.Cogl;
const GnomeDesktop = imports.gi.GnomeDesktop;

const Mainloop = imports.mainloop;

const Config = imports.misc.config;
const Background = imports.ui.background;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Util = imports.misc.util;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Extension = Me.imports.extension;
const Prefs = Me.imports.prefs;
const DBusUtils = Me.imports.dbusUtils;
const DesktopIconsUtil = Me.imports.desktopIconsUtil;

const Gettext = imports.gettext.domain('desktop-icons');

const _ = Gettext.gettext;

const DRAG_TRESHOLD = 8;

var S_IXUSR = 0o00100;
var S_IWOTH = 0o00002;

var State = {
    NORMAL: 0,
    GONE: 1,
};

var FileItem = GObject.registerClass({
    GTypeName: 'DesktopIcons_FileItem',
    Signals: {
        'rename-clicked': {},
        'selected': {
            param_types: [GObject.TYPE_BOOLEAN, GObject.TYPE_BOOLEAN, GObject.TYPE_BOOLEAN]
        }
    }
}, class FileItem extends St.Bin {
    _init(file, fileInfo, fileExtra) {
        super._init({ visible: true });

        this._fileExtra = fileExtra;
        this._loadThumbnailDataCancellable = null;
        this._thumbnailScriptWatch = 0;
        this._setMetadataCancellable = null;
        this._queryFileInfoCancellable = null;
        this._isSpecial = this._fileExtra != Prefs.FileType.NONE;

        this._file = file;
        this._mount = null;
        if (this._fileExtra == Prefs.FileType.MOUNT_DISK)
            this._mount = this._file.find_enclosing_mount(null);

        this._savedCoordinates = null;
        let savedCoordinates = fileInfo.get_attribute_as_string('metadata::nautilus-icon-position');
        if (savedCoordinates != null)
            this._savedCoordinates = savedCoordinates.split(',').map(x => Number(x));

        this._state = State.NORMAL;

        try {
            this.set_fill(true, true);
        } catch(error) {}
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        this._delegate = this;
        this.connect('destroy', () => this._onDestroy());

        this._container = new St.BoxLayout({ reactive: true,
                                             track_hover: true,
                                             can_focus: true,
                                             style_class: 'file-item',
                                             x_expand: true,
                                             y_expand: true,
                                             x_align: Clutter.ActorAlign.FILL,
                                             vertical: true });
        this.set_child(this._container);
        this._icon = new St.Bin();
        this._icon.set_height(Prefs.get_icon_size() * scaleFactor);
        this._iconAllocationIdleId = 0;
        this._iconAllocationId = this._icon.connect("notify::allocation", () => {
            if (this._iconAllocationIdleId)
                GLib.source_remove(this._iconAllocationIdleId);
            this._iconAllocationIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                GLib.source_remove(this._iconAllocationIdleId);
                this._iconAllocationIdleId = 0;
                this._updateIcon();
                return GLib.SOURCE_REMOVE;
            });
        });

        this._iconContainer = new St.Bin({ visible: true });
        this._iconContainer.child = this._icon;
        this._container.add_child(this._iconContainer);

        this._label = new St.Label({
            style_class: 'name-label'
        });

        this._container.add_child(this._label);
        let clutterText = this._label.get_clutter_text();
        /* TODO: Convert to gobject.set for 3.30 */
        clutterText.set_line_wrap(true);
        clutterText.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR);
        clutterText.set_ellipsize(Pango.EllipsizeMode.END);

        this._container.connect('button-press-event', (actor, event) => this._onPressButton(actor, event));
        this._container.connect('motion-event', (actor, event) => this._onMotion(actor, event));
        this._container.connect('leave-event', (actor, event) => this._onLeave(actor, event));
        this._container.connect('enter-event', (actor, event) => this._onEnter(actor, event));
        this._container.connect('button-release-event', (actor, event) => this._onReleaseButton(actor, event));

        /* Set the metadata and update relevant UI */
        this._updateMetadataFromFileInfo(fileInfo);

        this._menuManager = null;
        this._menu = null;
        this._updateIcon();

        this._isSelected = false;
        this._primaryButtonPressed = false;
        if (this._attributeCanExecute && !this._isValidDesktopFile)
            this._execLine = this.file.get_path();
        if (fileExtra == Prefs.FileType.USER_DIRECTORY_TRASH) {
            // if this icon is the trash, monitor the state of the directory to update the icon
            this._trashChanged = false;
            this._queryTrashInfoCancellable = null;
            this._scheduleTrashRefreshId = 0;
            this._monitorTrashDir = this._file.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, null);
            this._monitorTrashId = this._monitorTrashDir.connect('changed', (obj, file, otherFile, eventType) => {
                switch(eventType) {
                    case Gio.FileMonitorEvent.DELETED:
                    case Gio.FileMonitorEvent.MOVED_OUT:
                    case Gio.FileMonitorEvent.CREATED:
                    case Gio.FileMonitorEvent.MOVED_IN:
                        if (this._queryTrashInfoCancellable || this._scheduleTrashRefreshId) {
                            if (this._scheduleTrashRefreshId)
                                GLib.source_remove(this._scheduleTrashRefreshId);
                            this._scheduleTrashRefreshId = Mainloop.timeout_add(200, () => this._refreshTrashIcon());
                        } else {
                            this._refreshTrashIcon();
                        }
                    break;
                }
            });
        }

        this._writebleByOthersId = Extension.desktopManager.connect('notify::writable-by-others', () => {
            if (!this._isValidDesktopFile)
                return;
            this._refreshMetadataAsync(true);
        });
    }

    set_margins(width, height) {
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        this.set_width(Prefs.getDesiredWidth(scaleFactor, width));
        this.set_height(Prefs.getDesiredHeight(scaleFactor, height));
    }

    onAttributeChanged() {
        this._refreshMetadataAsync(this._isDesktopFile);
    }

    _onDestroy() {
        /* Regular file data */
        if (this._setMetadataCancellable)
            this._setMetadataCancellable.cancel();
        if (this._queryFileInfoCancellable)
            this._queryFileInfoCancellable.cancel();

        Extension.desktopManager.disconnect(this._writebleByOthersId);

        /* Thumbnailing */
        if (this._thumbnailScriptWatch)
            GLib.source_remove(this._thumbnailScriptWatch);
        if (this._loadThumbnailDataCancellable)
            this._loadThumbnailDataCancellable.cancel();

        /* Desktop file */
        if (this._monitorDesktopFileId) {
            this._monitorDesktopFile.disconnect(this._monitorDesktopFileId);
            this._monitorDesktopFile.cancel();
        }

        /* Trash */
        if (this._monitorTrashDir) {
            this._monitorTrashDir.disconnect(this._monitorTrashId);
            this._monitorTrashDir.cancel();
        }
        if (this._queryTrashInfoCancellable)
            this._queryTrashInfoCancellable.cancel();
        if (this._scheduleTrashRefreshId)
            GLib.source_remove(this._scheduleTrashRefreshId);

        /* Icon */
        this._icon.disconnect(this._iconAllocationId);
        if (this._iconAllocationIdleId)
            GLib.source_remove(this._iconAllocationIdleId);

        /* Menu */
        this._removeMenu();
    }

    _refreshMetadataAsync(rebuild) {
        if (this._queryFileInfoCancellable)
            this._queryFileInfoCancellable.cancel();
        this._queryFileInfoCancellable = new Gio.Cancellable();
        this._file.query_info_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES,
                                    Gio.FileQueryInfoFlags.NONE,
                                    GLib.PRIORITY_DEFAULT,
                                    this._queryFileInfoCancellable,
            (source, result) => {
                try {
                    let newFileInfo = source.query_info_finish(result);
                    this._queryFileInfoCancellable = null;
                    this._updateMetadataFromFileInfo(newFileInfo);
                    if (rebuild)
                        this._recreateMenu();
                    this._updateIcon();
                } catch(error) {
                    if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                        global.log("Error getting the file info: " + error);
                }
            });
    }

    _updateMetadataFromFileInfo(fileInfo) {
        this._fileInfo = fileInfo;

        let oldLabelText = this._label.text;

        this._displayName = fileInfo.get_attribute_as_string('standard::display-name');
        this._attributeCanExecute = fileInfo.get_attribute_boolean('access::can-execute');
        this._unixmode = fileInfo.get_attribute_uint32('unix::mode');
        this._writableByOthers = (this._unixmode & S_IWOTH) != 0;
        this._trusted = fileInfo.get_attribute_as_string('metadata::trusted') == 'true';
        this._attributeContentType = fileInfo.get_content_type();
        this._isDesktopFile = this._attributeContentType == 'application/x-desktop';

        if (this._isDesktopFile && this._writableByOthers)
            log(`desktop-icons: File ${this._displayName} is writable by others - will not allow launching`);

        if (this._isDesktopFile) {
            this._desktopFile = Gio.DesktopAppInfo.new_from_filename(this._file.get_path());
            if (!this._desktopFile) {
                log(`Couldn’t parse ${this._displayName} as a desktop file, will treat it as a regular file.`);
                this._isValidDesktopFile = false;
            } else {
                this._isValidDesktopFile = true;
            }
        } else {
            this._isValidDesktopFile = false;
        }

        if (this.displayName != oldLabelText) {
            this._label.text = this.displayName;
        }

        this._fileType = fileInfo.get_file_type();
        this._isDirectory = this._fileType == Gio.FileType.DIRECTORY;
        this._isSpecial = this._fileExtra != Prefs.FileType.NONE;
        this._isHidden = fileInfo.get_is_hidden() | fileInfo.get_is_backup();
        this._isSymlink = fileInfo.get_is_symlink();
        this._modifiedTime = this._fileInfo.get_attribute_uint64("time::modified");
        /*
         * This is a glib trick to detect broken symlinks. If a file is a symlink, the filetype
         * points to the final file, unless it is broken; thus if the file type is SYMBOLIC_LINK,
         * it must be a broken link.
         * https://developer.gnome.org/gio/stable/GFile.html#g-file-query-info
         */
        this._isBrokenSymlink = this._isSymlink && this._fileType == Gio.FileType.SYMBOLIC_LINK;
    }

    onFileRenamed(file) {
        this._file = file;
        this._refreshMetadataAsync(false);
    }

    _updateIcon() {
        if (this._fileExtra == Prefs.FileType.USER_DIRECTORY_TRASH) {
            this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null);
            return;
        }
        if (this._fileExtra == Prefs.FileType.MOUNT_DISK) {
            this._icon.child = this._createEmblemedStIcon(this._mount.get_icon(), null);
            return;
        }

        let thumbnailFactory = GnomeDesktop.DesktopThumbnailFactory.new(GnomeDesktop.DesktopThumbnailSize.LARGE);
        if ((Prefs.nautilusSettings.get_string('show-image-thumbnails') != 'never') &&
            (thumbnailFactory.can_thumbnail(this._file.get_uri(),
                                            this._attributeContentType,
                                            this._modifiedTime))) {
            let thumbnail = thumbnailFactory.lookup(this._file.get_uri(), this._modifiedTime);
            if (thumbnail == null) {
                if (!thumbnailFactory.has_valid_failed_thumbnail(this._file.get_uri(),
                                                                 this._modifiedTime)) {
                    let argv = [];
                    argv.push(GLib.build_filenamev([ExtensionUtils.getCurrentExtension().path,
                                                   'createThumbnail.js']));
                    argv.push(this._file.get_path());
                    let [success, pid] = GLib.spawn_async(null, argv, null,
                                                          GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, null);
                    if (this._thumbnailScriptWatch)
                        GLib.source_remove(this._thumbnailScriptWatch);
                    this._thumbnailScriptWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT,
                                                                      pid,
                        (pid, exitCode) => {
                            this._thumbnailScriptWatch = 0;
                            if (exitCode == 0)
                                this._updateIcon();
                            else
                                global.log('Failed to generate thumbnail for ' + this._filePath);
                            GLib.spawn_close_pid(pid);
                            return false;
                        }
                    );
                }
            } else {
                if (this._loadThumbnailDataCancellable)
                    this._loadThumbnailDataCancellable.cancel();
                this._loadThumbnailDataCancellable = new Gio.Cancellable();
                let thumbnailFile = Gio.File.new_for_path(thumbnail);
                thumbnailFile.load_bytes_async(this._loadThumbnailDataCancellable,
                    (source, result) => {
                        try {
                            this._loadThumbnailDataCancellable = null;
                            let [thumbnailData, etag_out] = source.load_bytes_finish(result);
                            let thumbnailStream = Gio.MemoryInputStream.new_from_bytes(thumbnailData);
                            let thumbnailPixbuf = GdkPixbuf.Pixbuf.new_from_stream(thumbnailStream, null);

                            if (thumbnailPixbuf != null) {
                                let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
                                let thumbnailImage = new Clutter.Image();
                                thumbnailImage.set_data(thumbnailPixbuf.get_pixels(),
                                                        thumbnailPixbuf.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888,
                                                        thumbnailPixbuf.width,
                                                        thumbnailPixbuf.height,
                                                        thumbnailPixbuf.rowstride
                                );
                                let icon = new Clutter.Actor();
                                icon.set_content(thumbnailImage);
                                let containerWidth = (this._icon.allocation.x2 - this._icon.allocation.x1) * scaleFactor;
                                let containerHeight = Prefs.get_icon_size() * scaleFactor;
                                let containerAspectRatio = containerWidth / containerHeight;
                                let iconAspectRatio = thumbnailPixbuf.width / thumbnailPixbuf.height;
                                if (containerAspectRatio > iconAspectRatio) {
                                    let iconWidth = containerHeight * iconAspectRatio;
                                    icon.set_size(iconWidth, containerHeight);
                                    let margin = (containerWidth - iconWidth) / 2;
                                    icon.margin_left = Math.ceil(margin);
                                    icon.margin_right = Math.floor(margin);
                                } else {
                                    let iconHeight = containerWidth / iconAspectRatio;
                                    icon.set_size(containerWidth, iconHeight);
                                    let margin = (containerHeight - iconHeight) / 2;
                                    icon.margin_top = Math.ceil(margin);
                                    icon.margin_bottom = Math.floor(margin);
                                }
                                this._icon.child = icon;
                            }
                        } catch (error) {
                            if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
                                global.log('Error while loading thumbnail: ' + error);
                                this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null);
                            }
                        }
                    }
                );
            }
        }

        if (this._isBrokenSymlink) {
            this._icon.child = this._createEmblemedStIcon(null, 'text-x-generic');
        } else {
            if (this.trustedDesktopFile && this._desktopFile.has_key('Icon'))
                this._icon.child = this._createEmblemedStIcon(null, this._desktopFile.get_string('Icon'));
            else
                this._icon.child = this._createEmblemedStIcon(this._fileInfo.get_icon(), null);
        }
    }

    _refreshTrashIcon() {
        if (this._queryTrashInfoCancellable)
            this._queryTrashInfoCancellable.cancel();
        this._queryTrashInfoCancellable = new Gio.Cancellable();

        this._file.query_info_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES,
                                    Gio.FileQueryInfoFlags.NONE,
                                    GLib.PRIORITY_DEFAULT,
                                    this._queryTrashInfoCancellable,
            (source, result) => {
                try {
                    this._fileInfo = source.query_info_finish(result);
                    this._queryTrashInfoCancellable = null;
                    this._updateIcon();
                } catch(error) {
                    if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                        global.log('Error getting the number of files in the trash: ' + error);
                }
            });

        this._scheduleTrashRefreshId = 0;
        return false;
    }

    get file() {
        return this._file;
    }

    get isHidden() {
        return this._isHidden;
    }

    _createEmblemedStIcon(icon, iconName) {
        if (icon == null) {
            if (GLib.path_is_absolute(iconName)) {
                let iconFile = Gio.File.new_for_commandline_arg(iconName);
                icon = new Gio.FileIcon({ file: iconFile });
            } else {
                icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName);
            }
        }
        let itemIcon = Gio.EmblemedIcon.new(icon, null);

        if (this._isSymlink) {
            if (this._isBrokenSymlink)
                itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-unreadable')));
            else
                itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link')));
        } else if (this.trustedDesktopFile) {
            itemIcon.add_emblem(Gio.Emblem.new(Gio.ThemedIcon.new('emblem-symbolic-link')));
        }

        return new St.Icon({ gicon: itemIcon,
                             icon_size: Prefs.get_icon_size()
        });
    }

    doRename() {
        if (!this.canRename()) {
            log (`Error: ${this.file.get_uri()} cannot be renamed`);
            return;
        }

        this.emit('rename-clicked');
    }

    _doOpenContext(context) {
        if (this._isBrokenSymlink) {
            log(`Error: Can’t open ${this.file.get_uri()} because it is a broken symlink.`);
            return;
        }

        if (this.trustedDesktopFile) {
            this._desktopFile.launch_uris_as_manager([], context, GLib.SpawnFlags.SEARCH_PATH, null, null);
            return;
        }

        if (this._attributeCanExecute &&
           !this._isDirectory &&
           !this._isValidDesktopFile &&
            Gio.content_type_can_be_executable(this._attributeContentType)) {
                if (this._execLine)
                    Util.spawnCommandLine(this._execLine);
                return;
        }

        Gio.AppInfo.launch_default_for_uri_async(this.file.get_uri(),
            null, null,
            (source, result) => {
                try {
                    Gio.AppInfo.launch_default_for_uri_finish(result);
                } catch (e) {
                    log('Error opening file ' + this.file.get_uri() + ': ' + e.message);
                }
            }
        );
    }

    doOpen() {
        this._doOpenContext(null);
    }

    _doDiscreteGpu() {
        let gpus = Extension.desktopManager.switcherooProxyGPUs;
        if (!gpus) {
            log('Could not apply discrete GPU environment, no GPUs in list');
        }

        for(let gpu in gpus) {
            if (!gpus[gpu])
                continue;

            let default_variant = gpus[gpu]['Default'];
            if (!default_variant || default_variant.get_boolean())
                continue;

            let env = gpus[gpu]['Environment'];
            if (!env)
                continue;

            let env_s = env.get_strv();
            let context = new Gio.AppLaunchContext;
            for (let i = 0; i < env_s.length; i=i+2) {
                context.setenv(env_s[i], env_s[i+1]);
            }
            this._doOpenContext(context);
            return;
        }
        log('Could not find discrete GPU data in switcheroo-control');
    }

    _onCopyClicked() {
        Extension.desktopManager.doCopy();
    }

    _onCutClicked() {
        Extension.desktopManager.doCut();
    }

    _onShowInFilesClicked() {

        DBusUtils.FreeDesktopFileManagerProxy.ShowItemsRemote([this.file.get_uri()], '',
            (result, error) => {
                if (error)
                    log('Error showing file on desktop: ' + error.message);
            }
        );
    }

    _onPropertiesClicked() {

        DBusUtils.FreeDesktopFileManagerProxy.ShowItemPropertiesRemote([this.file.get_uri()], '',
            (result, error) => {
                if (error)
                    log('Error showing properties: ' + error.message);
            }
        );
    }

    _onMoveToTrashClicked() {
        Extension.desktopManager.doTrash();
    }

    _onEmptyTrashClicked() {
        Extension.desktopManager.doEmptyTrash();
    }

    _onEjectClicked() {
        DesktopIconsUtil.eject(this._mount);
    }

    get _allowLaunchingText() {
        if (this.trustedDesktopFile)
            return _("Don’t Allow Launching");

        return _("Allow Launching");
    }

    get metadataTrusted() {
        return this._trusted;
    }

    set metadataTrusted(value) {
        this._trusted = value;

        let info = new Gio.FileInfo();
        info.set_attribute_string('metadata::trusted',
                                  value ? 'true' : 'false');
        this._file.set_attributes_async(info,
                                        Gio.FileQueryInfoFlags.NONE,
                                        GLib.PRIORITY_LOW,
                                        null,
            (source, result) => {
                try {
                    source.set_attributes_finish(result);
                    this._refreshMetadataAsync(true);
                } catch(e) {
                    log(`Failed to set metadata::trusted: ${e.message}`);
                }
        });
    }

    _onAllowDisallowLaunchingClicked() {
        this.metadataTrusted = !this.trustedDesktopFile;

        /*
         * we're marking as trusted, make the file executable too. note that we
         * do not ever remove the executable bit, since we don't know who set
         * it.
         */
        if (this.metadataTrusted && !this._attributeCanExecute) {
            let info = new Gio.FileInfo();
            let newUnixMode = this._unixmode | S_IXUSR;
            info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, newUnixMode);
            this._file.set_attributes_async(info,
                                            Gio.FileQueryInfoFlags.NONE,
                                            GLib.PRIORITY_LOW,
                                            null,
                (source, result) => {
                    try {
                        source.set_attributes_finish (result);
                    } catch(e) {
                        log(`Failed to set unix mode: ${e.message}`);
                    }
            });
        }
    }

    canRename() {
        return !this.trustedDesktopFile && this._fileExtra == Prefs.FileType.NONE;
    }

    _doOpenWith() {
        DBusUtils.openFileWithOtherApplication(this.file.get_path());
    }

    _getSelectionStyle() {
        let rgba = DesktopIconsUtil.getGtkClassBackgroundColor('view', Gtk.StateFlags.SELECTED);
        let background_color =
            'rgba(' + rgba.red * 255 + ', ' + rgba.green * 255 + ', ' + rgba.blue * 255 + ', 0.6)';
        let border_color =
            'rgba(' + rgba.red * 255 + ', ' + rgba.green * 255 + ', ' + rgba.blue * 255 + ', 0.8)';

        return 'background-color: ' + background_color + ';' +
               'border-color: ' + border_color + ';';
    }

    get menu() {
        return this._menu;
    }

    _removeMenu() {
        if (this._menu != null) {
            if (this._menuManager != null)
                this._menuManager.removeMenu(this._menu);

            Main.layoutManager.uiGroup.remove_child(this._menu.actor);
            this._menu.destroy();
            this._menu = null;
        }

        this._menuManager = null;
    }

    _recreateMenu() {
        this._removeMenu();
        this._menuManager = new PopupMenu.PopupMenuManager(this);
        let side = St.Side.LEFT;
        if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
            side = St.Side.RIGHT;
        this._menu = new PopupMenu.PopupMenu(this, 0.5, side);
        this._menu.addAction(_('Open'), () => this.doOpen());
        switch (this._fileExtra) {
        case Prefs.FileType.NONE:
            if (!this._isDirectory) {
                this._actionOpenWith = this._menu.addAction(_('Open With Other Application'), () => this._doOpenWith());
                if (Extension.desktopManager.discreteGpuAvailable)
                    this._menu.addAction(_('Launch using Dedicated Graphics Card'), () => this._doDiscreteGpu());
            } else {
                this._actionOpenWith = null;
            }
            this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
            this._actionCut = this._menu.addAction(_('Cut'), () => this._onCutClicked());
            this._actionCopy = this._menu.addAction(_('Copy'), () => this._onCopyClicked());
            if (this.canRename())
                this._menu.addAction(_('Rename…'), () => this.doRename());
            this._actionTrash = this._menu.addAction(_('Move to Trash'), () => this._onMoveToTrashClicked());
            if (this._isValidDesktopFile && !Extension.desktopManager.writableByOthers && !this._writableByOthers) {
                this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
                this._allowLaunchingMenuItem = this._menu.addAction(this._allowLaunchingText,
                                                                    () => this._onAllowDisallowLaunchingClicked());

            }
            break;
        case Prefs.FileType.USER_DIRECTORY_TRASH:
            this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
            this._menu.addAction(_('Empty Trash'), () => this._onEmptyTrashClicked());
            break;
        case Prefs.FileType.MOUNT_DISK:
            this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
            this._menu.addAction(_('Eject'), () => this._onEjectClicked());
            break;
        default:
            break;
        }
        this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this._menu.addAction(_('Properties'), () => this._onPropertiesClicked());
        this._menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this._menu.addAction(_('Show in Files'), () => this._onShowInFilesClicked());
        if (this._isDirectory && this.file.get_path() != null)
            this._actionOpenInTerminal = this._menu.addAction(_('Open in Terminal'), () => this._onOpenTerminalClicked());

        this._menuManager.addMenu(this._menu);

        Main.layoutManager.uiGroup.add_child(this._menu.actor);
        this._menu.actor.hide();
    }

    _ensureMenu() {
        if (this._menu == null)
            this._recreateMenu();

        return this._menu;
    }

    _onOpenTerminalClicked () {
        DesktopIconsUtil.launchTerminal(this.file.get_path());
    }

    _onPressButton(actor, event) {
        let button = event.get_button();
        if (button == 3) {
            if (!this.isSelected)
                this.emit('selected', false, false, true);
            this._ensureMenu().toggle();
            if (this._actionOpenWith) {
                let allowOpenWith = (Extension.desktopManager.getNumberOfSelectedItems() == 1);
                this._actionOpenWith.setSensitive(allowOpenWith);
            }
            let specialFilesSelected = Extension.desktopManager.checkIfSpecialFilesAreSelected();
            if (this._actionCut)
                this._actionCut.setSensitive(!specialFilesSelected);
            if (this._actionCopy)
                this._actionCopy.setSensitive(!specialFilesSelected);
            if (this._actionTrash)
                this._actionTrash.setSensitive(!specialFilesSelected);
            return Clutter.EVENT_STOP;
        } else if (button == 1) {
            if (event.get_click_count() == 1) {
                let [x, y] = event.get_coords();
                this._primaryButtonPressed = true;
                this._buttonPressInitialX = x;
                this._buttonPressInitialY = y;
                let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK);
                let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK);
                if (controlPressed || shiftPressed)
                    this.emit('selected', true, false, !this._isSelected);
                else
                    this.emit('selected', false, false, true);
            }
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _onEnter(actor, event) {
        if (Prefs.CLICK_POLICY_SINGLE)
            global.display.set_cursor(Meta.Cursor.POINTING_HAND);
        else
            global.display.set_cursor(Meta.Cursor.DEFAULT);
    }

    _onLeave(actor, event) {
        this._primaryButtonPressed = false;
        if (Prefs.CLICK_POLICY_SINGLE)
           global.display.set_cursor(Meta.Cursor.DEFAULT);
    }

    _onMotion(actor, event) {
        let [x, y] = event.get_coords();
        if (this._primaryButtonPressed) {
            let xDiff = x - this._buttonPressInitialX;
            let yDiff = y - this._buttonPressInitialY;
            let distance = Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2));
            if (distance > DRAG_TRESHOLD) {
                // Don't need to track anymore this if we start drag, and also
                // avoids reentrance here
                this._primaryButtonPressed = false;
                let event = Clutter.get_current_event();
                let [x, y] = event.get_coords();
                Extension.desktopManager.dragStart();
            }
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _onReleaseButton(actor, event) {
        let button = event.get_button();
        if (button == 1) {
            // primaryButtonPressed is TRUE only if the user has pressed the button
            // over an icon, and if (s)he has not started a drag&drop operation
            if (this._primaryButtonPressed) {
                this._primaryButtonPressed = false;
                let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK);
                let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK);
                if ((event.get_click_count() == 1) && Prefs.CLICK_POLICY_SINGLE && !shiftPressed && !controlPressed)
                    this.doOpen();
                return Clutter.EVENT_STOP;
            }
            if ((event.get_click_count() == 2) && (!Prefs.CLICK_POLICY_SINGLE))
                this.doOpen();
        }
        return Clutter.EVENT_PROPAGATE;
    }

    get savedCoordinates() {
        return this._savedCoordinates;
    }

    _onSetMetadataFileFinished(source, result) {
        try {
            let [success, info] = source.set_attributes_finish(result);
        } catch (error) {
            if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log('Error setting metadata to desktop files ', error);
        }
    }

    set savedCoordinates(pos) {
        if (this._setMetadataCancellable)
            this._setMetadataCancellable.cancel();

        this._setMetadataCancellable = new Gio.Cancellable();
        this._savedCoordinates = [pos[0], pos[1]];
        let info = new Gio.FileInfo();
        info.set_attribute_string('metadata::nautilus-icon-position',
                                  `${pos[0]},${pos[1]}`);
        this.file.set_attributes_async(info,
                                       Gio.FileQueryInfoFlags.NONE,
                                       GLib.PRIORITY_DEFAULT,
                                       this._setMetadataCancellable,
            (source, result) => {
                this._setMetadataCancellable = null;
                this._onSetMetadataFileFinished(source, result);
            }
        );
    }

    intersectsWith(argX, argY, argWidth, argHeight) {
        let rect = new Meta.Rectangle({ x: argX, y: argY, width: argWidth, height: argHeight });
        let [containerX, containerY] = this._container.get_transformed_position();
        let boundingBox = new Meta.Rectangle({ x: containerX,
                                               y: containerY,
                                               width: this._container.allocation.x2 - this._container.allocation.x1,
                                               height: this._container.allocation.y2 - this._container.allocation.y1 });
        let [intersects, _] = rect.intersect(boundingBox);

        return intersects;
    }

    set isSelected(isSelected) {
        isSelected = !!isSelected;
        if (isSelected == this._isSelected)
            return;

        if (isSelected) {
            this._container.set_style(this._getSelectionStyle());
        } else {
            this._container.set_style('background-color: transparent');
            this._container.set_style('border-color: transparent');
        }

        this._isSelected = isSelected;
    }

    get isSelected() {
        return this._isSelected;
    }

    get isSpecial() {
        return this._isSpecial;
    }

    get state() {
        return this._state;
    }

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

        this._state = state;
    }

    get isDirectory() {
        return this._isDirectory;
    }

    get trustedDesktopFile() {
        return this._isValidDesktopFile &&
               this._attributeCanExecute &&
               this.metadataTrusted &&
               !Extension.desktopManager.writableByOthers &&
               !this._writableByOthers;
    }

    get fileName() {
        return this._fileInfo.get_name();
    }

    get displayName() {
        if (this._fileExtra == Prefs.FileType.USER_DIRECTORY_HOME)
            return _("Home");
        if (this.trustedDesktopFile)
            return this._desktopFile.get_name();

        return this._displayName || null;
    }

    acceptDrop() {
        return Extension.desktopManager.selectionDropOnFileItem(this);
    }
});
