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

851 lines
24 KiB
JavaScript

/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
* SwitcherooControl code based on code original from Marsch84
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Gtk, Gdk, Gio, GLib} from '../dependencies/gi.js';
import {DesktopIconItem} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {FileItemIcon};
const Signals = imports.signals;
const FileItemIcon = class extends DesktopIconItem {
constructor(desktopManager, file, fileInfo, fileTypeEnum, gioMount) {
super(desktopManager, fileTypeEnum);
this.DBusUtils = desktopManager.DBusUtils;
this._fileInfo = fileInfo;
this._gioMount = gioMount;
this._file = file;
this.isStackTop = false;
this.stackUnique = false;
this.readSavedCoordinates();
this.readDropCoordinates();
this._createIconActor();
/* Set the metadata */
this._updateMetadataFromFileInfo(fileInfo);
if (this._attributeCanExecute)
this._execLine = this.file.get_path();
else
this._execLine = null;
this._updateName();
if (this._dropCoordinates)
this.setSelected();
}
/** *********************
* Destroyers *
***********************/
_destroy() {
super._destroy();
if (this._updatingIconCancellable)
this._updatingIconCancellable.cancel();
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
if (this._savedCoordinatesCancellable)
this._savedCoordinatesCancellable.cancel();
if (this._dropCoordinatesCancellable)
this._dropCoordinatesCancellable.cancel();
/* Metadata */
if (this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable.cancel();
}
/** *********************
* Creators *
***********************/
_getVisibleName() {
return this._fileInfo.get_display_name();
}
_setFileName(text) {
this._setLabelName(text);
}
_setAccesibilityName() {
const visibleName = this._getVisibleName();
const folderName = _('Folder');
const fileName = _('File');
if (this._isDirectory) {
/** TRANSLATORS: when using a screen reader, this is the text
* read when a folder is selected. Example: if a folder named
* "things" is selected, it will say "things Folder" */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${visibleName} ${folderName}`]
);
} else {
/** TRANSLATORS: when using a screen reader, this is the text
* read when a normal file is selected. Example: if a file
* named "my_picture.jpg" is selected, it will say
* "my_picture.jpg File" */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${visibleName} ${fileName}`]
);
}
}
readSavedCoordinates() {
const array = this._readCoordinatesFromAttribute(this._fileInfo,
'metadata::desktop-icon-position'
);
this._parseSavedCoordinates(array);
}
readDropCoordinates() {
const array = this._readCoordinatesFromAttribute(this._fileInfo,
'metadata::nautilus-drop-position'
);
this._parseDropCoordinates(array);
}
_readCoordinatesFromAttribute(fileInfo, attribute) {
const readCoordinates = fileInfo.get_attribute_as_string(attribute);
if (readCoordinates !== null && readCoordinates !== '')
return readCoordinates.split(',');
return null;
}
async _refreshMetadataAsync(cancellable) {
if ((cancellable && cancellable.is_cancelled()) || this._destroyed) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED,
'Operation was cancelled');
} else if (!cancellable) {
cancellable = new Gio.Cancellable();
}
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
this._queryFileInfoCancellable = cancellable;
try {
const newFileInfo =
await this._file.query_info_async(
this.Enums.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
cancellable
);
this._updateMetadataFromFileInfo(newFileInfo);
this._updateName();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
console.error(e, `Error getting file info: ${e.message}`);
} finally {
if (this._queryFileInfoCancellable === cancellable)
this._queryFileInfoCancellable = null;
}
}
_updateMetadataFromFileInfo(fileInfo) {
this._fileInfo = fileInfo;
this._displayName = this._getVisibleName();
this._attributeCanExecute = fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE
);
this._unixmode = fileInfo.get_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE
);
this._writableByOthers =
(this._unixmode & this.Enums.UnixPermissions.S_IWOTH) !== 0;
this._attributeContentType = fileInfo.get_content_type();
this._fileType = fileInfo.get_file_type();
this._isDirectory = this._fileType === Gio.FileType.DIRECTORY;
this._isSpecial = this._fileTypeEnum !== this.Enums.FileType.NONE;
this._isHidden =
fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) ||
fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_BACKUP);
this._modifiedTime = fileInfo.get_attribute_uint64(
Gio.FILE_ATTRIBUTE_TIME_MODIFIED
);
if (this.Prefs.showLinkEmblem)
this._setEncryptionStatus().catch(logError);
}
async _setEncryptionStatus() {
if (this.isEncrypted)
return;
switch (this._attributeContentType) {
case 'application/x-7z-compressed':
this._isEncrypted =
this.DesktopIconsUtil.checkIf7zEncrypted(this._file);
break;
case 'application/pdf':
// eslint-disable-next-line no-case-declarations
const isEncrypted =
await this.DesktopIconsUtil.checkIfPdfEncrypted(this._file);
// File may have no password or null password, so we may still be
// able to read/display it. It will therefore have a generated
// thumbnail. Check by generating the thumbnail if needed.
// Don't show the locked item in this case, it is encrypted in pdf
// per pdf specification but a user can still read it.
if (isEncrypted && !this.thumbnail) {
this.thumbnail =
await this.ThumbnailLoader.getThumbnail(
this,
null
);
}
this._isEncrypted = isEncrypted && !this.thumbnail;
break;
case 'application/zip':
this._isEncrypted =
await this.DesktopIconsUtil.checkIfZipEncrypted(this._file);
break;
case 'application/epub+zip':
this._isEncrypted =
await this.DesktopIconsUtil.checkIfZipEncrypted(this._file);
break;
default:
this._isEncrypted = false;
}
if (!this._isEncrypted)
return;
this.updateIcon()
.catch(e =>
console.error(`Error updating after setting encryption status ${e}`)
);
}
async _doOpenContext(context = null, fileList) {
if (!fileList)
fileList = [];
if (!this.DBusUtils.GnomeArchiveManager.isAvailable &&
this._fileType === Gio.FileType.REGULAR &&
this._desktopManager.autoAr.fileIsCompressed(this.fileName)
) {
this._desktopManager.autoAr.extractFile(this.fileName);
return;
}
try {
await Gio.AppInfo.launch_default_for_uri_async(
this.file.get_uri(),
context,
null
);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED)) {
const title = _('Opening File Failed');
const defaultAppInfo =
Gio.content_type_get_description(this.attributeContentType);
const error =
_('There is no application installed to open "{fo}" files.')
.replace('{fo}', defaultAppInfo);
const helpURI =
'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
this._showerrorpopup(title, error, helpURI);
} else {
console.error(
e, `Error opening file ${this.file.get_uri()}: ${e.message}`
);
}
}
}
_showerrorpopup(title, error, helpURI = null) {
const errorDialog = this._desktopManager.showError(
title,
error,
helpURI
);
errorDialog.show();
}
_updateName() {
this._setFileName(this._getVisibleName());
this._setAccesibilityName();
}
/** *********************
* Button Clicks *
***********************/
_doButtonOnePressed(
button, nPress, X, Y, x, y, shiftPressed, controlPressed
) {
super._doButtonOnePressed(
button, nPress, X, Y, x, y, shiftPressed, controlPressed
);
if (nPress === 2 && !this.Prefs.CLICK_POLICY_SINGLE)
this.doOpen();
}
_doButtonOneReleased(
_button, nPress, _X, _Y, _x, _y, shiftPressed, controlPressed
) {
if (nPress === 1 &&
this.Prefs.CLICK_POLICY_SINGLE &&
!shiftPressed &&
!controlPressed)
this.doOpen();
}
/** *********************
* Drag and Drop *
***********************/
async receiveDrop(
X, Y,
x, y,
dropData,
acceptFormat,
gdkDropAction,
localDrop,
event,
dragItem
) {
if (!this.dropCapable)
return false;
if (acceptFormat !== this.Enums.DndTargetInfo.DING_ICON_LIST &&
acceptFormat !== this.Enums.DndTargetInfo.GNOME_ICON_LIST &&
acceptFormat !== this.Enums.DndTargetInfo.URI_LIST)
return false;
const fileList =
this._dragManager.makeFileListFromSelection(dropData, acceptFormat);
if (!fileList)
return false;
if (dragItem && (dragItem.uri === this._file.get_uri() ||
!(this._isValidDesktopFile || this.isDirectory))) {
// Dragging a file/folder over itself or over another file will
// do nothing, allow drag to directory or valid desktop file
return false;
}
const dropReturnValue = await this._handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
);
return dropReturnValue;
}
async _handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
) {
const forceCopy = gdkDropAction === Gdk.DragAction.COPY;
let returnAction;
if (gdkDropAction === Gdk.DragAction.MOVE ||
gdkDropAction === Gdk.DragAction.COPY
) {
if (localDrop)
this._dragManager.saveCurrentFileCoordinatesForUndo();
try {
returnAction =
await this._dragManager.copyOrMoveUris(
fileList, this._file.get_uri(), event, {forceCopy}
);
} catch (e) {
console.error(e);
return false;
}
} else {
if (gdkDropAction >= Gdk.DragAction.LINK)
returnAction = Gdk.DragAction.LINK;
else
returnAction = Gdk.DragAction.COPY;
this._dragManager.askWhatToDoWithFiles(
fileList,
this._file.get_uri(),
X, Y,
x, y,
event,
{desktopActions: false}
);
}
return returnAction;
}
_hasToRouteDragToGrid() {
return this._isSelected &&
this._dragManager.dragItem &&
this._dragManager.dragItem.uri !== this._file.get_uri();
}
_dropCapable() {
if (this._isDirectory ||
this._hasToRouteDragToGrid()
)
return true;
else
return false;
}
/** *********************
* Icon Rendering *
***********************/
async _reloadIcon(cancellable) {
if (!cancellable)
cancellable = new Gio.Cancellable();
this._updatingIconCancellable = cancellable;
try {
await this._refreshMetadataAsync(cancellable);
await this._updateIcon(cancellable);
this._icon.queue_draw();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
`Exception while updating ${
this._getVisibleName()
? this._getVisibleName()
: 'updating icon'
}: ${e.message}`);
throw e;
}
} finally {
if (this._updatingIconCancellable === cancellable)
this._updatingIconCancellable = null;
}
}
_addEmblemsToIconIfNeeded(iconPaintable, position = 0) {
let emblem = null;
let newIconPaintable = iconPaintable;
if (this.isEncrypted && this.Prefs.showLinkEmblem) {
emblem = Gio.ThemedIcon.new('icon-emblem-locked');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
return newIconPaintable;
}
/** *********************
* Class Methods *
***********************/
onAttributeChanged() {
if (this._destroyed)
return;
this._reloadIcon()
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Exception while updating icon on Attribute Changed: ' +
`${e.message}`
);
}
}
);
}
updatedMetadata() {
this._reloadIcon().catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Exception while updating icon on Metadata Changed: ' +
`${e.message}`
);
}
});
}
doOpen(fileList) {
if (!fileList)
fileList = [];
this._doOpenContext(null, fileList).catch(e => console.error(e));
}
async onAllowDisallowLaunchingClicked() {
/*
* we're marking as trusted, make the file executable too. Note that we
* do not ever remove the executable bit, since we don't know who set
* it.
*/
if (this.metadataTrusted && !this._attributeCanExecute) {
let info = new Gio.FileInfo();
let newUnixMode = this._unixmode |
this.Enums.UnixPermissions.S_IXUSR |
this.Enums.UnixPermissions.S_IXGRP |
this.Enums.UnixPermissions.S_IXOTH;
info.set_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE,
newUnixMode
);
await this._setFileAttributes(info).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
});
}
this._updateName();
}
doDiscreteGpu() {
if (!this.DBusUtils.discreteGpuAvailable) {
console.log(
'Could not apply discrete GPU environment, switcheroo-control' +
' not available'
);
return;
}
let gpus = this.DBusUtils.SwitcherooControl.proxy.GPUs;
if (!gpus) {
console.log(
'Could not apply discrete GPU environment. No GPUs in list.'
);
return;
}
for (let gpu in gpus) {
if (!gpus[gpu])
continue;
let defaultVariant = gpus[gpu]['Default'];
if (!defaultVariant || defaultVariant.get_boolean())
continue;
let env = gpus[gpu]['Environment'];
if (!env)
continue;
let envS = env.get_strv();
let context = new Gio.AppLaunchContext();
for (let i = 0; i < envS.length; i += 2)
context.setenv(envS[i], envS[i + 1]);
this._doOpenContext(context, null).catch(e => console.error(e));
return;
}
console.log('Could not find discrete GPU data in switcheroo-control');
}
async _setFileAttributes(fileInfo, cancellable = null, updateIcon = true) {
await this._file.set_attributes_async(
fileInfo,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
cancellable
);
if (cancellable && cancellable.is_cancelled()) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED,
'Operation was cancelled');
}
if (updateIcon) {
await this._reloadIcon(cancellable).catch(e => {
console.error(
'Error while updating icon while setting attributes'
);
throw e;
});
}
}
async _storeCoordinates(name, coords, cancellable = null) {
const info = new Gio.FileInfo();
info.set_attribute_string(
`metadata::${name}`,
`${coords ? coords.join(',') : ''}`
);
const updateIcon = true;
await this._setFileAttributes(info, cancellable, !updateIcon);
}
writeSavedCoordinates(pos) {
const oldPos = this._savedCoordinates;
this._parseSavedCoordinates(pos);
if (this._savedCoordinatesCancellable)
this._savedCoordinatesCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._savedCoordinatesCancellable = cancellable;
this._storeCoordinates(
'desktop-icon-position',
pos,
cancellable
).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Failed to store the desktop coordinates for ' +
`${this.uri}: ${e.message}`
);
this._savedCoordinates = oldPos;
}
}).finally(() => {
if (this._savedCoordinatesCancellable === cancellable)
this._savedCoordinatesCancellable = null;
});
}
writeDroppedCoordinates(pos) {
const oldPos = this._dropCoordinates;
this._parseDropCoordinates(pos);
if (this._dropCoordinatesCancellable)
this._dropCoordinatesCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._dropCoordinatesCancellable = cancellable;
this._storeCoordinates(
'nautilus-drop-position',
pos,
cancellable
).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(e,
'Failed to store the desktop coordinates for ' +
`${this.uri}: ${e.message}`
);
this._dropCoordinates = oldPos;
}
}).finally(() => {
if (this._dropCoordinatesCancellable === cancellable)
this._dropCoordinatesCancellable = null;
});
}
/** *********************
* Getters and setters *
***********************/
get attributeContentType() {
return this._attributeContentType;
}
get attributeCanExecute() {
return this._attributeCanExecute;
}
get canRename() {
return !this.trustedDesktopFile &&
(this._fileTypeEnum === this.Enums.FileType.NONE);
}
get displayName() {
return this._displayName || null;
}
get dropCoordinates() {
return this._dropCoordinates;
}
set dropCoordinates(pos) {
if (this.DesktopIconsUtil.coordinatesEqual(this._dropCoordinates, pos))
return;
this.writeDroppedCoordinates(pos);
}
get execLine() {
return this._execLine;
}
get executableContentType() {
return Gio.content_type_can_be_executable(this.attributeContentType);
}
get file() {
return this._file;
}
get fileContainsText() {
return this._attributeContentType === 'text/plain';
}
get fileName() {
return this._fileInfo.get_name();
}
get fileSize() {
return this._fileInfo.get_size();
}
get isAllSelectable() {
return this._fileTypeEnum === this.Enums.FileType.NONE;
}
get isDirectory() {
return this._isDirectory;
}
get isExecutable() {
return this._attributeCanExecute;
}
get isHidden() {
return this._isHidden;
}
get metadataTrusted() {
return this._trusted;
}
set metadataTrusted(value) {
this._trusted = value;
if (this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._setMetadataTrustedCancellable = cancellable;
let info = new Gio.FileInfo();
info.set_attribute_string('metadata::trusted',
value ? 'true' : 'false');
this._setFileAttributes(info, cancellable)
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console
.error(e, `Failed to set metadata::trusted: ${e.message}`);
}
})
.finally(() => {
if (cancellable === this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable = null;
});
}
get modifiedTime() {
return this._modifiedTime;
}
get path() {
return this._file.get_path();
}
get savedCoordinates() {
return this._savedCoordinates;
}
set savedCoordinates(pos) {
if (this.DesktopIconsUtil.coordinatesEqual(this._savedCoordinates, pos))
return;
this.writeSavedCoordinates(pos);
}
get x() {
return this._x1;
}
get y() {
return this._y1;
}
get X() {
return this._savedCoordinates[0];
}
get Y() {
return this._savedCoordinates[1];
}
get uri() {
return this._file.get_uri();
}
get writableByOthers() {
return this._writableByOthers;
}
get isStackMarker() {
if (this.isStackTop && !this.stackUnique)
return true;
else
return false;
}
};
Signals.addSignalMethods(FileItemIcon.prototype);