iso-profiles-settings/tromjaro/gnome/live-overlay/etc/skel/.local/share/gnome-shell/extensions/desktop-icons@csoriano/desktopManager.js
2019-07-09 04:57:09 +03:00

753 lines
27 KiB
JavaScript

/* 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 GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Animation = imports.ui.animation;
const Background = imports.ui.background;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
const GrabHelper = imports.ui.grabHelper;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Extension = Me.imports.extension;
const DesktopGrid = Me.imports.desktopGrid;
const FileItem = Me.imports.fileItem;
const Prefs = Me.imports.prefs;
const DBusUtils = Me.imports.dbusUtils;
const DesktopIconsUtil = Me.imports.desktopIconsUtil;
const Clipboard = St.Clipboard.get_default();
const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD;
var S_IWOTH = 0x00002;
function getDpy() {
return global.screen || global.display;
}
function findMonitorIndexForPos(x, y) {
return getDpy().get_monitor_index_for_rect(new Meta.Rectangle({x, y}));
}
var DesktopManager = GObject.registerClass({
Properties: {
'writable-by-others': GObject.ParamSpec.boolean(
'writable-by-others',
'WritableByOthers',
'Whether the desktop\'s directory can be written by others (o+w unix permission)',
GObject.ParamFlags.READABLE,
false
)
}
}, class DesktopManager extends GObject.Object {
_init(params) {
super._init(params);
this._layoutChildrenId = 0;
this._deleteChildrenId = 0;
this._monitorDesktopDir = null;
this._desktopMonitorCancellable = null;
this._desktopGrids = {};
this._fileItemHandlers = new Map();
this._fileItems = new Map();
this._dragCancelled = false;
this._queryFileInfoCancellable = null;
this._unixMode = null;
this._writableByOthers = null;
this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', () => this._recreateDesktopIcons());
this._rubberBand = new St.Widget({ style_class: 'rubber-band' });
this._rubberBand.hide();
Main.layoutManager._backgroundGroup.add_child(this._rubberBand);
this._grabHelper = new GrabHelper.GrabHelper(global.stage);
this._addDesktopIcons();
this._monitorDesktopFolder();
this.settingsId = Prefs.settings.connect('changed', () => this._recreateDesktopIcons());
this.gtkSettingsId = Prefs.gtkSettings.connect('changed', (obj, key) => {
if (key == 'show-hidden')
this._recreateDesktopIcons();
});
this._selection = new Set();
this._currentSelection = new Set();
this._inDrag = false;
this._dragXStart = Number.POSITIVE_INFINITY;
this._dragYStart = Number.POSITIVE_INFINITY;
}
startRubberBand(x, y) {
this._rubberBandInitialX = x;
this._rubberBandInitialY = y;
this._initRubberBandColor();
this._updateRubberBand(x, y);
this._rubberBand.show();
this._grabHelper.grab({ actor: global.stage });
Extension.lockActivitiesButton = true;
this._stageReleaseEventId = global.stage.connect('button-release-event', (actor, event) => {
this.endRubberBand();
});
this._rubberBandId = global.stage.connect('motion-event', (actor, event) => {
/* In some cases, when the user starts a rubberband selection and ends it
* (by releasing the left button) over a window instead of doing it over
* the desktop, the stage doesn't receive the "button-release" event.
* This happens currently with, at least, Dash to Dock extension, but
* it probably also happens with other applications or extensions.
* To fix this, we also end the rubberband selection if we detect mouse
* motion in the stage without the left button pressed during a
* rubberband selection.
* */
let button = event.get_state();
if (!(button & Clutter.ModifierType.BUTTON1_MASK)) {
this.endRubberBand();
return;
}
[x, y] = event.get_coords();
this._updateRubberBand(x, y);
let x0, y0, x1, y1;
if (x >= this._rubberBandInitialX) {
x0 = this._rubberBandInitialX;
x1 = x;
} else {
x1 = this._rubberBandInitialX;
x0 = x;
}
if (y >= this._rubberBandInitialY) {
y0 = this._rubberBandInitialY;
y1 = y;
} else {
y1 = this._rubberBandInitialY;
y0 = y;
}
for (let [fileUri, fileItem] of this._fileItems) {
fileItem.emit('selected', true, true,
fileItem.intersectsWith(x0, y0, x1 - x0, y1 - y0));
}
});
}
endRubberBand() {
this._rubberBand.hide();
Extension.lockActivitiesButton = false;
this._grabHelper.ungrab();
global.stage.disconnect(this._rubberBandId);
global.stage.disconnect(this._stageReleaseEventId);
this._rubberBandId = 0;
this._stageReleaseEventId = 0;
this._selection = new Set([...this._selection, ...this._currentSelection]);
this._currentSelection.clear();
}
_updateRubberBand(currentX, currentY) {
let x = this._rubberBandInitialX < currentX ? this._rubberBandInitialX
: currentX;
let y = this._rubberBandInitialY < currentY ? this._rubberBandInitialY
: currentY;
let width = Math.abs(this._rubberBandInitialX - currentX);
let height = Math.abs(this._rubberBandInitialY - currentY);
/* TODO: Convert to gobject.set for 3.30 */
this._rubberBand.set_position(x, y);
this._rubberBand.set_size(width, height);
}
_recreateDesktopIcons() {
this._destroyDesktopIcons();
this._addDesktopIcons();
}
_addDesktopIcons() {
forEachBackgroundManager(bgManager => {
let newGrid = new DesktopGrid.DesktopGrid(bgManager);
newGrid.actor.connect('destroy', (actor) => {
// if a grid loses its actor, remove it from the grid list
for (let grid in this._desktopGrids)
if (this._desktopGrids[grid].actor == actor) {
delete this._desktopGrids[grid];
break;
}
});
this._desktopGrids[bgManager._monitorIndex] = newGrid;
});
this._scanFiles();
}
_destroyDesktopIcons() {
Object.values(this._desktopGrids).forEach(grid => grid.actor.destroy());
this._desktopGrids = {};
}
/**
* Initialize rubberband color from the GTK rubberband class
* */
_initRubberBandColor() {
let rgba = DesktopIconsUtil.getGtkClassBackgroundColor('rubberband', Gtk.StateFlags.NORMAL);
let background_color =
'rgba(' + rgba.red * 255 + ', ' + rgba.green * 255 + ', ' + rgba.blue * 255 + ', 0.4)';
this._rubberBand.set_style('background-color: ' + background_color);
}
async _scanFiles() {
for (let [fileItem, id] of this._fileItemHandlers)
fileItem.disconnect(id);
this._fileItemHandlers = new Map();
if (!this._unixMode) {
let desktopDir = DesktopIconsUtil.getDesktopDir();
let fileInfo = desktopDir.query_info(Gio.FILE_ATTRIBUTE_UNIX_MODE,
Gio.FileQueryInfoFlags.NONE,
null);
this._unixMode = fileInfo.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE);
this._setWritableByOthers((this._unixMode & S_IWOTH) != 0);
}
try {
let tmpFileItems = new Map();
for (let [file, info, extra] of await this._enumerateDesktop()) {
let fileItem = new FileItem.FileItem(file, info, extra);
tmpFileItems.set(fileItem.file.get_uri(), fileItem);
let id = fileItem.connect('selected',
this._onFileItemSelected.bind(this));
this._fileItemHandlers.set(fileItem, id);
}
this._fileItems = tmpFileItems;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log(`Error loading desktop files ${e.message}`);
return;
}
this.scheduleReLayoutChildren();
}
getDesktopFileNames () {
let fileList = [];
for (let [uri, item] of this._fileItems) {
fileList.push(item.fileName);
}
return fileList;
}
_enumerateDesktop() {
return new Promise((resolve, reject) => {
if (this._desktopEnumerateCancellable)
this._desktopEnumerateCancellable.cancel();
this._desktopEnumerateCancellable = new Gio.Cancellable();
let desktopDir = DesktopIconsUtil.getDesktopDir();
desktopDir.enumerate_children_async(DesktopIconsUtil.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
this._desktopEnumerateCancellable,
(source, result) => {
try {
let fileEnum = source.enumerate_children_finish(result);
let resultGenerator = function *() {
let info;
for (let [newFolder, extras] of DesktopIconsUtil.getExtraFolders()) {
yield [newFolder, newFolder.query_info(DesktopIconsUtil.DEFAULT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, this._desktopEnumerateCancellable), extras];
}
while ((info = fileEnum.next_file(null)))
yield [fileEnum.get_child(info), info, Prefs.FileType.NONE];
}.bind(this);
resolve(resultGenerator());
} catch (e) {
reject(e);
}
});
});
}
_monitorDesktopFolder() {
if (this._monitorDesktopDir) {
this._monitorDesktopDir.cancel();
this._monitorDesktopDir = null;
}
let desktopDir = DesktopIconsUtil.getDesktopDir();
this._monitorDesktopDir = desktopDir.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, null);
this._monitorDesktopDir.set_rate_limit(1000);
this._monitorDesktopDir.connect('changed', (obj, file, otherFile, eventType) => this._updateDesktopIfChanged(file, otherFile, eventType));
}
checkIfSpecialFilesAreSelected() {
for (let fileItem of this._selection) {
if (fileItem.isSpecial)
return true;
}
return false;
}
getNumberOfSelectedItems() {
return this._selection.size;
}
get writableByOthers() {
return this._writableByOthers;
}
_setWritableByOthers(value) {
if (value == this._writableByOthers)
return;
this._writableByOthers = value
this.notify('writable-by-others');
}
_updateDesktopIfChanged (file, otherFile, eventType) {
let {
DELETED, MOVED_IN, MOVED_OUT, CREATED, RENAMED, CHANGES_DONE_HINT, ATTRIBUTE_CHANGED
} = Gio.FileMonitorEvent;
let fileUri = file.get_uri();
let fileItem = null;
if (this._fileItems.has(fileUri))
fileItem = this._fileItems.get(fileUri);
switch(eventType) {
case RENAMED:
this._fileItems.delete(fileUri);
this._fileItems.set(otherFile.get_uri(), fileItem);
fileItem.onFileRenamed(otherFile);
return;
case CHANGES_DONE_HINT:
case ATTRIBUTE_CHANGED:
/* a file changed, rather than the desktop itself */
let desktopDir = DesktopIconsUtil.getDesktopDir();
if (file.get_uri() != desktopDir.get_uri())
return;
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
file.query_info_async(Gio.FILE_ATTRIBUTE_UNIX_MODE,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
this._queryFileInfoCancellable,
(source, result) => {
try {
let info = source.query_info_finish(result);
this._queryFileInfoCancellable = null;
this._unixMode = info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE);
this._setWritableByOthers((this._unixMode & S_IWOTH) != 0);
if (this._writableByOthers)
log(`desktop-icons: Desktop is writable by others - will not allow launching any desktop files`);
} catch(error) {
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
global.log('Error getting desktop unix mode: ' + error);
}
});
return;
}
// Only get a subset of events we are interested in.
// Note that CREATED will emit a CHANGES_DONE_HINT
if (![DELETED, MOVED_IN, MOVED_OUT, CREATED].includes(eventType))
return;
this._recreateDesktopIcons();
}
_setupDnD() {
this._draggableContainer = new St.Widget({
visible: true,
width: 1,
height: 1,
x: 0,
y: 0,
style_class: 'draggable'
});
this._draggableContainer._delegate = this;
this._draggable = DND.makeDraggable(this._draggableContainer,
{
manualMode: true,
dragActorOpacity: 100
});
this._draggable.connect('drag-cancelled', () => this._onDragCancelled());
this._draggable.connect('drag-end', () => this._onDragEnd());
this._draggable._dragActorDropped = event => this._dragActorDropped(event);
}
dragStart() {
if (this._inDrag) {
return;
}
this._setupDnD();
let event = Clutter.get_current_event();
let [x, y] = event.get_coords();
[this._dragXStart, this._dragYStart] = event.get_coords();
this._inDrag = true;
for (let fileItem of this._selection) {
let clone = new Clutter.Clone({
source: fileItem.actor,
reactive: false
});
clone.x = fileItem.actor.get_transformed_position()[0];
clone.y = fileItem.actor.get_transformed_position()[1];
this._draggableContainer.add_child(clone);
}
Main.layoutManager.uiGroup.add_child(this._draggableContainer);
this._draggable.startDrag(x, y, global.get_current_time(), event.get_event_sequence());
}
_onDragCancelled() {
let event = Clutter.get_current_event();
let [x, y] = event.get_coords();
this._dragCancelled = true;
}
_onDragEnd() {
this._inDrag = false;
Main.layoutManager.uiGroup.remove_child(this._draggableContainer);
}
_dragActorDropped(event) {
let [dropX, dropY] = event.get_coords();
let target = this._draggable._dragActor.get_stage().get_actor_at_pos(Clutter.PickMode.ALL,
dropX, dropY);
// We call observers only once per motion with the innermost
// target actor. If necessary, the observer can walk the
// parent itself.
let dropEvent = {
dropActor: this._draggable._dragActor,
targetActor: target,
clutterEvent: event
};
for (let dragMonitor of DND.dragMonitors) {
let dropFunc = dragMonitor.dragDrop;
if (dropFunc)
switch (dropFunc(dropEvent)) {
case DragDropResult.FAILURE:
case DragDropResult.SUCCESS:
return true;
case DragDropResult.CONTINUE:
continue;
}
}
// At this point it is too late to cancel a drag by destroying
// the actor, the fate of which is decided by acceptDrop and its
// side-effects
this._draggable._dragCancellable = false;
let destroyActor = false;
while (target) {
if (target._delegate && target._delegate.acceptDrop) {
let [r, targX, targY] = target.transform_stage_point(dropX, dropY);
if (target._delegate.acceptDrop(this._draggable.actor._delegate,
this._draggable._dragActor,
targX,
targY,
event.get_time())) {
// If it accepted the drop without taking the actor,
// handle it ourselves.
if (this._draggable._dragActor.get_parent() == Main.uiGroup) {
if (this._draggable._restoreOnSuccess) {
this._draggable._restoreDragActor(event.get_time());
return true;
}
else {
// We need this in order to make sure drag-end is fired
destroyActor = true;
}
}
this._draggable._dragInProgress = false;
getDpy().set_cursor(Meta.Cursor.DEFAULT);
this._draggable.emit('drag-end', event.get_time(), true);
if (destroyActor) {
this._draggable._dragActor.destroy();
}
this._draggable._dragComplete();
return true;
}
}
target = target.get_parent();
}
this._draggable._cancelDrag(event.get_time());
return true;
}
acceptDrop(xEnd, yEnd) {
let savedCoordinates = new Map();
let [xDiff, yDiff] = [xEnd - this._dragXStart, yEnd - this._dragYStart];
/* Remove all items before dropping new ones, so we can freely reposition
* them.
*/
for (let item of this._selection) {
let [itemX, itemY] = item.actor.get_transformed_position();
let monitorIndex = findMonitorIndexForPos(itemX, itemY);
savedCoordinates.set(item, [itemX, itemY]);
this._desktopGrids[monitorIndex].removeFileItem(item);
}
for (let item of this._selection) {
let [itemX, itemY] = savedCoordinates.get(item);
/* Set the new ideal position where the item drop should happen */
let newFileX = Math.round(xDiff + itemX);
let newFileY = Math.round(yDiff + itemY);
let monitorIndex = findMonitorIndexForPos(newFileX, newFileY);
this._desktopGrids[monitorIndex].addFileItemCloseTo(item, newFileX, newFileY, DesktopGrid.StoredCoordinates.OVERWRITE);
}
return true;
}
selectionDropOnFileItem (fileItemDestination) {
if (!fileItemDestination.isDirectory)
return false;
let droppedUris = [];
for (let fileItem of this._selection) {
if (fileItem.isSpecial)
return false;
if (fileItemDestination.file.get_uri() == fileItem.file.get_uri())
return false;
droppedUris.push(fileItem.file.get_uri());
}
if (droppedUris.length == 0)
return true;
DBusUtils.NautilusFileOperationsProxy.MoveURIsRemote(droppedUris,
fileItemDestination.file.get_uri(),
(result, error) => {
if (error)
throw new Error('Error moving files: ' + error.message);
}
);
for (let fileItem of this._selection) {
fileItem.state = FileItem.State.GONE;
}
this._recreateDesktopIcons();
return true;
}
_resetGridsAndScheduleLayout() {
this._deleteChildrenId = 0;
Object.values(this._desktopGrids).forEach((grid) => grid.reset());
this._layoutChildrenId = GLib.idle_add(GLib.PRIORITY_LOW, () => this._layoutChildren());
return GLib.SOURCE_REMOVE;
}
scheduleReLayoutChildren() {
if (this._deleteChildrenId != 0)
return;
if (this._layoutChildrenId != 0) {
GLib.source_remove(this._layoutChildrenId);
this._layoutChildrenId = 0;
}
this._deleteChildrenId = GLib.idle_add(GLib.PRIORITY_LOW, () => this._resetGridsAndScheduleLayout());
}
_addFileItemCloseTo(item) {
let coordinates;
let x = 0;
let y = 0;
let coordinatesAction = DesktopGrid.StoredCoordinates.ASSIGN;
if (item.savedCoordinates != null) {
[x, y] = item.savedCoordinates;
coordinatesAction = DesktopGrid.StoredCoordinates.PRESERVE;
}
let monitorIndex = findMonitorIndexForPos(x, y);
let desktopGrid = this._desktopGrids[monitorIndex];
try {
desktopGrid.addFileItemCloseTo(item, x, y, coordinatesAction);
} catch (e) {
log(`Error adding children to desktop: ${e.message}`);
}
}
_layoutChildren() {
let showHidden = Prefs.gtkSettings.get_boolean('show-hidden');
/*
* Paint the icons in two passes:
* * first pass paints those that have their coordinates defined in the metadata
* * second pass paints those new files that still don't have their definitive coordinates
*/
for (let [fileUri, fileItem] of this._fileItems) {
if (fileItem.savedCoordinates == null)
continue;
if (fileItem.state != FileItem.State.NORMAL)
continue;
if (!showHidden && fileItem.isHidden)
continue;
this._addFileItemCloseTo(fileItem);
}
for (let [fileUri, fileItem] of this._fileItems) {
if (fileItem.savedCoordinates !== null)
continue;
if (fileItem.state != FileItem.State.NORMAL)
continue;
if (!showHidden && fileItem.isHidden)
continue;
this._addFileItemCloseTo(fileItem);
}
this._layoutChildrenId = 0;
return GLib.SOURCE_REMOVE;
}
doRename() {
if (this._selection.size != 1)
return;
let item = [...this._selection][0];
if (item.canRename())
item.doRename();
}
doOpen() {
for (let fileItem of this._selection)
fileItem.doOpen();
}
doTrash() {
DBusUtils.NautilusFileOperationsProxy.TrashFilesRemote([...this._selection].map((x) => { return x.file.get_uri(); }),
(source, error) => {
if (error)
throw new Error('Error trashing files on the desktop: ' + error.message);
}
);
}
doEmptyTrash() {
DBusUtils.NautilusFileOperationsProxy.EmptyTrashRemote( (source, error) => {
if (error)
throw new Error('Error trashing files on the desktop: ' + error.message);
});
}
_onFileItemSelected(fileItem, keepCurrentSelection, rubberBandSelection, addToSelection) {
if (!keepCurrentSelection && !this._inDrag)
this.clearSelection();
let selection = keepCurrentSelection && rubberBandSelection ? this._currentSelection : this._selection;
if (addToSelection)
selection.add(fileItem);
else
selection.delete(fileItem);
for (let [fileUri, fileItem] of this._fileItems)
fileItem.isSelected = this._currentSelection.has(fileItem) || this._selection.has(fileItem);
}
clearSelection() {
for (let [fileUri, fileItem] of this._fileItems)
fileItem.isSelected = false;
this._selection = new Set();
this._currentSelection = new Set();
}
_getClipboardText(isCopy) {
let action = isCopy ? 'copy' : 'cut';
let text = `x-special/nautilus-clipboard\n${action}\n${
[...this._selection].map(s => s.file.get_uri()).join('\n')
}\n`;
return text;
}
doCopy() {
Clipboard.set_text(CLIPBOARD_TYPE, this._getClipboardText(true));
}
doCut() {
Clipboard.set_text(CLIPBOARD_TYPE, this._getClipboardText(false));
}
destroy() {
if (this._monitorDesktopDir)
this._monitorDesktopDir.cancel();
this._monitorDesktopDir = null;
if (this.settingsId)
Prefs.settings.disconnect(this.settingsId);
this.settingsId = 0;
if (this.gtkSettingsId)
Prefs.gtkSettings.disconnect(this.gtkSettingsId);
this.gtkSettingsId = 0;
if (this._layoutChildrenId)
GLib.source_remove(this._layoutChildrenId);
this._layoutChildrenId = 0;
if (this._deleteChildrenId)
GLib.source_remove(this._deleteChildrenId);
this._deleteChildrenId = 0;
if (this._monitorsChangedId)
Main.layoutManager.disconnect(this._monitorsChangedId);
this._monitorsChangedId = 0;
if (this._stageReleaseEventId)
global.stage.disconnect(this._stageReleaseEventId);
this._stageReleaseEventId = 0;
if (this._rubberBandId)
global.stage.disconnect(this._rubberBandId);
this._rubberBandId = 0;
this._rubberBand.destroy();
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
Object.values(this._desktopGrids).forEach(grid => grid.actor.destroy());
this._desktopGrids = {}
}
});
function forEachBackgroundManager(func) {
Main.layoutManager._bgManagers.forEach(func);
}