/* DING: Desktop Icons New Generation for GNOME Shell * * Gtk4 Port Copyright (C) 2022- 2025 Sundeep Mediratta (smedius@gmail.com) * Copyright (C) 2021 Sundeep Mediratta (smedius@gmail.com) * Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com) * Based on code original (C) Carlos Soriano * 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, Graphene, Gsk, GLib, Pango, GdkPixbuf} from '../dependencies/gi.js'; import {_} from '../dependencies/gettext.js'; export {DesktopIconItem}; const Signals = imports.signals; const PIXBUF_CONTENT_TYPES = new Set(); GdkPixbuf.Pixbuf .get_formats() .forEach(f => PIXBUF_CONTENT_TYPES.add(...f.get_mime_types())); const DesktopIconItem = class { constructor(desktopManager, fileTypeEnum) { this._desktopManager = desktopManager; this._dragManager = desktopManager.dragManager; this.DesktopIconsUtil = desktopManager.DesktopIconsUtil; this.FileUtils = desktopManager.FileUtils; this.Prefs = desktopManager.Prefs; this.Enums = desktopManager.Enums; this.ThumbnailLoader = desktopManager.ThumbnailLoader; this._fileTypeEnum = fileTypeEnum; this._queryFileInfoCancellable = null; this._grid = null; this._column = null; this._row = null; this._lastClickTime = 0; this._lastClickButton = 0; this._clickCount = 0; this._isSelected = false; this._isSpecial = false; this._keyboardSelected = false; this._savedCoordinates = null; this._dropCoordinates = null; this._normalCoordinates = null; this._monitorIndex = null; this._destroyed = false; this.thumbnail = null; this.thumbnailFile = null; } /** ********************* * Destroyers * ***********************/ removeFromGrid(opts = {callOnDestroy: false}) { if (this._grid) { this._grid.removeItem(this); this._grid = null; } if (opts.callOnDestroy) this.onDestroy(); } _destroy() { /* Regular file data */ if (this._queryFileInfoCancellable) this._queryFileInfoCancellable.cancel(); /* Icons update */ if (this._updateIconCancellable) this._updateIconCancellable.cancel(); /* Container */ if (this._containerId) { this.container.disconnect(this._containerId); this._containerId = 0; } /* DragItem */ if (this.dragIconSignal) this.dragIcon.disconnect(this.dragIconSignal); if (this._iconStateFlag) this._iconContainer.disconnect(this._iconStateFlag); if (this._labelStateFlag) this._labelContainer.disconnect(this._labelStateFlag); this._destroyToolTip(); } onDestroy() { this._destroy(); this._destroyed = true; } /** ********************* * Creators * ***********************/ _createIconActor() { this.container = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, halign: Gtk.Align.CENTER, focusable: true, can_focus: true, accessible_role: Gtk.AccessibleRole.LABEL, }); this.container.add_css_class('desktop-icon-container'); this._containerId = this.container.connect('destroy', () => this.onDestroy()); this._icon = new Gtk.Picture({ can_shrink: false, keep_aspect_ratio: true, halign: Gtk.Align.CENTER, }); this._iconContainer = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, halign: Gtk.Align.CENTER, hexpand: false, baseline_position: Gtk.BaselinePosition.CENTER, }); this._iconContainer.append(this._icon); this._label = new Gtk.Label({ halign: Gtk.Align.CENTER, natural_wrap_mode: Gtk.NaturalWrapMode.WORD, ellipsize: Pango.EllipsizeMode.END, wrap: true, wrap_mode: Pango.WrapMode.WORD_CHAR, yalign: 0.0, xalign: 0.0, justify: Gtk.Justification.CENTER, lines: 2, }); this._labelContainer = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, name: 'file-item', }); this.iconRectangle = new Gdk.Rectangle(); this.iconLocalWindowRectangle = new Gdk.Rectangle(); this.labelRectangle = new Gdk.Rectangle(); this._iconContainerEventController = new Gtk.EventControllerMotion({ propagation_phase: Gtk.PropagationPhase.CAPTURE, }); this._icon.add_controller(this._iconContainerEventController); this._iconContainerEventController.connect('enter', () => { this._showToolTip(); }); this._iconContainerEventController.connect('leave', () => { this._destroyToolTip(); }); // This controls how the icons look - Rectangular or skinny trapezoid if (!this.Prefs.freePositionIcons) { this._labelContainer.append(this._iconContainer); this._label.add_css_class('file-label-vertical'); this._labelContainer.append(this._label); this.container.append(this._labelContainer); } else { this._labelContainer.append(this._label); this.container.append(this._iconContainer); this.container.append(this._labelContainer); this._iconStateFlag = this._iconContainer.connect('state-flags-changed', () => { if (this._checkHasHoveredPointer(this._iconContainer)) { this._onEnter(); this._labelContainer.add_css_class('mimic-hovered'); } else { this._onLeave(); this._labelContainer.remove_css_class('mimic-hovered'); } }); this._iconContainer.set_name('file-item'); } this._labelStateFlag = this._labelContainer.connect('state-flags-changed', () => { if (this._checkHasHoveredPointer(this._labelContainer)) { this._onEnter(); if (this.Prefs.freePositionIcons) this._iconContainer.add_css_class('mimic-hovered'); } else { this._onLeave(); if (this.Prefs.freePositionIcons) this._iconContainer.remove_css_class('mimic-hovered'); } }); this.dragIcon = Gtk.WidgetPaintable.new(this.container); this.dragIconSignal = this.dragIcon.connect('invalidate-size', () => { this._doIconSizeAllocated(); }); this.container.show(); } _doIconSizeAllocated() { // If icons are hidden during stacking, they are not assigned a grid // if (!this._grid) return; this._calculateIconRectangle(); this._calculateLabelRectangle(); this.iconPlacedPromiseResolve(true); } iconPlaced = new Promise(resolve => { this.iconPlacedPromiseResolve = resolve; }); _calculateIconRectangle() { this.iconwidth = this._iconContainer.get_allocated_width(); this.iconheight = this._iconContainer.get_allocated_height(); const [x, y] = this._grid.coordinatesLocalToGlobal(0, 0, this._iconContainer); this.iconRectangle.x = x; this.iconRectangle.y = y; this.iconRectangle.width = this.iconwidth; this.iconRectangle.height = this.iconheight; this._calculateLocalWindowRectangle(); } _calculateLocalWindowRectangle() { const [x, y] = this._grid.coordinatesLocalToWindow(0, 0, this._iconContainer); this.iconLocalWindowRectangle.x = x; this.iconLocalWindowRectangle.y = y; this.iconLocalWindowRectangle.width = this.iconwidth; this.iconLocalWindowRectangle.height = this.iconheight; } _calculateLabelRectangle() { this.labelwidth = this._labelContainer.get_allocated_width(); this.labelheight = this._labelContainer.get_allocated_height(); const [x, y] = this._grid.coordinatesLocalToGlobal(0, 0, this._labelContainer); this.labelRectangle.x = x; this.labelRectangle.y = y; this.labelRectangle.width = this.labelwidth; this.labelRectangle.height = this.labelheight; } setCoordinates(x, y, width, height, margin, grid) { this._x1 = x; this._y1 = y; this.width = width; this.height = height; this._grid = grid; this.container.set_size_request(width, height); this._label.margin_start = margin; this._label.margin_end = margin; this._label.margin_bottom = margin; this._iconContainer.margin_top = margin; this._calculateIconRectangle(); this._calculateLabelRectangle(); } getCoordinates() { this._x2 = this._x1 + this.container.get_allocated_width() - 1; this._y2 = this._y1 + this.container.get_allocated_height() - 1; return [this._x1, this._y1, this._x2, this._y2, this._grid]; } writeSavedCoordinates(pos) { this._parseSavedCoordinates(pos); } writeDropCoordinates(pos) { this._parseDropCoordinates(pos); } readSavedCoordinates() { this._parseSavedCoordinates([]); } readDropCoordinates() { this._parseDropCoordinates([]); } _parseDropCoordinates(pos) { if (!Array.isArray(pos) || pos.some(e => isNaN(e))) { this._dropCoordinates = null; return; } pos = pos.map(e => Number(e)); if (pos?.length === 2) this._dropCoordinates = pos; else this._dropCoordinates = null; } _parseSavedCoordinates(pos) { if (!Array.isArray(pos) || pos.some(e => isNaN(e))) { this._savedCoordinates = null; this._normalCoordinates = null; this._monitorIndex = null; return; } pos = pos.map(e => Number(e)); if (pos?.length === 2) { this._savedCoordinates = pos; this._normalCoordinates = null; this._monitorIndex = null; } else if (pos?.length === 5) { this._savedCoordinates = pos.slice(0, 2); this._normalCoordinates = pos.slice(2, 4); this._monitorIndex = pos[4]; } else { this._savedCoordinates = null; this._normalCoordinates = null; this._monitorIndex = null; } } _setLabelName(text) { this._currentFileName = text; this._label.label = text; } /** ********************* * Button Clicks * ***********************/ _checkHasHoveredPointer(widget) { let stateFlags = widget.get_state_flags(); if ((stateFlags & Gtk.StateFlags.PRELIGHT) === Gtk.StateFlags.PRELIGHT) return true; else return false; } _onPressButton(actor, nPress, X, Y, x, y, shiftPressed, controlPressed) { const button = actor.get_current_button(); this._buttonPressInitialX = x - this._x1; this._buttonPressInitialY = y - this._y1; this._desktopManager.activeFileItem = this; if (button === 3) { this._doButtonThreePressed( button, nPress, X, Y, x, y, shiftPressed, controlPressed ); } else if (button === 1) { this._doButtonOnePressed( button, nPress, X, Y, x, y, shiftPressed, controlPressed ); } } _onLongPressButton( _actor, _X, _Y, _x, _y, _button, _shiftPressed, _controlPressed) { // Handle long press events here if needed } _onReleaseButton(actor, X, Y, x, y, shiftPressed, controlPressed) { let button = actor.get_current_button(); this._grid?.makeTopLayerOnGrid(this); if (button === 1) { this._doButtonOneReleased( button, X, Y, x, y, shiftPressed, controlPressed ); } } _doButtonThreePressed( button, nPress, X, Y, x, y, shiftPressed, controlPressed ) { if (!this._isSelected) this._dragManager.selected(this, this.Enums.Selection.RIGHT_BUTTON); this._destroyToolTip(); this._desktopManager.fileItemMenu.showMenu( this, button, X, Y, x, y, shiftPressed, controlPressed ); } _doButtonOnePressed( button, nPress, X, Y, x, y, shiftPressed, controlPressed ) { if (nPress === 1) { if (shiftPressed || controlPressed) { this._dragManager.selected( this, this.Enums.Selection.WITH_SHIFT ); } else { this._dragManager.selected( this, this.Enums.Selection.ALONE ); } } } _doButtonOneReleased( // eslint-disable-next-line no-unused-vars button, nPressX, Y, x, y, shiftPressed, controlPressed) { } /** ********************* * Drag and Drop * ***********************/ _onEnter() { if (!this._grid) return true; if (this.Prefs.CLICK_POLICY_SINGLE) { let window = this._grid._window; if (window) window.set_cursor(Gdk.Cursor.new_from_name('hand', null)); } return false; } _onLeave() { if (!this._grid) return true; if (this.Prefs.CLICK_POLICY_SINGLE) { let window = this._grid._window; if (window) window.set_cursor(Gdk.Cursor.new_from_name('default', null)); } return false; } _showToolTip() { if (this._toolTipTimer) return; this._toolTipTimer = GLib.timeout_add( GLib.PRIORITY_DEFAULT, this.Enums.TOOLTIP_HOVER_TIMEOUT, () => { this._desktopManager.fileItemMenu.showToolTip(this); this._toolTipTimer = 0; return GLib.SOURCE_REMOVE; } ); } _destroyToolTip() { if (this._toolTipTimer) { GLib.Source.remove(this._toolTipTimer); this._toolTipTimer = 0; } this._desktopManager.fileItemMenu.hideToolTip(this); } _hasToRouteDragToGrid() { if (this._grid) return true; else return false; } _updateDragStatus(context, time) { if (this.DesktopIconsUtil .getModifiersInDnD(context, Gdk.ModifierType.CONTROL_MASK) ) Gdk.drag_status(context, Gdk.DragAction.COPY, time); else Gdk.drag_status(context, Gdk.DragAction.MOVE, time); } setHighLighted() { if (!this._iconContainer .get_css_classes() .includes('desktop-icons-selected') ) this._iconContainer.add_css_class('desktop-icons-selected'); if (!this._labelContainer .get_css_classes() .includes('desktop-icons-selected') ) this._labelContainer.add_css_class('desktop-icons-selected'); if (!this.container .get_css_classes() .includes('desktop-icons-selected') ) this.container.add_css_class('desktop-icons-selected'); } setUnHighLighted() { if (this._iconContainer .get_css_classes() .includes('desktop-icons-selected') ) this._iconContainer.remove_css_class('desktop-icons-selected'); if (this._labelContainer .get_css_classes() .includes('desktop-icons-selected') ) this._labelContainer.remove_css_class('desktop-icons-selected'); if (this.container .get_css_classes() .includes('desktop-icons-selected') ) this.container.remove_css_class('desktop-icons-selected'); } highLightDropTarget() { if (this._hasToRouteDragToGrid()) { this._grid.receiveMotion(this._x1, this._y1, true); return; } this.setHighLighted(); this._grid?.highLightGridAt(this._x1, this._y1); } unHighLightDropTarget() { if (this._hasToRouteDragToGrid()) { this._grid?.receiveLeave(); return; } this.setUnHighLighted(); this._grid?.unHighLightGrids(); } setSelected() { this._isSelected = true; this._setSelectedStatus(); } unsetSelected() { this._isSelected = false; this._setSelectedStatus(); } toggleSelected() { this._isSelected = !this._isSelected; this._setSelectedStatus(); } _setSelectedStatus() { if (this._isSelected) { this.setHighLighted(); this.container.grab_focus(); } if (!this._isSelected) this.setUnHighLighted(); } keyboardSelected() { if (!this._iconContainer.get_css_classes().includes('mimic-hovered')) { this._iconContainer.add_css_class('mimic-hovered'); this._labelContainer.add_css_class('mimic-hovered'); } if (!this.container.get_css_classes().includes('keyboard-selected')) this.container.add_css_class('keyboard-selected'); this._keyboardSelected = true; } keyboardUnSelected() { if (this._iconContainer.get_css_classes().includes('mimic-hovered')) { this._iconContainer.remove_css_class('mimic-hovered'); this._labelContainer.remove_css_class('mimic-hovered'); } if (this.container.get_css_classes().includes('keyboard-selected')) this.container.remove_css_class('keyboard-selected'); this._keyboardSelected = false; } get KeyboardSelected() { return this._keyboardSelected; } // eslint-disable-next-line no-unused-vars receiveDrop(x, y, selection, info) { } _dropCapable() { return false; } /** ********************* * Icon Rendering * ***********************/ async updateIcon() { await this._updateIcon().catch(e => { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { console.error(e, `Exception while updating ${this._getVisibleName ? this._getVisibleName() : 'an icon'}: ${e.message}`); } }); } async _updateIcon(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._updateIconCancellable) this._updateIconCancellable.cancel(); this._updateIconCancellable = cancellable; if (this.Prefs.darkText) { this._label.remove_css_class('file-label'); this._label.add_css_class('file-label-dark'); } else { this._label.remove_css_class('file-label-dark'); this._label.add_css_class('file-label'); } try { const customIcon = this._fileInfo.get_attribute_as_string('metadata::custom-icon'); if (customIcon && (customIcon !== '')) { const customIconFile = Gio.File.new_for_uri(customIcon); if (await this._loadImageAsIcon(customIconFile, cancellable)) return; } if (this.thumbnailFile && (this.thumbnailFile !== '')) { const customIconFile = Gio.File.new_for_path(this.thumbnailFile); if (await this.FileUtils.queryExists(customIconFile)) { const loadedImage = await this._loadImageAsIcon(customIconFile, cancellable); if (loadedImage | this._destroyed) return; } } } catch (error) { if (error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw error; console.error(error, `Error while updating icon: ${error.message}`); } if (this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_TRASH) { let pixbuf = this._createEmblemedIcon(this._fileInfo.get_icon(), null); if (cancellable.is_cancelled()) return; this._icon.set_paintable(pixbuf); return; } let iconSet = false; if (this.Prefs.showImageThumbnails) { try { if (!this.thumbnail) { this.thumbnail = await this.ThumbnailLoader.getThumbnail( this, cancellable ); } if (this.thumbnail !== null) { const thumbnailFile = Gio.File.new_for_path(this.thumbnail); iconSet = await this._loadImageAsIcon(thumbnailFile, cancellable); } } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw e; console.error( e, `Error while generating thumbnail: ${e.message}` ); } } if (!iconSet) { let iconPaintable; if (this._isBrokenSymlink) { iconPaintable = this._createEmblemedIcon(null, 'text-x-generic'); } else if (this._desktopFile && this._desktopFile.has_key('Icon')) { iconPaintable = this._createEmblemedIcon( null, this._desktopFile.get_string('Icon') ); } else { iconPaintable = this._createEmblemedIcon(this._getDefaultIcon(), null); } if (cancellable.is_cancelled()) return; this._icon.set_paintable(iconPaintable); } if (cancellable === this._updateIconCancellable) this._updateIconCancellable = null; } _getDefaultIcon() { return this._fileInfo.get_icon(); } async _loadImageAsIcon(imageFile, cancellable) { try { const [thumbnailData] = await imageFile.load_bytes_async(cancellable); const iconTexture = Gdk.Texture.new_from_bytes(thumbnailData); let width = this.Prefs.DesiredWidth; let height = this.Prefs.IconSize; const aspectRatio = iconTexture.width / iconTexture.height; if ((width / height) > aspectRatio) width = height * aspectRatio; else height = width / aspectRatio; const iconPaintableSnapshot = Gtk.Snapshot.new(); iconTexture.snapshot( iconPaintableSnapshot, Math.floor(width), Math.floor(height) ); let icon = iconPaintableSnapshot.to_paintable(null); icon = this._addEmblemsToIconIfNeeded(icon); this._icon.set_paintable(icon); return true; } catch (e) { if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) throw e; console .error(e, `Error while loading ${imageFile.get_uri()} as icon`); return false; } } _addEmblem(iconPaintable, emblem = null, position = 0) { if (!emblem) return iconPaintable; const scale = this._icon.get_scale_factor(); let ratio; switch (this.Prefs.IconSize) { case 36: ratio = 3; break; case 48: ratio = 3; break; case 64: ratio = 4; break; case 96: ratio = 5; } const finalSize = Math.floor(this.Prefs.IconSize / ratio) * scale; const iconWidth = iconPaintable.get_intrinsic_width(); const iconHeight = iconPaintable.get_intrinsic_height(); const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); const emblemIcon = theme.lookup_by_gicon( emblem, finalSize / scale, scale, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_SIZE | Gtk.IconLookupFlags.FORCE_SYMBOLIC ); const emblemWidth = emblemIcon.get_intrinsic_width(); const emblemHeight = emblemIcon.get_intrinsic_height(); const emblemSnapshot = Gtk.Snapshot.new(); const rect = new Graphene.Rect(); rect.init(2, 2, emblemWidth - 4, emblemHeight - 4); const rr = new Gsk.RoundedRect(); rr.init_from_rect(rect, 5); emblemSnapshot.append_color(this.Prefs.hoverColor, rect); emblemIcon.snapshot(emblemSnapshot, emblemWidth, emblemHeight); const emblemNode = emblemSnapshot.to_node(); const iconPaintableSnapshot = Gtk.Snapshot.new(); iconPaintable.snapshot(iconPaintableSnapshot, iconWidth, iconHeight); iconPaintableSnapshot.save(); if (!this._emblemX) { const desiredWidth = this.Prefs.DesiredWidth - 8; const estimatedWidth = iconWidth + 2 * emblemWidth; const finalWidth = Math.min(desiredWidth, estimatedWidth); this._emblemX = Math.round((iconWidth + finalWidth) / 2 - emblemWidth); } const x = this._emblemX; const y = Math.max(0, position * (emblemHeight + 1)); iconPaintableSnapshot.translate(new Graphene.Point({x, y})); iconPaintableSnapshot.append_node(emblemNode); iconPaintableSnapshot.restore(); return iconPaintableSnapshot.to_paintable(null); } _addEmblemsToIconIfNeeded(iconPaintable) { return this._addEmblem(iconPaintable); } _createEmblemedIcon(icon, iconName) { if (icon === null) { if (GLib.path_is_absolute(iconName)) { try { let iconFile = Gio.File.new_for_commandline_arg(iconName); icon = new Gio.FileIcon({file: iconFile}); } catch (e) { icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName); } } else { try { icon = Gio.Icon.new_for_string(iconName); } catch (e) { icon = Gio.ThemedIcon.new_with_default_fallbacks(iconName); } } } let theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); const scale = this._icon.get_scale_factor(); let iconPaintable = null; try { iconPaintable = theme.lookup_by_gicon( icon, this.Prefs.IconSize, scale, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_SIZE ); } catch (e) { iconPaintable = theme.lookup_icon( 'text-x-generic', [], this.Prefs.IconSize, scale, Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_SIZE ); } return this._addEmblemsToIconIfNeeded(iconPaintable); } /** ********************* * Getters and setters * ***********************/ get _desktopDir() { return this._desktopManager.desktopDir; } get state() { return this._state; } set state(state) { if (state === this._state) return; this._state = state; } set opacity(number) { this.container.set_opacity(number); } get dropCapable() { return this._dropCapable(); } get isDrive() { return this._fileTypeEnum === this.Enums.FileType.EXTERNAL_DRIVE; } get isSelected() { return this._isSelected; } get isSpecial() { return this._isSpecial; } get savedCoordinates() { return this._savedCoordinates; } get normalCoordinates() { return this._normalCoordinates; } get monitorIndex() { return this._monitorIndex; } get dropCoordinates() { return this._dropCoordinates; } get isEncrypted() { if (this._isEncrypted === undefined) return false; return this._isEncrypted; } get column() { return this._column; } get row() { return this._row; } set column(num) { this._column = num; } set row(num) { this._row = num; } set dropCoordinates(pos) { this.writeDropCoordinates(pos); } set savedCoordinates(pos) { this.writeSavedCoordinates(pos); } set temporarySavedPosition(pos) { this._savedCoordinates = pos; } set temporaryMonitorIndex(index) { this._monitorIndex = index; } }; Signals.addSignalMethods(DesktopIconItem.prototype);