Files
system-taskbar/ding/app/desktopManager.js

1875 lines
62 KiB
JavaScript

/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022-25 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
* Based on code original (C) Carlos Soriano
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
AppChooser,
AskRenamePopup,
AutoAr,
DesktopMenu,
DesktopMonitor,
DragManager,
FileItemMenu,
GnomeShellDragDrop,
ShortcutManager,
ShowErrorPopup,
StackItem,
TemplatesScriptsManager,
WindowManager,
WidgetManager
} from '../dependencies/localFiles.js';
import {Adw, Gtk, Gdk, Gio, GLib, GLibUnix} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {DesktopManager};
const DesktopManager = class {
constructor(Data, Utils, desktopList, codePath, asDesktop, primaryIndex) {
// Inherit
this.mainApp = Data.mainApp;
this.codePath = codePath;
this._asDesktop = asDesktop;
if (asDesktop) {
// Don't close the application if there are no desktops
this.mainApp.hold();
this._hold_active = true;
}
this.GnomeShellVersion = Data.gnomeversion;
this.uuid = Data.uuid;
// Init and import Scripts and classes
this.DesktopIconsUtil = Utils.DesktopIconsUtil;
this.FileUtils = Utils.FileUtils;
this.Enums = Data.Enums;
this.DBusUtils = Utils.DBusUtils;
this.dbusManager = Utils.DBusUtils.dbusManagerObject;
this.Prefs = Utils.Preferences;
this.showErrorPopup = ShowErrorPopup;
this.templatesScriptsManager = TemplatesScriptsManager;
this.GnomeShellDragDrop = GnomeShellDragDrop;
this.appChooser = AppChooser;
this.ThumbnailLoader = Utils.ThumbnailLoader;
// init methods
this.dragManager = new DragManager.DragManager(this);
this.windowManager = new WindowManager.WindowManager(this,
desktopList,
asDesktop,
primaryIndex
);
this.desktopMonitor = new DesktopMonitor.DesktopMonitor(this);
this.autoAr = new AutoAr.AutoAr(this);
this.fileItemMenu = new FileItemMenu.FileItemMenu(this);
this.fileItemActions = new FileItemMenu.FileItemActions(this);
this.desktopActions = new DesktopMenu.DesktopActions(this);
this.desktopMenuManager = new DesktopMenu.DesktopBackgroundMenu(this);
this.Prefs.init(this);
this.shortcutManager = new ShortcutManager(this);
this.widgetManager = new WidgetManager.WidgetManager(this);
// Init Variables
this._clickX = null;
this._clickY = null;
this._compositeStackList = null;
this._displayList = [];
// Track last seen keyboard modifier state so accelerators can query it
this._lastModifierState = 0;
this.ignoreKeys = this.Enums.IgnoreKeys.map(_k => Gdk[_k]);
// setup gracefull termination
if (this._asDesktop) {
this._sigtermID = GLibUnix.signal_add_full(
GLib.PRIORITY_DEFAULT,
15,
() => {
GLib.source_remove(this._sigtermID);
this.terminateProgram();
if (this._hold_active) {
this.mainApp.release();
this._hold_active = false;
}
return false;
}
);
}
this._syncStartupDesktop().catch(e => logError(e));
}
async _syncStartupDesktop() {
// startup in a particular order
// First create and make sure windows are created
const windowscreated = new Promise(resolve => {
this.windowsPromiseResolve = resolve;
this.windowManager.createGridWindows().catch(e => logError(e));
// If this desktop List is null, ask for a new one
this.windowManager.requestGeometryUpdate();
});
// Monitor is attached, windows are created with proper geometry
await windowscreated.catch(e => logError(e));
// Now we can actually display errors, so check for them
// Ensure that there is a 'Desktop' folder set and it exists
// Verify if Gnome Files is available and executable, otherwise warn
// Confirm Gnome Files is registered with xdg-utils
// to handle inode/directory
this._performSanityChecks().catch(e => logError(e));
// The initialRead parameter insures tha grid positions are recalculated
// and recaculated postions of all fileItems will be re-written to
// disk with write mode 'OVERWRITE'
const initialRead = true;
// prior fileList, even if triggered through desktopdir changes
// will not be displayed as windows were not there.
const fileList = await this.desktopMonitor.getFileList();
// This is no longer needed, if true it blocks and all updates.
this.windowsPromiseResolve = null;
await this._drawDesktop(fileList, {initialRead}).catch(e => logError(e));
// First intitiation complete, valid file read from
// desktopdir, even if a prior fileList was read, the
// forced new read will recalculate and resave new
// normalized coordinates and monitor information.
this._startWidgetDisplay();
// force a queue draw of all windows now that we have drawn the desktop,
// and poke mutter to map the meta window.
this.windowManager.queue_draw();
}
async _performSanityChecks() {
// show error if monitor frame buffer scaling is not enabled first,
// as windows may be awry
if (this.windowManager.differentZooms &&
!this.Prefs.usingX11 &&
!this.fractionalScaling &&
!this._framebufferWarningDone) {
const header = _('Monitor Frame Buffer Scaling is not enabled');
const text = _('Multiple monitors with different zoom settings, recommend per monitor framebuffer scaling.\n\nPlease enable in Mutter Dconf Settings');
// show notification as well as error dialog as windows may
// not be postioned correctly
this.dbusManager.doNotify(header, text);
this._framebufferWarningDone = true;
const window = this.mainApp.get_active_window();
const dialog = new Adw.AlertDialog();
dialog.set_body_use_markup(true);
dialog.set_heading_use_markup(true);
dialog.set_heading(header);
const secondaryText = _('Multiple monitors with different zoom settings.\n\nEnable per monitor framebuffer scaling in Mutter Dconf Settings?');
dialog.set_body(secondaryText);
dialog.add_response('cancel', _('Cancel'));
dialog.add_response('enable', _('Enable'));
dialog.set_close_response('cancel');
dialog.set_default_response('enable');
dialog.set_response_appearance(
'enable', Adw.ResponseAppearance.SUGGESTED);
dialog.set_response_appearance(
'cancel', Adw.ResponseAppearance.DEFAULT);
dialog.set_prefer_wide_layout(true);
const runDialog = new Promise(resolve => {
dialog.choose(window, null, (actor, asyncResult) => {
const response = actor.choose_finish(asyncResult);
if (response === 'enable')
this.Prefs.fractionalScaling = true;
dialog.close();
resolve(response);
});
});
await runDialog;
}
const fileType = this._desktopDir.query_file_type(
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null);
if (fileType === Gio.FileType.SYMBOLIC_LINK) {
const header = _('Can Not Show the Desktop');
// TRANSLATORS: {desktop} folder path automatically inserted
const text = _('The Desktop folder {desktop} is a Symbolic Link\n\nPlease set the Desktop Folder to a real Folder');
const errorDialog = this.showError(
header,
text.replace('{desktop}', this._desktopDir.get_path())
);
await errorDialog.run();
this._desktops.forEach(d => d.setErrorState());
} else if (fileType !== Gio.FileType.DIRECTORY) {
const header = _('Can Not Show the Desktop');
// TRANSLATORS: {desktop} folder path automatically inserted
const text = _('The Desktop folder {desktop} does not exist, or is not a Directory\n\nCheck your xdg-utils installation and set the correct Desktop Folder');
const errorDialog = this.showError(
header,
text.replace('{desktop}', this._desktopDir.get_path())
);
await errorDialog.run();
this._desktops.forEach(d => d.setErrorState());
}
const inodeHandlers = Gio.AppInfo.get_all_for_type('inode/directory');
if (!GLib.find_program_in_path('nautilus')) {
const header = _('GNOME Files not found');
const text = _('The GNOME Files application is required by Gtk4 Desktop Icons NG.');
const errorDialog = this.showError(header, text);
await errorDialog.run();
}
if (!inodeHandlers.length) {
const helpURL = 'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
const header = _('There is no default File Manager');
const text = _('There is no application that handles mimetype "inode/directory"');
const errorDialog = this.showError(header, text, helpURL);
await errorDialog.run();
}
if (!inodeHandlers.map(a => a.get_id()).includes('org.gnome.Nautilus.desktop')) {
const helpURL = 'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
const header = _('Gnome Files is not registered as a File Manager');
const text = _('The Gnome Files application is not programmed to open Folders!\nCheck your xdg-utils installation\nCheck Gnome Files .desktop File installation');
const errorDialog = this.showError(header, text, helpURL);
await errorDialog.run();
}
}
showError(text, secondaryText, helpURL = null, timeout = 0) {
const errorDialog = new ShowErrorPopup.ShowErrorPopup(
text,
secondaryText,
this.DesktopIconsUtil.waitDelayMs,
helpURL
);
if (timeout)
errorDialog.runAutoClose(timeout);
return errorDialog;
}
terminateProgram() {
this.desktopMonitor.stopMonitoring();
if (this._dbusGeometryIface)
this._dbusGeometryIface.unexport();
if (this._compositeStackList && this._compositeStackList.length) {
this._displayList.forEach(f => {
if (f.isStackMarker)
f.onDestroy();
});
this._compositeStackList.forEach(f => f.onDestroy());
} else {
this._displayList.forEach(f => f.onDestroy());
}
this._stopWidgetDisplay();
this.windowManager.destroyDesktops();
}
// Keyboard and Mouse Events
onPressButton(X, Y, x, y, button, shiftPressed, controlPressed) {
this._clickX = Math.floor(X);
this._clickY = Math.floor(Y);
// Left Click
if (button === 1) {
if (!shiftPressed && !controlPressed) {
// clear selection
this.unselectAll();
}
this.dragManager.startRubberband(X, Y);
}
}
async onReleaseButton(X, Y, x, y, button, isShift, isCtrl, grid) {
// Right Click
if (button === 3) {
await this.desktopMenuManager
.showDesktopMenu(x, y, grid)
.catch(e => logError(e));
}
}
onLongPressButton(_X, _Y, _x, _y, button, _isShift, _isCtrl, _grid) {
if (button === 3)
this.mainApp.activate_action('displayShellBackgroundMenu', null);
}
onWidgetDisplayChanged() {
if (this.Prefs.showDesktopWidgets)
this._startWidgetDisplay();
else
this._stopWidgetDisplay();
}
_startWidgetDisplay() {
this.widgetManager
.startWidgetDisplay(this._desktops, {redisplay: true})
.catch(e => logError(e));
}
_stopWidgetDisplay() {
this.widgetManager.stopWidgetDisplay();
}
onKeyPress(keyval, keycode, state, grid) {
this.keyEventGrid = grid;
if (this.popupmenu || this.fileItemMenu.popupmenu)
return true;
if (this.ignoreKeys.includes(keyval))
return true;
let key = String.fromCharCode(Gdk.keyval_to_unicode(keyval));
if (this.keypressTimeoutID && this.searchString)
this.searchString = this.searchString.concat(key);
else
this.searchString = key;
if (this.searchString !== '') {
let found = this._scanForFiles(this.searchString, false);
if (found) {
if ((this.getNumberOfSelectedItems() >= 1) &&
!this.keypressTimeoutID) {
const secondaryText = null;
const helpURL = null;
const timeoutClose = 2000; // In ms
this.showError(
_('Clear current selection before new search'),
secondaryText,
helpURL,
timeoutClose
);
return true;
}
this.searchEventTime = GLib.get_monotonic_time();
if (!this.keypressTimeoutID) {
this.keypressTimeoutID =
GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
1000,
() => {
if (GLib.get_monotonic_time() -
this.searchEventTime < 1500000)
return true;
this.searchString = null;
this.keypressTimeoutID = null;
if (this._findFileWindow) {
this._findFileWindow
.response(Gtk.ResponseType.OK);
}
return false;
});
}
this.findFiles(this.searchString);
}
return true;
} else {
return false;
}
}
updateModifierState(state) {
// cache modifier state for use by accelerator-driven handlers
this._lastModifierState = state;
}
clearModifierState() {
// reset cached modifier state
this._lastModifierState = 0;
}
closePopUps() {
if (this._renameWindow) {
this._renameWindow.close();
return true;
}
if (this.dialogCancellable) {
this.dialogCancellable.cancel();
this.dialogCancellable = null;
return true;
}
return false;
}
clearAllLayersFromGrids() {
// Icons: clear from all grids
this._displayList.forEach(x => x.removeFromGrid());
// Widgets: clear from all grids
this.widgetManager.clearFromGrids();
}
async applyDesktopLayoutChange({redisplay, monitorschanged, gridschanged}) {
this._performSanityChecks();
// Icons first
await this.reFrameDesktop({
redisplay,
monitorschanged,
gridschanged,
});
await this.widgetManager.applyLayoutChange(this._desktops, {redisplay});
}
// Destktop Icon Placement and Display
// ********************************************************************** */
_removeAllFilesFromGrids() {
for (let fileItem of this._displayList)
fileItem.removeFromGrid({callOnDestroy: true});
this._displayList = [];
}
_clearAllFilesFromGrids() {
for (let fileItem of this._displayList)
fileItem.removeFromGrid({callOnDestroy: false});
this._displayList = [];
}
async _drawDesktop(fileList, opts = {initialRead: false}) {
if (this.windowsPromiseResolve || !fileList)
return;
const selectedFiles = this.getCurrentSelectionAsUri();
// Update the Icon before placing on Desktop prevent flickering Icons //
const updateUI = fileList.map(async fileItem => {
await fileItem.updateIcon();
if (selectedFiles) {
if (selectedFiles.includes(fileItem.uri))
fileItem.setSelected();
}
});
await Promise.all([...updateUI]);
//* Remove all files from the grids just before placing new files to
// prevent flickering icons *//
if (opts.initialRead)
this._removeAllFilesFromGrids();
else
this._clearAllFilesFromGrids();
this._displayList = fileList;
this._placeAllFilesOnGrids(opts);
//* Detect all Icon sizes are allocated and Icons are now shown and
// placed on Grid. Desktop draw/paint is now complete *//
const drawComplete = this._displayList.map(async fileItem => {
await fileItem.iconPlaced;
});
await Promise.all([...drawComplete]);
//* Reposition open Menus, renameFileItem pop up's **//
//* Any task after complete desktop draw can now be done *//
this._refreshMenus();
}
_refreshMenus() {
if ((this.newItemDoRename && this.newItemDoRename.size) ||
this.fileItemMenu.popupmenu ||
this.activeFileItem) {
let activeItem = false;
let newItemDoRename = false;
this._displayList.forEach(f => {
if (this.activeFileItem &&
(f.fileName === this.activeFileItem.fileName))
this.activeFileItem = activeItem = f;
if (this.newItemDoRename &&
this.newItemDoRename.has(f.fileName))
newItemDoRename = f;
});
if (this._renameWindow)
this._renameWindow.close();
if (newItemDoRename) {
newItemDoRename.setSelected();
const allowReturnOnSameName = true;
this.doRename(newItemDoRename, allowReturnOnSameName)
.catch(e => logError(e));
}
if (this.fileItemMenu.popupmenu) {
if (!activeItem)
this.fileItemMenu.popupmenu.popdown();
}
if (!activeItem)
this.activeFileItem = null;
}
}
_placeAllFilesOnGrids(opts = {redisplay: false}) {
if (this.Prefs.keepStacked) {
this.doStacks(opts);
return;
}
if (this.Prefs.keepArranged) {
this.doSorts(opts);
return;
}
let storeMode = this.Enums.StoredCoordinates.PRESERVE;
if (opts.redisplay ||
opts.initialRead) {
// write the new recomputed positions to metadata when assigned
storeMode = this.Enums.StoredCoordinates.OVERWRITE;
this._sortByCurrentPosition();
this._recomputeWindowPositions();
}
if (opts.gridschanged && !this.Prefs.freePositionIcons) {
// if snap to grid, recompute column, row for fileItems
// so they end up in the same relative grid, otherwise they keep
// shifting postions. This keeps them in the same relative grid
// position. For snap to grid this will apply the new global x,y of
// the grid assigned
this._recomputeGridPositions();
}
this._addFilesToDesktop(this._displayList, storeMode);
}
_recomputeGridPositions(fileList) {
if (!fileList)
fileList = this._displayList;
fileList.forEach(fileItem => {
if (fileItem.savedCoordinates === null)
return;
if (fileItem._monitorIndex == null)
return;
const column = fileItem.column;
const row = fileItem.row;
if (column == null || row == null)
return;
const index = fileItem._monitorIndex;
const [desktop] = this._desktops.filter(d => {
return d.monitorIndex === index;
});
if (!desktop)
return;
const [newGlobalX, newGlobalY] =
desktop.recomputeGridPosition(column, row);
fileItem.temporarySavedPosition = [newGlobalX + 2, newGlobalY + 2];
});
}
_recomputeWindowPositions(fileList) {
if (!fileList)
fileList = this._displayList;
if (!this._desktops.length)
return;
fileList.forEach(fileItem => {
if (fileItem.savedCoordinates == null)
return;
if (fileItem._normalCoordinates == null) {
fileItem.savedCoordinates = null;
return;
}
if (fileItem._monitorIndex == null)
return;
const itemMonitorIndex = fileItem._monitorIndex;
let desktop;
// reassign to monitors
// if on primary monitor, reassign to new primary
if (itemMonitorIndex === this._priorPrimaryMonitorIndex &&
this._primaryMonitorIndex != null) {
if (!this.Prefs.showOnSecondaryMonitor) {
[desktop] = this._desktops.filter(d => {
return d.monitorIndex === this._primaryMonitorIndex;
});
} else {
desktop = this.preferredDisplayDesktop;
}
}
// reassign not on primary monitor to prior monitor if
// if the prior monitor is still in index
if (!desktop) {
[desktop] = this._desktops.filter(d => {
return d.monitorIndex === itemMonitorIndex;
});
}
// reassingn to new monitor, prior monitor not available
if (!desktop)
desktop = this.preferredDisplayDesktop;
// if any error, leave unmapped to new monitor, placement algorithm
// will find placement from the old global position
if (!desktop)
return;
fileItem.temporaryMonitorIndex = desktop.monitorIndex;
// recompute coordinates for the new monitor
const x = fileItem._normalCoordinates[0];
const y = fileItem._normalCoordinates[1];
const [newlocalX, newlocalY] =
desktop.setNormalizedCoordinates(x, y);
const [newGlobalX, newGlobalY] =
desktop.coordinatesLocalToGlobal(newlocalX, newlocalY);
fileItem.temporarySavedPosition = [newGlobalX, newGlobalY];
});
}
_addFilesToDesktop(fileList, storeMode) {
let preferredDesktop = this.preferredDisplayDesktop;
if (!preferredDesktop)
return;
let outOfDesktops = [];
let notAssignedYet = [];
let droppedFiles = [];
// First, add those icons that have saved coordinates
// and fit in the current desktops
for (let fileItem of fileList) {
if (fileItem.savedCoordinates === null) {
if (fileItem.dropCoordinates !== null)
droppedFiles.push(fileItem);
else
notAssignedYet.push(fileItem);
continue;
}
if (fileItem.dropCoordinates !== null)
fileItem.dropCoordinates = null;
let [itemX, itemY] = fileItem.savedCoordinates;
let addedToDesktop = false;
for (let desktop of this._desktops) {
if (desktop
.coordinatesBelongToThisGridWindow(itemX, itemY) &&
desktop.isAvailable()) {
addedToDesktop = true;
desktop
.addFileItemCloseTo(fileItem, itemX, itemY, storeMode);
break;
}
}
if (!addedToDesktop)
outOfDesktops.push(fileItem);
}
// Now, assign icons that have landed in changed margins, belong to
// monitor and the window, however no longer fit on the grid as
// they overlap margins.
if (outOfDesktops.length) {
this._addFilesCloseToAssignedDesktop(
outOfDesktops,
storeMode,
preferredDesktop
);
}
outOfDesktops = [];
// Now assign those icons that have dropped coordinates
for (let fileItem of droppedFiles) {
let [x, y] = fileItem.dropCoordinates;
storeMode = this.Enums.StoredCoordinates.OVERWRITE;
let addedToDesktop = false;
for (let desktop of this._desktops) {
if (desktop.coordinatesBelongToThisGrid(x, y) &&
desktop.isAvailable()) {
fileItem.dropCoordinates = null;
desktop.addFileItemCloseTo(fileItem, x, y, storeMode);
addedToDesktop = true;
break;
}
}
if (!addedToDesktop)
outOfDesktops.push(fileItem);
}
// Now, try again assign those icons that had dropped coordinates and
// did not fit on dropped desktop, to the preferred or closest desktop
if (outOfDesktops.length) {
this._addFilesCloseToAssignedDesktop(
outOfDesktops,
storeMode,
preferredDesktop
);
outOfDesktops = [];
}
// Finally, assign coordinates of preferred desktop to those new icons
// that still don't have coordinates and place on preferred desktop or
// the next closest one
for (let fileItem of notAssignedYet) {
let x = preferredDesktop.gridGlobalRectangle.x;
let y = preferredDesktop.gridGlobalRectangle.y;
storeMode = this.Enums.StoredCoordinates.ASSIGN;
// try first in the designated desktop
let assigned = false;
if (preferredDesktop.coordinatesBelongToThisGrid(x, y) &&
preferredDesktop.isAvailable()) {
preferredDesktop.addFileItemCloseTo(fileItem, x, y, storeMode);
assigned = true;
}
if (!assigned)
outOfDesktops.push(fileItem);
}
// if there was no space in the preferred desktop, place on the
// desktop closest to preferred
if (outOfDesktops.length) {
this._addFilesCloseToAssignedDesktop(
outOfDesktops,
storeMode,
preferredDesktop
);
}
}
_addFilesCloseToAssignedDesktop(fileList, storeMode, preferredDesktop) {
for (let fileItem of fileList) {
let desktopX;
let x = desktopX = preferredDesktop.gridGlobalRectangle.x;
let desktopY = preferredDesktop.gridGlobalRectangle.y;
if (fileItem.savedCoordinates) {
x = fileItem.savedCoordinates[0];
storeMode = this.Enums.StoredCoordinates.ASSIGN;
} else if (fileItem.droppedCoordinates) {
x = fileItem.droppedCoordinates[0];
storeMode = this.Enums.StoredCoordinates.OVERWRITE;
}
// Find the closest desktop to given position, is null
// if not available
const newDesktop = this.windowManager.getClosestDesktop(x);
if (newDesktop) {
desktopX = newDesktop.gridGlobalRectangle.x;
desktopY = newDesktop.gridGlobalRectangle.y;
if (fileItem.droppedCoordinates)
fileItem.droppedCoordinates = null;
newDesktop
.addFileItemCloseTo(fileItem, desktopX, desktopY, storeMode);
} else {
console.log('Not enough space to add icons');
}
}
}
_unstack() {
if (this.stackInitialCoordinates && this._compositeStackList) {
this._displayList.forEach(f => {
f.removeFromGrid();
if (f.isStackMarker)
f.onDestroy();
});
this._restoreStackInitialCoordinates();
this._displayList = this._compositeStackList;
this._compositeStackList = null;
if (this.sortingSubMenu && this.sortingMenu) {
this.sortingSubMenu.prepend_item(this.keepArrangedMenuItem);
this.sortingMenu.prepend_item(this.cleanUpMenuItem);
}
if (this.Prefs.keepArranged) {
this.doSorts();
} else {
this._addFilesToDesktop(
this._displayList,
this.Enums.StoredCoordinates.OVERWRITE
);
}
}
}
_saveStackInitialCoordinates() {
this.stackInitialCoordinates = [];
for (let fileItem of this._displayList) {
this.stackInitialCoordinates.push({
fileName: fileItem.fileName,
savedCoordinates: fileItem.savedCoordinates,
_normalCoordinates: fileItem._normalCoordinates,
_monitorIndex: fileItem._monitorIndex,
});
}
}
_transformSavedStackInitialCoordinates() {
if (!this.stackInitialCoordinates &&
this.stackInitialCoordinates.length)
return;
this._recomputeWindowPositions(this.stackInitialCoordinates);
this.stackInitialCoordinates.forEach(o =>
(o.savedCoordinates = o.temporarySavedPosition));
}
_restoreStackInitialCoordinates() {
if (this.stackInitialCoordinates &&
this.stackInitialCoordinates.length) {
this._compositeStackList.forEach(fileItem => {
this.stackInitialCoordinates.forEach(savedItem => {
if (savedItem.fileName === fileItem.fileName) {
fileItem.savedCoordinates = savedItem.savedCoordinates;
fileItem._normalCoordinates =
savedItem._normalCoordinates;
fileItem._monitorIndex = savedItem._monitorIndex;
}
});
});
}
this.stackInitialCoordinates = null;
}
_makeStackTopMarkerFolder(type, list) {
let stackAttribute = type.split('/')[1];
let fileItem = new StackItem.StackItem(
this,
stackAttribute,
type,
this.Enums.FileType.STACK_TOP
);
list.push(fileItem);
}
_sortAllFilesFromGridsByKindStacked(opts = {redisplay: false}) {
/**
* Looks through the generated fileItems
*/
function determineStackTopSizeOrTime() {
for (let item of otherFiles) {
if (item.isStackMarker) {
for (let unstackitem of stackedFiles) {
if (item.attributeContentType ===
unstackitem.attributeContentType) {
item.size = unstackitem.fileSize;
item.time = unstackitem.modifiedTime;
break;
}
}
}
}
}
/**
* Sorts fileItems by file size
*
* @param {integer} a the first file size
* @param {integer} b the secondfile size
*/
function bySize(a, b) {
return a.fileSize - b.fileSize;
}
/**
* Sorts fileItems by time
*
* @param {integer} a the first file timestamp
* @param {integer} b the second file timestamp
*/
function byTime(a, b) {
return a._modifiedTime - b._modifiedTime;
}
let specialFiles = [];
let directoryFiles = [];
let validDesktopFiles = [];
let otherFiles = [];
let stackedFiles = [];
let newFileList = [];
let stackTopMarkerFolderList = [];
let unstackList = this.Prefs.UnstackList;
if (this._compositeStackList && opts.redisplay) {
this._displayList.forEach(f => {
if (f.isStackMarker)
f.onDestroy();
});
this._displayList = this._compositeStackList;
}
this._sortByName(this._displayList);
for (let fileItem of this._displayList) {
if (fileItem.isSpecial) {
specialFiles.push(fileItem);
continue;
}
if (fileItem.isDirectory) {
directoryFiles.push(fileItem);
continue;
}
if (fileItem._isValidDesktopFile) {
validDesktopFiles.push(fileItem);
continue;
} else {
let type = fileItem.attributeContentType;
let stacked = false;
for (let item of otherFiles) {
if (type === item.attributeContentType) {
stackedFiles.push(fileItem);
stacked = true;
}
}
if (!stacked) {
fileItem.isStackTop = true;
otherFiles.push(fileItem);
}
continue;
}
}
for (let a of otherFiles) {
let instack = false;
for (let c of stackedFiles) {
if (c.attributeContentType === a.attributeContentType) {
instack = true;
break;
}
}
if (!instack)
a.stackUnique = true;
continue;
}
for (let item of otherFiles) {
if (!item.stackUnique) {
this._makeStackTopMarkerFolder(
item.attributeContentType,
stackTopMarkerFolderList
);
item.isStackTop = false;
stackedFiles.push(item);
}
if (item.stackUnique)
stackTopMarkerFolderList.push(item);
item.updateIcon()
.catch(e => console.error(e, 'Error loading stackMarker icon'));
}
otherFiles = [];
this._sortByName(specialFiles);
this._sortByName(directoryFiles);
this._sortByName(validDesktopFiles);
this._sortByKindByName(stackedFiles);
this._sortByKindByName(stackTopMarkerFolderList);
otherFiles.push(...specialFiles);
otherFiles.push(...validDesktopFiles);
otherFiles.push(...directoryFiles);
otherFiles.push(...stackTopMarkerFolderList);
switch (this.Prefs.sortOrder) {
case this.Enums.SortOrder.NAME:
this._sortByName(otherFiles);
break;
case this.Enums.SortOrder.DESCENDINGNAME:
this._sortByName(otherFiles);
otherFiles.reverse();
this._sortByName(stackedFiles);
stackedFiles.reverse();
break;
case this.Enums.SortOrder.MODIFIEDTIME:
stackedFiles.sort(byTime);
determineStackTopSizeOrTime();
otherFiles.sort(byTime);
break;
case this.Enums.SortOrder.KIND:
break;
case this.Enums.SortOrder.SIZE:
stackedFiles.sort(bySize);
determineStackTopSizeOrTime();
otherFiles.sort(bySize);
break;
default:
break;
}
for (let item of otherFiles) {
newFileList.push(item);
let itemtype = item.attributeContentType;
for (let unstackitem of stackedFiles) {
if (unstackList.includes(unstackitem.attributeContentType) &&
(unstackitem.attributeContentType === itemtype))
newFileList.push(unstackitem);
}
}
if (this._compositeStackList)
this._compositeStackList = this._displayList;
this._displayList = newFileList;
}
_sortByName(fileList) {
/**
* @param {string} a fileItem filename for A
* @param {string} b fileItem filename for B
*/
function byName(a, b) {
// sort by label name instead of the the fileName or displayName so
// that the "Home" folder is sorted in the correct order
// alphabetical sort taking into account accent characters &
// locale, natural language sort for numbers, ie 10.etc before 2.etc
// other options for locale are best fit, or by specifying directly
// in function below for translators
return a._label.get_text()
.localeCompare(
b._label.get_text(),
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
);
}
fileList.sort(byName);
}
_sortByKindByName(fileList) {
/**
* Sort by Kind, then by name
*
* @param {string} a fileItem
* @param {string} b fileItem
*/
function byKindByName(a, b) {
return (
a.attributeContentType
.localeCompare(b.attributeContentType) ||
a._label.get_text()
.localeCompare(
b._label.get_text(),
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
)
);
}
fileList.sort(byKindByName);
}
_sortAllFilesFromGridsByName(order) {
this._sortByName(this._displayList);
if (order === this.Enums.SortOrder.DESCENDINGNAME)
this._displayList.reverse();
this._reassignFilesToDesktop();
}
_sortByOriginalPosition() {
let cornerInversion = this.Prefs.StartCorner;
if (!cornerInversion[0] && !cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.X < b.X)
return -1;
if (a.X > b.X)
return 1;
if (a.Y < b.Y)
return -1;
if (a.Y > b.Y)
return 1;
return 0;
});
}
if (cornerInversion[0] && cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.X < b.X)
return 1;
if (a.X > b.X)
return -1;
if (a.Y < b.Y)
return 1;
if (a.Y > b.Y)
return -1;
return 0;
});
}
if (cornerInversion[0] && !cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.X < b.X)
return 1;
if (a.X > b.X)
return -1;
if (a.Y < b.Y)
return -1;
if (a.Y > b.Y)
return 1;
return 0;
});
}
if (!cornerInversion[0] && cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.X < b.X)
return -1;
if (a.X > b.X)
return 1;
if (a.Y < b.Y)
return 1;
if (a.Y > b.Y)
return -1;
return 0;
});
}
}
_sortByCurrentPosition() {
let cornerInversion = this.Prefs.StartCorner;
if (!cornerInversion[0] && !cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.x < b.x)
return -1;
if (a.x > b.x)
return 1;
if (a.y < b.y)
return -1;
if (a.y > b.y)
return 1;
return 0;
});
}
if (cornerInversion[0] && cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.x < b.x)
return 1;
if (a.x > b.x)
return -1;
if (a.y < b.y)
return 1;
if (a.y > b.y)
return -1;
return 0;
});
}
if (cornerInversion[0] && !cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.x < b.x)
return 1;
if (a.x > b.x)
return -1;
if (a.y < b.y)
return -1;
if (a.y > b.y)
return 1;
return 0;
});
}
if (!cornerInversion[0] && cornerInversion[1]) {
this._displayList.sort((a, b) => {
if (a.x < b.x)
return -1;
if (a.x > b.x)
return 1;
if (a.y < b.y)
return 1;
if (a.y > b.y)
return -1;
return 0;
});
}
}
_reassignFilesToDesktop() {
if (!this.Prefs.sortSpecialFolders) {
this._reassignFilesToDesktopPreserveSpecialFiles();
return;
}
for (let fileItem of this._displayList) {
fileItem.temporarySavedPosition = null;
fileItem.dropCoordinates = null;
}
this._addFilesToDesktop(
this._displayList,
this.Enums.StoredCoordinates.ASSIGN
);
}
_reassignFilesToDesktopPreserveSpecialFiles() {
let specialFiles = [];
let otherFiles = [];
let newFileList = [];
for (let fileItem of this._displayList) {
if (fileItem._isSpecial) {
specialFiles.push(fileItem);
continue;
}
if (!fileItem._isSpecial) {
otherFiles.push(fileItem);
fileItem.temporarySavedPosition = null;
fileItem.dropCoordinates = null;
continue;
}
}
newFileList.push(...specialFiles);
newFileList.push(...otherFiles);
if (this._displayList.length === newFileList.length)
this._displayList = newFileList;
this._addFilesToDesktop(
this._displayList,
this.Enums.StoredCoordinates.PRESERVE
);
}
// Desktop Manager Main methods
// ********************************************************************** */
findFiles(text) {
if (this._findFileWindow) {
this._findFileWindow.present();
return;
}
const activeWindow = this.mainApp.get_active_window();
this._findFileWindow = new Gtk.Dialog({
use_header_bar: true,
resizable: false,
});
this._findFileButton =
this._findFileWindow.add_button(_('OK'), Gtk.ResponseType.OK);
this._findFileButton.sensitive = false;
this._findFileWindow.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
this._findFileWindow.set_modal(true);
this._findFileWindow.set_title(_('Find Files on Desktop'));
const modal = true;
this.DesktopIconsUtil
.windowHidePagerTaskbarModal(this._findFileWindow, modal);
this._findFileWindow.set_transient_for(activeWindow);
let contentArea = this._findFileWindow.get_content_area();
this._findFileTextArea = new Gtk.Entry();
this._findFileTextArea.set_margin_top(5);
this._findFileTextArea.set_margin_bottom(5);
this._findFileTextArea.set_margin_start(5);
this._findFileTextArea.set_margin_end(5);
contentArea.append(this._findFileTextArea);
contentArea.set_homogeneous(true);
contentArea.set_baseline_position(Gtk.BaselinePosition.CENTER);
this._findFileTextArea.connect('activate', () => {
if (this._findFileButton.sensitive)
this._findFileWindow.response(Gtk.ResponseType.OK);
});
this._findFileTextArea.connect('changed', () => {
let context = this._findFileTextArea.get_style_context();
if (this._scanForFiles(this._findFileTextArea.text, true)) {
this._findFileButton.sensitive = true;
if (context.has_class('not-found'))
context.remove_class('not-found');
} else {
this._findFileButton.sensitive = false;
this._findFileTextArea.error_bell();
if (!context.has_class('not-found'))
context.add_class('not-found');
}
this.searchEventTime = GLib.get_monotonic_time();
});
this._findFileTextArea.grab_focus_without_selecting();
if (text) {
this._findFileTextArea.set_text(text);
this._findFileTextArea.set_position(text.length);
} else {
this._scanForFiles(null);
}
this._findFileWindow.show();
this.mainApp.activate_action('textEntryAccelsTurnOff', null);
this._findFileWindow.connect('close', () => {
this._findFileWindow.response(Gtk.ResponseType.CANCEL);
});
this._findFileWindow.connect('response', (actor, retval) => {
if (retval === Gtk.ResponseType.CANCEL)
this.unselectAll();
this.mainApp.activate_action('textEntryAccelsTurnOn', null);
this._displayList.forEach(f => (f.opacity = 1));
this._findFileWindow.destroy();
this._findFileWindow = null;
});
}
_scanForFiles(text, setselected) {
let found = [];
if (text && (text !== '')) {
const lowerCaseText = text.toLowerCase();
found =
this._displayList.filter(
f => {
const lowerCaseFilename = f.fileName.toLowerCase();
const lowerCaseLabel =
f._label.get_text().toLowerCase();
return (
lowerCaseFilename.includes(lowerCaseText) ||
lowerCaseLabel.includes(lowerCaseText)
);
}
);
}
if (found.length !== 0) {
if (setselected) {
this.unselectAll();
const notfound = this._displayList.filter(
f => !found.includes(f)
);
found.forEach(f => {
f.setSelected();
f.opacity = 1;
});
notfound.forEach(f => (f.opacity = 0.2));
}
return true;
} else {
this.unselectAll();
return false;
}
}
sortAllFilesFromGridsByPosition() {
if (this.Prefs.keepArranged)
return;
this._displayList.map(f => f.removeFromGrid({callOnDestroy: false}));
this._sortByCurrentPosition();
this._reassignFilesToDesktop();
}
_sortAllFilesFromGridsByModifiedTime() {
/**
* @param {integer} a fileItem file modified time
* @param {integer} b fileItem file modified time
*/
function byTime(a, b) {
return a._modifiedTime - b._modifiedTime;
}
this._displayList.sort(byTime);
this._reassignFilesToDesktop();
}
_sortAllFilesFromGridsBySize() {
/**
* @param {integer} a fileItem fileSize
* @param {integer} b fileItem fileSize
*/
function bySize(a, b) {
return a.fileSize - b.fileSize;
}
this._displayList.sort(bySize);
this._reassignFilesToDesktop();
}
_sortAllFilesFromGridsByKind() {
let specialFiles = [];
let directoryFiles = [];
let validDesktopFiles = [];
let otherFiles = [];
let newFileList = [];
for (let fileItem of this._displayList) {
if (fileItem._isSpecial) {
specialFiles.push(fileItem);
continue;
}
if (fileItem._isDirectory) {
directoryFiles.push(fileItem);
continue;
}
if (fileItem._isValidDesktopFile) {
validDesktopFiles.push(fileItem);
continue;
} else {
otherFiles.push(fileItem);
continue;
}
}
this._sortByName(specialFiles);
this._sortByName(directoryFiles);
this._sortByName(validDesktopFiles);
this._sortByKindByName(otherFiles);
newFileList.push(...specialFiles);
newFileList.push(...validDesktopFiles);
newFileList.push(...directoryFiles);
newFileList.push(...otherFiles);
if (this._displayList.length === newFileList.length)
this._displayList = newFileList;
this._reassignFilesToDesktop();
}
doSorts(opts = {redisplay: false}) {
if (opts.redisplay)
this._displayList.map(f => f.removeFromGrid());
switch (this.Prefs.sortOrder) {
case this.Enums.SortOrder.NAME:
this._sortAllFilesFromGridsByName();
break;
case this.Enums.SortOrder.DESCENDINGNAME:
this._sortAllFilesFromGridsByName(
this.Enums.SortOrder.DESCENDINGNAME);
break;
case this.Enums.SortOrder.MODIFIEDTIME:
this._sortAllFilesFromGridsByModifiedTime();
break;
case this.Enums.SortOrder.KIND:
this._sortAllFilesFromGridsByKind();
break;
case this.Enums.SortOrder.SIZE:
this._sortAllFilesFromGridsBySize();
break;
default:
this._addFilesToDesktop(
this._displayList,
this.Enums.StoredCoordinates.PRESERVE
);
break;
}
}
doStacks(opts = {redisplay: false}) {
if (opts.redisplay) {
for (let fileItem of this._displayList)
fileItem.removeFromGrid();
}
if (!this.stackInitialCoordinates && !this._compositeStackList) {
this._compositeStackList = [];
this._saveStackInitialCoordinates();
if (this.sortingSubMenu && this.sortingMenu) {
this.sortingSubMenu.remove(0);
this.sortingMenu.remove(0);
}
opts.redisplay = false;
}
if ((opts.monitorschanged ||
opts.initialRead) &&
this.stackInitialCoordinates)
this._transformSavedStackInitialCoordinates();
this._sortAllFilesFromGridsByKindStacked(opts);
this._reassignFilesToDesktop();
}
unselectAll() {
this._displayList.forEach(f => {
f.unsetSelected();
f.keyboardUnSelected();
f.opacity = 1;
});
this.desktopActions.lastAnchorSelected = null;
this.fileItemMenu.activeFileItem = null;
}
getCurrentSelection() {
const selectedList = this._displayList.filter(f => f.isSelected);
if (selectedList.length)
return selectedList;
return null;
}
getCurrentSelectionAsUri() {
return this.getCurrentSelection()?.map(f => f.uri);
}
getNumberOfSelectedItems() {
const count = this.getCurrentSelection();
if (count)
return count.length;
return 0;
}
checkIfSpecialFilesAreSelected() {
for (let item of this._displayList) {
if (item.isSelected && item.isSpecial)
return true;
}
return false;
}
checkIfDirectoryIsSelected() {
for (let item of this._displayList) {
if (item.isSelected && item.isDirectory)
return true;
}
return false;
}
async doRename(fileItem, allowReturnOnSameName = false) {
const selection = this.getCurrentSelection();
if (!(selection && (selection.length === 1)))
return;
if (fileItem === null) {
fileItem = selection[0];
allowReturnOnSameName = false;
}
if (!fileItem.canRename)
return;
if (!this._renameWindow) {
this.mainApp.activate_action('textEntryAccelsTurnOff', null);
if (!this.newItemDoRename)
this.newItemDoRename = new Set();
this.newItemDoRename.add(fileItem.fileName);
if (this.desktopMenuManager.popupmenu) {
await this.desktopMenuManager.menuclosed()
.catch(e => logError(e));
}
if (this.fileItemMenu.popupmenu)
await this.fileItemMenu.menuclosed().catch(e => logError(e));
this._renameWindow = new AskRenamePopup.AskRenamePopup(
fileItem,
allowReturnOnSameName,
() => {
this.mainApp.get_active_window().grab_focus();
this.mainApp.activate_action('textEntryAccelsTurnOn', null);
if (this.newItemDoRename)
this.newItemDoRename.delete(fileItem.fileName);
this._renameWindow = null;
},
this.dragManager.setPendingDropCoordinates.bind(this),
{
FileUtils: this.FileUtils,
DesktopIconsUtil: this.DesktopIconsUtil,
DBusUtils: this.DBusUtils,
}
);
}
}
async doNewFolder(
position = null,
suggestedName = null,
opts = {rename: true}
) {
this.unselectAll();
if (!position)
position = [this._clickX, this._clickY];
const baseName = suggestedName ? suggestedName : _('New Folder');
let newName = this.desktopMonitor.getDesktopUniqueFileName(baseName);
if (newName) {
const dir = this._desktopDir.get_child(newName);
try {
await dir.make_directory_async(GLib.PRIORITY_DEFAULT, null);
const info = new Gio.FileInfo();
info.set_attribute_string(
'metadata::nautilus-drop-position',
`${position.join(',')}`
);
info.set_attribute_string(
'metadata::desktop-icon-position',
''
);
info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, 0o700);
try {
await dir.set_attributes_async(info,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
null);
} catch (e) {
console.error(
e, `Failed to set attributes to ${dir.get_path()}`);
}
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
this._performSanityChecks();
else
console.error(e, `Failed to create folder ${e.message}`);
const header = _('Folder Creation Failed');
const text = _('Could not create folder');
this.dbusManager.doNotify(header, text);
if (position || suggestedName)
return null;
return null;
}
if (opts.rename) {
if (!this.newItemDoRename)
this.newItemDoRename = new Set();
this.newItemDoRename.add(newName);
}
if (position || suggestedName)
return dir.get_uri();
}
return null;
}
async redrawDesktop() {
// fileList is not changed, we just need to render the desktop again
// with changes in icon color, emblem, appearance, theme change etc.
const opts = {initialRead: false, redisplay: true};
const fileList = this.desktopMonitor.fileList;
await this._drawDesktop(fileList, opts).catch(e => {
console.error(
`Error while redrawing desktop: ${e.message}\n${e.stack}`
);
});
}
async reLoadDesktop() {
await this.desktopMonitor.reLoadFileList();
}
async refreshDesktop() {
// fileList is changed, we need to render the desktop again
// with latest fileList from the desktopMonitor. The position of the
// icons is also recomputed from the normalized coordinates.
const opts = {initialRead: true};
const fileList = this.desktopMonitor.fileList;
await this._drawDesktop(fileList, opts).catch(e => {
console.error(`Error while refreshing desktop: ${e.message}`);
});
}
async reFrameDesktop(opts) {
// fileList is not changed, grids changed, monitor added, removed,
// monitor geometry, zoom, or index changed.
// We need to recompute the position of the icons
// from the normalized coordinates and redraw the desktop and reassign
// the icons to the correct grid and monitors
const fileList = this.desktopMonitor.fileList;
await this._drawDesktop(fileList, opts).catch(e => {
console.error(`Error while reframing desktop: ${e.message}`);
});
}
onMutterSettingsChanged() {
this.windowManager.requestGeometryUpdate();
}
async onGtkSettingsChanged() {
await this.desktopMonitor.getFileList();
await this.reLoadDesktop().catch(e => {
console.log('Exception while updating desktop after the hidden ' +
`settings changed: ${e.message}\n${e.stack}`);
});
this.desktopMenuManager.updateTemplates();
}
onKeepArrangedChanged() {
if (this.Prefs.keepArranged)
this.doSorts({redisplay: true});
}
onUnstackedTypesChanged() {
if (this.Prefs.keepStacked)
this.doStacks({redisplay: true});
}
onkeepStackedChanged() {
if (!this.Prefs.keepStacked)
this._unstack();
else
this.doStacks({redisplay: true});
}
onSortOrderChanged() {
if (this.Prefs.keepStacked)
this.doStacks({redisplay: true});
else
this.doSorts({redisplay: true});
}
onIconSizeChanged() {
this._displayList.forEach(x => x.removeFromGrid());
for (let desktop of this._desktops)
desktop.resizeGrid();
this.reLoadDesktop().catch(e => {
console.log('Exception while reloading desktop after icon ' +
`size change: ${e.message}\n${e.stack}`);
});
}
onDarkModeChanged() {
this.widgetManager.onThemeChanged();
}
onAnimationChanged() {
this.widgetManager.onAnimationChanged();
}
// Getters and Setters
get _desktopDir() {
return this.desktopMonitor.desktopDir;
}
get fractionalScaling() {
return this.Prefs.fractionalScaling;
}
set fractionalScaling(boolean) {
this.Prefs.fractionalScaling = boolean;
}
get _desktops() {
return this.windowManager.desktops;
}
get _primaryMonitorIndex() {
return this.windowManager.primaryMonitorIndex;
}
get _priorPrimaryMonitorIndex() {
return this.windowManager.priorPrimaryMonitorIndex;
}
get preferredDisplayDesktop() {
return this.windowManager.preferredDisplayDesktop;
}
get templatesMonitor() {
return this.desktopActions.templatesMonitor;
}
get currentWorkingList() {
let currentCompleteList;
if (this._compositeStackList && (this._compositeStackList.length > 0))
currentCompleteList = this._compositeStackList;
else
currentCompleteList = this._displayList;
return currentCompleteList;
}
get activeFileItem() {
return this.fileItemMenu.activeFileItem;
}
set activeFileItem(fileItem) {
this.fileItemMenu.activeFileItem = fileItem;
}
get pendingDropFiles() {
return this.dragManager.pendingDropFiles;
}
set pendingDropFiles(object) {
this.dragManager.pendingDropFiles = object;
}
get pendingSelfCopyFiles() {
return this.dragManager.pendingSelfCopyFiles;
}
set pendingSelfCopyFiles(object) {
this.dragManager.pendingSelfCopyFiles = object;
}
get writableByOthers() {
return this.desktopMonitor._writableByOthers;
}
get modifierMode() {
return {
state: this._lastModifierState,
ctrl: (this._lastModifierState & Gdk.ModifierType.CONTROL_MASK) !== 0,
shift: (this._lastModifierState & Gdk.ModifierType.SHIFT_MASK) !== 0,
alt: (this._lastModifierState & Gdk.ModifierType.ALT_MASK) !== 0,
};
}
};