/* 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 St = imports.gi.St;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Shell = imports.gi.Shell;

const Layout = imports.ui.layout;
const Main = imports.ui.main;
const BoxPointer = imports.ui.boxpointer;
const PopupMenu = imports.ui.popupMenu;
const GrabHelper = imports.ui.grabHelper;
const Config = imports.misc.config;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const CreateFolderDialog = Me.imports.createFolderDialog;
const Extension = Me.imports.extension;
const FileItem = Me.imports.fileItem;
const Prefs = Me.imports.prefs;
const DBusUtils = Me.imports.dbusUtils;
const DesktopIconsUtil = Me.imports.desktopIconsUtil;
const Util = imports.misc.util;

const Clipboard = St.Clipboard.get_default();
const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD;
const Gettext = imports.gettext.domain('desktop-icons');

const _ = Gettext.gettext;


/* From NautilusFileUndoManagerState */
var UndoStatus = {
    NONE: 0,
    UNDO: 1,
    REDO: 2,
};

var StoredCoordinates = {
    PRESERVE: 0,
    OVERWRITE:1,
    ASSIGN:2,
};

var Placeholder = GObject.registerClass({
    GTypeName: 'DesktopIcons_Placeholder',
}, class Placeholder extends St.Bin {
});

var DesktopGrid = GObject.registerClass({
    GTypeName: 'DesktopIcons_DesktopGrid',
}, class DesktopGrid extends St.Widget {
    _init(bgManager) {
        this._bgManager = bgManager;

        this._fileItemHandlers = new Map();
        this._fileItems = [];

        this.layout = new Clutter.GridLayout({
            orientation: Clutter.Orientation.VERTICAL,
            column_homogeneous: true,
            row_homogeneous: true
        });

        this._actorLayout = new Clutter.BinLayout({
            x_align: Clutter.BinAlignment.FIXED,
            y_align: Clutter.BinAlignment.FIXED
        });

        super._init({
            layout_manager: this._actorLayout
        });
        this._delegate = this;

        this.connect('style-changed', () => {
            if (this.isFirst)
                Extension.desktopManager.scheduleReLayoutChildren();
        });

        this._grid = new St.Widget({
            name: 'DesktopGrid',
            layout_manager: this.layout,
            reactive: true,
            x_expand: true,
            y_expand: true,
            can_focus: true,
            opacity: 255
        });
        this.add_child(this._grid);

        this._menuManager = new PopupMenu.PopupMenuManager(this);

        this._bgManager._container.add_child(this);

        let monitorIndex = bgManager._monitorIndex;
        this._monitorConstraint = new Layout.MonitorConstraint({
            index: monitorIndex,
            work_area: true
        });
        this.add_constraint(this._monitorConstraint);

        this._addDesktopBackgroundMenu();

        this._bgDestroyedId = bgManager.backgroundActor.connect('destroy',
            () => this._backgroundDestroyed());

        this._grid.connect('button-press-event', (actor, event) => this._onPressButton(actor, event));

        this._grid.connect('key-press-event', this._onKeyPress.bind(this));

        this._grid.connect('notify::allocation', () => Extension.desktopManager.scheduleReLayoutChildren());

        let themeNodeBase = this.get_theme_node();
        let themeContext = St.ThemeContext.get_for_stage(global.stage);
        let themeNode = St.ThemeNode.new(themeContext, null,
            themeNodeBase.get_theme(), themeNodeBase.get_element_type(),
            null, "file-item", null, "fake-id {}");
        this._extra_width =  themeNode.get_margin(St.Side.LEFT) +
                             themeNode.get_margin(St.Side.RIGHT) +
                             themeNode.get_border_width(St.Side.LEFT) +
                             themeNode.get_border_width(St.Side.RIGHT) +
                             themeNode.get_horizontal_padding();
        this._extra_height = themeNode.get_margin(St.Side.TOP) +
                             themeNode.get_margin(St.Side.BOTTOM) +
                             themeNode.get_border_width(St.Side.TOP) +
                             themeNode.get_border_width(St.Side.BOTTOM) +
                             themeNode.get_vertical_padding();

        this.connect('destroy', () => this._onDestroy());
    }

    _onKeyPress(actor, event) {
        if (global.stage.get_key_focus() != actor)
            return Clutter.EVENT_PROPAGATE;

        let symbol = event.get_key_symbol();
        let isCtrl = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) != 0;
        let isShift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK) != 0;

        if (isCtrl && isShift && [Clutter.KEY_Z, Clutter.KEY_z].indexOf(symbol) > -1) {
            this._doRedo();
            return Clutter.EVENT_STOP;
        }
        else if (isCtrl && [Clutter.KEY_Z, Clutter.KEY_z].indexOf(symbol) > -1) {
            this._doUndo();
            return Clutter.EVENT_STOP;
        }
        else if (isCtrl && [Clutter.KEY_C, Clutter.KEY_c].indexOf(symbol) > -1) {
            Extension.desktopManager.doCopy();
            return Clutter.EVENT_STOP;
        }
        else if (isCtrl && [Clutter.KEY_X, Clutter.KEY_x].indexOf(symbol) > -1) {
            Extension.desktopManager.doCut();
            return Clutter.EVENT_STOP;
        }
        else if (isCtrl && [Clutter.KEY_V, Clutter.KEY_v].indexOf(symbol) > -1) {
            this._doPaste();
            return Clutter.EVENT_STOP;
        }
        else if (symbol == Clutter.KEY_Return) {
            Extension.desktopManager.doOpen();
            return Clutter.EVENT_STOP;
        }
        else if (symbol == Clutter.KEY_Delete) {
            Extension.desktopManager.doTrash();
            return Clutter.EVENT_STOP;
        } else if (symbol == Clutter.KEY_F2) {
            // Support renaming other grids file items.
            Extension.desktopManager.doRename();
            return Clutter.EVENT_STOP;
        } else if (symbol == Clutter.KEY_space) {
            Extension.desktopManager.doPreview();
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _backgroundDestroyed() {
        this._bgDestroyedId = 0;
        if (this._bgManager == null)
            return;

        if (this._bgManager._backgroundSource) {
            this._bgDestroyedId = this._bgManager.backgroundActor.connect('destroy',
                () => this._backgroundDestroyed());
        } else {
            this.destroy();
        }
    }

    _onDestroy() {
        if (this._bgDestroyedId && this._bgManager.backgroundActor)
            this._bgManager.backgroundActor.disconnect(this._bgDestroyedId);
        this._bgDestroyedId = 0;
        this._bgManager = null;
        this._menuManager = null;
    }

    _onNewFolderClicked() {

        let dialog = new CreateFolderDialog.CreateFolderDialog();

        dialog.connect('response', (dialog, name) => {
            let dir = DesktopIconsUtil.getDesktopDir().get_child(name);
            DBusUtils.NautilusFileOperationsProxy.CreateFolderRemote(dir.get_uri(),
                (result, error) => {
                    if (error)
                        throw new Error('Error creating new folder: ' + error.message);
                }
            );
        });

        dialog.open();
    }

    _parseClipboardText(text) {
        if (text === null)
            return [false, false, null];

        let lines = text.split('\n');
        let [mime, action, ...files] = lines;

        if (mime != 'x-special/nautilus-clipboard')
            return [false, false, null];

        if (!(['copy', 'cut'].includes(action)))
            return [false, false, null];
        let isCut = action == 'cut';

        /* Last line is empty due to the split */
        if (files.length <= 1)
            return [false, false, null];
        /* Remove last line */
        files.pop();

        return [true, isCut, files];
    }

    _doPaste() {
        Clipboard.get_text(CLIPBOARD_TYPE,
            (clipboard, text) => {
                let [valid, is_cut, files] = this._parseClipboardText(text);
                if (!valid)
                    return;

                let desktopDir = `${DesktopIconsUtil.getDesktopDir().get_uri()}`;
                if (is_cut) {
                    DBusUtils.NautilusFileOperationsProxy.MoveURIsRemote(files, desktopDir,
                        (result, error) => {
                            if (error)
                                throw new Error('Error moving files: ' + error.message);
                        }
                    );
                } else {
                    DBusUtils.NautilusFileOperationsProxy.CopyURIsRemote(files, desktopDir,
                        (result, error) => {
                            if (error)
                                throw new Error('Error copying files: ' + error.message);
                        }
                    );
                }
            }
        );
    }

    _onPasteClicked() {
        this._doPaste();
    }

    _doUndo() {
        DBusUtils.NautilusFileOperationsProxy.UndoRemote(
            (result, error) => {
                if (error)
                    throw new Error('Error performing undo: ' + error.message);
            }
        );
    }

    _onUndoClicked() {
        this._doUndo();
    }

    _doRedo() {
        DBusUtils.NautilusFileOperationsProxy.RedoRemote(
            (result, error) => {
                if (error)
                    throw new Error('Error performing redo: ' + error.message);
            }
        );
    }

    _onRedoClicked() {
        this._doRedo();
    }

    _onOpenDesktopInFilesClicked() {
        Gio.AppInfo.launch_default_for_uri_async(DesktopIconsUtil.getDesktopDir().get_uri(),
            null, null,
            (source, result) => {
                try {
                    Gio.AppInfo.launch_default_for_uri_finish(result);
                } catch (e) {
                   log('Error opening Desktop in Files: ' + e.message);
                }
            }
        );
    }

    _onOpenTerminalClicked() {
        let desktopPath = DesktopIconsUtil.getDesktopDir().get_path();
        DesktopIconsUtil.launchTerminal(desktopPath);
    }

    _syncUndoRedo() {
        this._undoMenuItem.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.UNDO;
        this._redoMenuItem.visible = DBusUtils.NautilusFileOperationsProxy.UndoStatus == UndoStatus.REDO;
    }

    _undoStatusChanged(proxy, properties, test) {
        if ('UndoStatus' in properties.deep_unpack())
            this._syncUndoRedo();
    }

    _createDesktopBackgroundMenu() {
        let menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor,
                                           0, St.Side.TOP);
        menu.addAction(_("New Folder"), () => this._onNewFolderClicked());
        this._submenu = new PopupMenu.PopupSubMenuMenuItem(_("New Document"), false);
        menu.addMenuItem(this._submenu);
        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this._pasteMenuItem = menu.addAction(_("Paste"), () => this._onPasteClicked());
        this._undoMenuItem = menu.addAction(_("Undo"), () => this._onUndoClicked());
        this._redoMenuItem = menu.addAction(_("Redo"), () => this._onRedoClicked());
        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        menu.addAction(_("Show Desktop in Files"), () => this._onOpenDesktopInFilesClicked());
        menu.addAction(_("Open in Terminal"), () => this._onOpenTerminalClicked());
        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        menu.addSettingsAction(_("Change Background…"), 'gnome-background-panel.desktop');
        menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        menu.addSettingsAction(_("Display Settings"), 'gnome-display-panel.desktop');
        menu.addSettingsAction(_("Settings"), 'gnome-control-center.desktop');

        menu.actor.add_style_class_name('background-menu');

        Main.layoutManager.uiGroup.add_child(menu.actor);
        menu.actor.hide();

        menu._propertiesChangedId = DBusUtils.NautilusFileOperationsProxy.connect('g-properties-changed',
            this._undoStatusChanged.bind(this));
        this._syncUndoRedo();

        menu.connect('destroy',
            () => DBusUtils.NautilusFileOperationsProxy.disconnect(menu._propertiesChangedId));
        menu.connect('open-state-changed',
            (popupm, isOpen) => {
                if (isOpen) {
                    Clipboard.get_text(CLIPBOARD_TYPE,
                        (clipBoard, text) => {
                            let [valid, is_cut, files] = this._parseClipboardText(text);
                            this._pasteMenuItem.setSensitive(valid);
                        }
                    );
                }
            }
        );
        this._pasteMenuItem.setSensitive(false);

        return menu;
    }

    _openMenu(x, y) {
        Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0);
        this._submenu.menu.removeAll();
        let templates = Extension.templateManager.getTemplates();
        if (templates.length == 0) {
            this._submenu.hide();
        } else {
            for(let template of templates) {
                this._submenu.menu.addAction(template["name"], () => {this._newDocument(template)}, template["icon"]);
            }
            this._submenu.show();
        }
        this._desktopBackgroundMenu.open(BoxPointer.PopupAnimation.NONE);
        /* Since the handler is in the press event it needs to ignore the release event
         * to not immediately close the menu on release
         */
        this._menuManager.ignoreRelease();
    }

    _newDocument(template) {
        let file = Extension.templateManager.getTemplateFile(template["file"]);
        if (file == null)
            return;
        let counter = 0;
        let finalName = `${template["name"]}${template["extension"]}`;
        let destination;
        do {
            if (counter != 0) {
                finalName = `${template["name"]} ${counter}${template["extension"]}`
            }
            destination = Gio.File.new_for_path(
                GLib.build_filenamev([GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP),
                                      finalName])
            );
            counter++;
        } while(destination.query_exists(null));
        file.copy_async(destination, Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, null, null, (source, result) => {
            try {
                source.copy_finish(result);
            } catch(e) {
                log(`Failed to create template ${e.message}`);
            }
        });
    }

    _addFileItemTo(fileItem, column, row, coordinatesAction) {
        let placeholder = this.layout.get_child_at(column, row);
        placeholder.child = fileItem;
        this._fileItems.push(fileItem);
        let fileItemHandler = {};
        fileItemHandler.selectedId = fileItem.connect('selected',
            this._onFileItemSelected.bind(this));
        fileItemHandler.renameId = fileItem.connect('rename-clicked',
            this._onRenameClicked.bind(this));
        fileItemHandler.destroyId = fileItem.connect('destroy', () => {
            this._fileItemHandlers.delete(fileItem);
        });
        this._fileItemHandlers.set(fileItem, fileItemHandler);

        /* If this file is new in the Desktop and hasn't yet
         * fixed coordinates, store the new possition to ensure
         * that the next time it will be shown in the same possition.
         * Also store the new possition if it has been moved by the user,
         * and not triggered by a screen change.
         */
        if ((fileItem.savedCoordinates == null) || (coordinatesAction == StoredCoordinates.OVERWRITE)) {
            let [fileX, fileY] = placeholder.get_transformed_position();
            fileItem.savedCoordinates = [Math.round(fileX), Math.round(fileY)];
        }
    }

    addFileItemCloseTo(fileItem, x, y, coordinatesAction) {
        let [column, row] = this._getEmptyPlaceClosestTo(x, y, coordinatesAction);
        fileItem.set_margins(this._extra_width, this._extra_height);
        this._addFileItemTo(fileItem, column, row, coordinatesAction);
    }

    _getEmptyPlaceClosestTo(x, y, coordinatesAction) {

        let [actorX, actorY] = this._grid.get_transformed_position();
        let actorWidth = this._grid.allocation.x2 - this._grid.allocation.x1;
        let actorHeight = this._grid.allocation.y2 - this._grid.allocation.y1;
        let placeX = Math.round((x - actorX) * this._columns / actorWidth);
        let placeY = Math.round((y - actorY) * this._rows / actorHeight);

        placeX = DesktopIconsUtil.clamp(placeX, 0, this._columns - 1);
        placeY = DesktopIconsUtil.clamp(placeY, 0, this._rows - 1);
        if (this.layout.get_child_at(placeX, placeY).child == null)
            return [placeX, placeY];
        let found = false;
        let resColumn = null;
        let resRow = null;
        let minDistance = Infinity;
        for (let column = 0; column < this._columns; column++) {
            for (let row = 0; row < this._rows; row++) {
                let placeholder = this.layout.get_child_at(column, row);
                if (placeholder.child != null)
                    continue;

                let [proposedX, proposedY] = placeholder.get_transformed_position();
                if (coordinatesAction == StoredCoordinates.ASSIGN)
                    return [column, row];
                let distance = DesktopIconsUtil.distanceBetweenPoints(proposedX, proposedY, x, y);
                if (distance < minDistance) {
                    found = true;
                    minDistance = distance;
                    resColumn = column;
                    resRow = row;
                }
            }
        }

        if (!found)
            throw new Error(`Not enough place at monitor ${this._bgManager._monitorIndex}`);

        return [resColumn, resRow];
    }

    removeFileItem(fileItem) {
        let index = this._fileItems.indexOf(fileItem);
        if (index > -1)
            this._fileItems.splice(index, 1);
        else
            throw new Error('Error removing children from container');

        let [column, row] = this._getPosOfFileItem(fileItem);
        let placeholder = this.layout.get_child_at(column, row);
        placeholder.child = null;
        let fileItemHandler = this._fileItemHandlers.get(fileItem);
        Object.values(fileItemHandler).forEach(id => fileItem.disconnect(id));
        this._fileItemHandlers.delete(fileItem);
    }

    _fillPlaceholders() {
        this._rows = this._getMaxRows();
        this._columns = this._getMaxColumns();
        for (let column = 0; column < this._columns; column++) {
            for (let row = 0; row < this._rows; row++) {
                this.layout.attach(new Placeholder(), column, row, 1, 1);
            }
        }
    }

    usingNewAllocationAPI() {
        return (this.get_fixed_position !== undefined);
    }

    reset() {
        let tmpFileItemsCopy = this._fileItems.slice();
        for (let fileItem of tmpFileItemsCopy)
            this.removeFileItem(fileItem);
        this._grid.remove_all_children();

        this._fillPlaceholders();

        if (this.usingNewAllocationAPI())
            this.allocate_preferred_size(0, 0);
    }

    _onStageMotion(actor, event) {
        if (this._drawingRubberBand) {
            let [x, y] = event.get_coords();
            this._updateRubberBand(x, y);
            this._selectFromRubberband(x, y);
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _onPressButton(actor, event) {
        let button = event.get_button();
        let [x, y] = event.get_coords();

        this._grid.grab_key_focus();

        if (button == 1) {
            let shiftPressed = !!(event.get_state() & Clutter.ModifierType.SHIFT_MASK);
            let controlPressed = !!(event.get_state() & Clutter.ModifierType.CONTROL_MASK);
            if (!shiftPressed && !controlPressed)
                Extension.desktopManager.clearSelection();
            let [gridX, gridY] = this._grid.get_transformed_position();
            Extension.desktopManager.startRubberBand(x, y, gridX, gridY);
            return Clutter.EVENT_STOP;
        }

        if (button == 3) {
            this._openMenu(x, y);

            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _addDesktopBackgroundMenu() {
        this._desktopBackgroundMenu = this._createDesktopBackgroundMenu();
        this._menuManager.addMenu(this._desktopBackgroundMenu);

        this.connect('destroy', () => {
            this._desktopBackgroundMenu.destroy();
            this._desktopBackgroundMenu = null;
        });
    }

    _getMaxColumns() {
        let gridWidth = this._grid.allocation.x2 - this._grid.allocation.x1;
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let desiredWidth = Prefs.getDesiredWidth(scaleFactor, this._extra_width);
        return Math.floor(gridWidth / desiredWidth);
    }

    _getMaxRows() {
        let gridHeight = this._grid.allocation.y2 - this._grid.allocation.y1;
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let desiredHeight = Prefs.getDesiredHeight(scaleFactor, this._extra_height);
        return Math.floor(gridHeight / desiredHeight);
    }

    acceptDrop(source, actor, x, y, time) {
        /* Coordinates are relative to the grid, we want to transform them to
         * absolute coordinates to work across monitors */
        let [gridX, gridY] = this.get_transformed_position();
        let [absoluteX, absoluteY] = [x + gridX, y + gridY];
        return Extension.desktopManager.acceptDrop(absoluteX, absoluteY);
    }

    _getPosOfFileItem(itemToFind) {
        if (itemToFind == null)
            throw new Error('Error at _getPosOfFileItem: child cannot be null');

        let found = false;
        let column = 0;
        let row = 0;
        for (column = 0; column < this._columns; column++) {
            for (row = 0; row < this._rows; row++) {
                let item = this.layout.get_child_at(column, row);
                if (item.child && item.child.file.equal(itemToFind.file)) {
                    found = true;
                    break;
                }
            }

            if (found)
                break;
        }

        if (!found)
            throw new Error('Position of file item was not found');

        return [column, row];
    }

    _onFileItemSelected(fileItem, keepCurrentSelection, rubberBandSelection, addToSelection) {
        this._grid.grab_key_focus();
    }

    _onRenameClicked(fileItem) {
        if (fileItem.menu && fileItem.menu.isOpen) {
            let id = fileItem.menu.connect('menu-closed', () => {
                fileItem.menu.disconnect(id);
                this.doRename(fileItem);
            });
        } else {
            this.doRename(fileItem);
        }
    }

    doRename(fileItem) {
        let renamePopup = new RenamePopup(fileItem);
        this._menuManager.addMenu(renamePopup);
        this._menuManager.ignoreRelease();

        renamePopup.connect('menu-closed', () => renamePopup.destroy());
        renamePopup.open();
    }
});

var RenamePopupMenuItem = GObject.registerClass(
class RenamePopupMenuItem extends PopupMenu.PopupBaseMenuItem {
    _init(fileItem) {
        super._init({
            style_class: 'rename-popup-item',
            reactive: false,
        });

        if (PopupMenu.Ornament.HIDDEN !== undefined)
            this.setOrnament(PopupMenu.Ornament.HIDDEN);
        else /* Support version prior 3.34.1 */
            this._ornamentLabel.visible = false;

        this._fileItem = fileItem;

        // Entry
        this._entry = new St.Entry({
            x_expand: true,
            width: 200,
        });
        this.add_child(this._entry);

        this._entry.clutter_text.connect(
            'notify::text', this._validate.bind(this));
        this._entry.clutter_text.connect(
            'activate', this._onRenameAccepted.bind(this));

        // Rename button
        this._button = new St.Button({
            style_class: 'button',
            reactive: true,
            button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
            can_focus: true,
            label: _('Rename'),
        });
        this.add_child(this._button);
        this._button.connect('clicked', this._onRenameAccepted.bind(this));
    }

    vfunc_map() {
        this._entry.text = this._fileItem.displayName;
        this._entry.clutter_text.set_selection(0, -1);
        super.vfunc_map();
    }

    vfunc_key_focus_in() {
        super.vfunc_key_focus_in();
        this._entry.clutter_text.grab_key_focus();
    }

    _isValidFolderName() {
        let newName = this._entry.text.trim();

        return newName.length > 0 && newName.indexOf('/') === -1 &&
               newName != this._fileItem.displayName;
    }

    _validate() {
        this._button.reactive = this._isValidFolderName();
    }

    _onRenameAccepted() {
        if (!this._isValidFolderName())
            return;

        DBusUtils.NautilusFileOperationsProxy.RenameFileRemote(
            this._fileItem.file.get_uri(),
            this._entry.text.trim(),
            (result, error) => {
                if (error)
                    throw new Error('Error renaming file: ' + error.message);
            }
        );

        this.activate(Clutter.get_current_event());
    }
});

var RenamePopup = class RenameFolderMenu extends PopupMenu.PopupMenu {
    constructor(fileItem) {
        super(fileItem, 0.5, St.Side.TOP);
        this.actor.add_style_class_name('rename-popup');

        // We want to keep the item hovered while the menu is up
        this.blockSourceEvents = true;

        let menuItem = new RenamePopupMenuItem(fileItem);
        this.addMenuItem(menuItem);

        if (this.focusActor !== undefined) {
            // Focus the text entry on menu pop-up, works starting 3.34.1
            this.focusActor = menuItem;
        } else {
            this.connect('open-state-changed', (_menu, state) => {
                if (state)
                    this._openId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                        delete this._openId;
                        menuItem.grab_key_focus()
                    });
                else if (this._openId)
                    GLib.source_remove(this._openId);
            });
        }

        // Chain our visibility and lifecycle to that of the fileItem source
        this._fileItemMappedId = fileItem.connect('notify::mapped', () => {
            if (!fileItem.mapped)
                this.close();
        });
        fileItem.connect('destroy', () => {
            fileItem.disconnect(this._fileItemMappedId);
            this.destroy();
        });

        Main.uiGroup.add_actor(this.actor);
    }
};
