/* 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 . */ 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);