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

3142 lines
97 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 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 {GObject, Gtk, Gdk, GLib, Gio, Graphene, Gsk, Adw} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {DesktopGrid};
// eslint-disable-next-line no-unused-vars
const DisplayGrid = class {
constructor(params) {
const {
desktopManager,
desktopName,
desktopDescription,
asDesktop,
hidden = false,
desktopIndex = 0,
} = params;
this._destroying = false;
this._desktopManager = desktopManager;
this._mainapp = desktopManager.mainApp;
this._dragManager = desktopManager.dragManager;
this.Prefs = this._desktopManager.Prefs;
this.DesktopIconsUtil = this._desktopManager.DesktopIconsUtil;
this.DBusUtils = this._desktopManager.DBusUtils;
this.Enums = this._desktopManager.Enums;
this.elementSpacing = this.Enums.GRID_ELEMENT_SPACING;
this.gridPadding = this.Enums.GRID_PADDING;
this._desktopName = desktopName;
this._desktopIndex = desktopIndex;
this._asDesktop = asDesktop;
this._desktopDescription = desktopDescription;
this._hidden = hidden;
this._using_X11 = this.DesktopIconsUtil.usingX11();
this.directoryOpenTimer = null;
this.windowGlobalRectangle = new Gdk.Rectangle();
this._updateWindowGeometry();
this._updateUnscaledHeightWidthMargins();
this._createGrids();
this._window =
new Gtk.ApplicationWindow(
{
application: desktopManager.mainApp,
'title': desktopName,
}
);
this._window.update_property(
[Gtk.AccessibleProperty.LABEL],
[_('Desktop Icons')]
);
if (this._asDesktop) {
this._window.set_decorated(false);
this._window.set_deletable(false);
this._window.set_resizable(false);
// Transparent Background only if this is working as a desktop
this._window.set_name('desktopwindow');
this._window
.set_default_size(this._windowWidth, this._windowHeight);
this._window
.set_size_request(this._windowWidth, this._windowHeight);
this._mappedPromise =
new Promise(resolve => (this._resolveMapped = resolve));
if (!this._using_X11) {
// Wayland Compositer hang on some high resolution
// requires all windows be maximized to map and display
// initially.
this._window.maximize();
}
this._window.connect('map', () => {
if (!this._resolveMapped)
return;
this._resolveMapped(true);
this._resolveMapped = null;
// Maximize however creates an error where the window can
// be moved by the user by dragging down on top panel.
// So we unmaximize all windows after they are mapped
// as maximization is not needed anymore.
this._window.unmaximize();
});
} else {
// Opaque black test window
this._window.set_name('testwindow');
}
// Remove any other css classes, even if applied by other apps later
this._window.set_css_classes(['background']);
this._window.connect('notify::css_classes', () => {
this._window.set_css_classes(['background']);
});
this._window.connect(
'close-request',
() => {
if (this._destroying)
return false;
if (this._asDesktop) {
// Do not destroy window when closing if the instance
// is working as desktop
return true;
} else {
// Exit if this instance is working as an
// stand-alone window
this._desktopManager.terminateProgram();
return false;
}
}
);
// New: one fixed root that contains both layers
this._rootFixed = new Gtk.Fixed();
this._rootFixed.set_size_request(this._windowWidth, this._windowHeight);
this._container = new Gtk.Fixed();
this._containerContext = this._container.get_style_context();
this._container.set_size_request(this._windowWidth, this._windowHeight);
this._containerContext.add_class('unhighlightdroptarget');
// icon grid goes in rootFixed
this._rootFixed.put(this._container, 0, 0);
this._overlay = new Gtk.Overlay();
this._overlay.set_hexpand(true);
this._overlay.set_vexpand(true);
this._overlay.set_child(this._rootFixed);
this._window.set_child(this._overlay);
this.gridGlobalRectangle = new Gdk.Rectangle();
this._selectedList = null;
this._setGridStatus();
this._updateGridRectangle();
}
ensureMapped() {
// show/present only here after the window is fully set up to
// and to avoit commiting content too early so that the shell
// errors on commiting first frame before acknowleding ack from wayland
// compositor.
this._window.set_visible(!this._hidden);
this._window.present();
return this._mappedPromise;
}
ensureAllocationComplete() {
if (this._allocPromise)
return this._allocPromise;
const w = this._container;
this._allocPromise = new Promise(resolve => {
let tickId = 0;
let stableFrames = 0;
const cleanup = () => {
if (tickId)
w.remove_tick_callback(tickId);
this._allocPromise = null;
};
const isAllocated = () => {
const aw = w.get_allocated_width();
const ah = w.get_allocated_height();
return aw > 0 && ah > 0;
};
if (isAllocated()) {
this._overlay.queue_draw();
resolve();
cleanup();
return;
}
tickId = w.add_tick_callback(() => {
if (isAllocated())
stableFrames++;
else
stableFrames = 0;
if (stableFrames >= 2) {
cleanup();
this._overlay.queue_draw();
resolve();
return GLib.SOURCE_REMOVE;
}
return GLib.SOURCE_CONTINUE;
});
});
return this._allocPromise;
}
setErrorState() {
this._window.set_name('errorstate');
}
unsetErrorState() {
if (this._asDesktop)
this._window.set_name('desktopwindow');
else
this._window.set_name('testwindow');
}
hide() {
this._window.hide();
this._hidden = true;
}
show() {
this._window.present();
this._hidden = false;
}
queue_draw() {
this._container.queue_draw();
this._overlay.queue_draw();
this._window.queue_draw();
}
// Establish and update window geometry, establish and update
// grid for the desktop icons
updateGridDescription(desktopDescription) {
this._desktopDescription = desktopDescription;
}
_updateWindowGeometry() {
this._zoom = this._desktopDescription.zoom;
this._x = this._desktopDescription.x;
this._y = this._desktopDescription.y;
this._monitor = this._desktopDescription.monitorIndex;
this._sizer = this._zoom;
if (this._asDesktop) {
if (this._using_X11)
this._sizer = Math.ceil(this._zoom);
else if (this.Prefs.fractionalScaling)
this._sizer = 1;
}
this._windowWidth =
Math.floor(this._desktopDescription.width / this._sizer);
this._windowHeight =
Math.floor(this._desktopDescription.height / this._sizer);
this.windowGlobalRectangle.x = this._x;
this.windowGlobalRectangle.y = this._y;
this.windowGlobalRectangle.width = this._windowWidth;
this.windowGlobalRectangle.height = this._windowHeight;
}
resizeWindow() {
this._updateWindowGeometry();
this._desktopName = `@!${this._x},${this._y};BDHF`;
this._window.set_title(this._desktopName);
this._window.set_default_size(this._windowWidth, this._windowHeight);
this._window.set_size_request(this._windowWidth, this._windowHeight);
this.scale = this._window.get_scale_factor();
}
_updateUnscaledHeightWidthMargins() {
this._marginLeftHiddenObject = false;
this._marginRightHiddenObject = false;
this._marginTopHiddenObject = false;
this._marginBottomHiddenObject = false;
this._marginTop = this._desktopDescription.marginTop + this.gridPadding;
if (this._marginTop > 1000) {
this._marginTopHiddenObject = true;
this._marginTop -= 1000;
}
this._marginBottom =
this._desktopDescription.marginBottom + this.gridPadding;
if (this._marginBottom > 1000) {
this._marginBottomHiddenObject = true;
this._marginBottom -= 1000;
}
this._marginLeft =
this._desktopDescription.marginLeft + this.gridPadding;
if (this._marginLeft > 1000) {
this._marginLeftHiddenObject = true;
this._marginLeft -= 1000;
}
this._marginRight =
this._desktopDescription.marginRight + this.gridPadding;
if (this._marginRight > 1000) {
this._marginRightHiddenObject = true;
this._marginRight -= 1000;
}
this._width =
this._desktopDescription.width -
this._marginLeft -
this._marginRight;
this._height =
this._desktopDescription.height -
this._marginTop -
this._marginBottom;
}
_createGrids() {
this._width = Math.floor(this._width / this._sizer);
this._height = Math.floor(this._height / this._sizer);
this._marginTop = Math.floor(this._marginTop / this._sizer);
this._marginBottom = Math.floor(this._marginBottom / this._sizer);
this._marginLeft = Math.floor(this._marginLeft / this._sizer);
this._marginRight = Math.floor(this._marginRight / this._sizer);
this._maxColumns =
Math.floor(
this._width /
(this.Prefs.DesiredWidth + 4 * this.elementSpacing)
);
this._maxRows =
Math.floor(
this._height /
(this.Prefs.DesiredHeight + 4 * this.elementSpacing)
);
this._elementWidth = Math.floor(this._width / this._maxColumns);
this._elementHeight = Math.floor(this._height / this._maxRows);
}
_updateGridRectangle() {
this.gridGlobalRectangle.x = this._x + this._marginLeft;
this.gridGlobalRectangle.y = this._y + this._marginTop;
this.gridGlobalRectangle.width = this._width;
this.gridGlobalRectangle.height = this._height;
}
_sizeContainer(widget) {
widget.margin_top = this._marginTop;
widget.margin_bottom = this._marginBottom;
const leftToRight = widget.get_direction() === Gtk.TextDirection.LTR;
if (leftToRight) {
widget.margin_start = this._marginLeft;
widget.margin_end = this._marginRight;
} else {
widget.margin_start = this._marginRight;
widget.margin_end = this._marginLeft;
}
}
_setGridStatus() {
this._fileItems = new Map();
this._gridStatus = new Map();
for (let y = 0; y < this._maxRows; y++) {
for (let x = 0; x < this._maxColumns; x++)
this._gridStatus.set(y * this._maxColumns + x, new Set());
}
}
resizeGrid() {
this._updateUnscaledHeightWidthMargins();
this._createGrids();
// Ensure event targets cover the full window even when no icons/widgets
this._container.set_size_request(this._windowWidth, this._windowHeight);
this._rootFixed.set_size_request(this._windowWidth, this._windowHeight);
this._sizeContainer(this._container);
this._updateGridRectangle();
this._setGridStatus();
}
destroy() {
this._destroying = true;
this._window.destroy();
}
recomputeGridPosition(column, row) {
if (column > this._maxColumns)
return [this._x, this._y];
if (row > this._maxRows)
return [this._x, this._y];
const [localX, localY] =
this._getLocalCoordinatesForGrid(column, row);
const [newGlobalX, newGlobalY] =
this.coordinatesLocalToGlobal(localX, localY);
return [newGlobalX, newGlobalY];
}
// Compute correct position for pop up menus relative to
// margins to prevent going under/over margins
getIntelligentPosition(gdkRectangle) {
if (!this._marginLeftHiddenObject &&
!this._marginRightHiddenObject &&
!this._marginTopHiddenObject &&
!this._marginBottomHiddenObject)
return null;
var clickLocation = 'center';
if (this._marginLeft > 0 &&
(gdkRectangle.x < (this._marginLeft * 2))
)
clickLocation = 'left';
if (this._marginRight > 0 &&
(
gdkRectangle.x + gdkRectangle.width >
(this._windowWidth - this._marginRight * 2.5)
)
)
clickLocation = 'right';
if (this._marginBottom > 0 &&
(
gdkRectangle.y + gdkRectangle.height >
(this._windowHeight - this._marginBottom * 2)
)
) {
switch (clickLocation) {
case 'left':
clickLocation = 'bottomLeft';
break;
case 'right':
clickLocation = 'bottomRight';
break;
default:
clickLocation = 'bottom';
}
}
if (this._marginTop > 0 &&
(
gdkRectangle.y < (this._marginTop * 2)
)
) {
switch (clickLocation) {
case 'left':
clickLocation = 'topLeft';
break;
case 'right':
clickLocation = 'topRight';
break;
default:
clickLocation = 'top';
}
}
var returnvalue;
switch (clickLocation) {
case 'left':
if (this._marginLeftHiddenObject)
returnvalue = Gtk.PositionType.RIGHT;
else
returnvalue = null;
break;
case 'right':
if (this._marginRightHiddenObject)
returnvalue = Gtk.PositionType.LEFT;
else
returnvalue = null;
break;
case 'top':
if (this._marginTopHiddenObject)
returnvalue = Gtk.PositionType.BOTTOM;
else
returnvalue = null;
break;
case 'bottom':
if (this._marginBottomHiddenObject)
returnvalue = Gtk.PositionType.TOP;
else
returnvalue = null;
break;
case 'center':
returnvalue = null;
break;
case 'bottomRight':
if (this._marginBottomHiddenObject &&
this._marginRightHiddenObject) {
returnvalue = Gtk.PositionType.LEFT;
break;
}
if (this._marginBottomHiddenObject) {
returnvalue = Gtk.PositionType.TOP;
break;
}
if (this._marginRightHiddenObject) {
returnvalue = Gtk.PositionType.LEFT;
break;
}
break;
case 'bottomLeft':
if (this._marginBottomHiddenObject &&
this._marginLeftHiddenObject) {
returnvalue = Gtk.PositionType.RIGHT;
break;
}
if (this._marginBottomHiddenObject) {
returnvalue = Gtk.PositionType.TOP;
break;
}
if (this._marginLeftHiddenObject) {
returnvalue = Gtk.PositionType.RIGHT;
break;
}
break;
case 'topRight':
if (this._marginTopHiddenObject && this._marginRightHiddenObject) {
returnvalue = Gtk.PositionType.LEFT;
break;
}
if (this._marginTopHiddenObject) {
returnvalue = Gtk.PositionType.BOTTOM;
break;
}
if (this._marginRightHiddenObject) {
returnvalue = Gtk.PositionType.LEFT;
break;
}
break;
case 'topLeft':
if (this._marginTopHiddenObject && this._marginLeftHiddenObject) {
returnvalue = Gtk.PositionType.RIGHT;
break;
}
if (this._marginTopHiddenObject) {
returnvalue = Gtk.PositionType.BOTTOM;
break;
}
if (this._marginLeftHiddenObject) {
returnvalue = Gtk.PositionType.RIGHT;
break;
}
break;
default:
returnvalue = null;
}
return returnvalue;
}
// Functions for computing postion/Geometry
_getColumnRowFromLocal(x, y) {
// Returns the column, row of the grid that holds the local x, y
let placeX = Math.floor(x / this._elementWidth);
let placeY = Math.floor(y / this._elementHeight);
placeX = this.DesktopIconsUtil.clamp(placeX, 0, this._maxColumns - 1);
placeY = this.DesktopIconsUtil.clamp(placeY, 0, this._maxRows - 1);
return [placeX, placeY];
}
_getGridLocalCoordinates(x, y) {
// Returns the local grid coordinates of top left rectangle
// vertex of the grid that has local x,y
const [column, row] = this._getColumnRowFromLocal(x, y);
return this._getLocalCoordinatesForGrid(column, row);
}
_getLocalCoordinatesForGrid(column, row) {
const localX = Math.floor(this._width * column / this._maxColumns);
const localY = Math.floor(this._height * row / this._maxRows);
return [localX, localY];
}
getDistance(x) {
// Returns the distance to the middle point of this grid from X //
return Math.pow(x - (this._x + this._windowWidth * this._zoom / 2), 2) +
Math.pow(x - (this._y + this._windowHeight * this._zoom / 2), 2);
}
_coordinatesGlobalToLocal(X, Y, widget = null) {
const [windowX, windowY] = this._coordinatesGlobalToWindow(X, Y);
const sourcePoint = new Graphene.Point({x: windowX, y: windowY});
if (!widget)
widget = this._container;
const [found, targetPoint] =
this._window.compute_point(widget, sourcePoint);
if (!found)
return [0, 0];
return [targetPoint.x, targetPoint.y];
}
_coordinatesGlobalToWindow(X, Y) {
X -= this._x;
Y -= this._y;
return [X, Y];
}
_coordinatesWidgetToWidget(x, y, widget1, widget2) {
const sourcePoint = new Graphene.Point({x, y});
const [found, targetPoint] =
widget1.compute_point(widget2, sourcePoint);
if (!found)
return [0, 0];
return [targetPoint.x, targetPoint.y];
}
coordinatesLocalToWindow(x, y, widget = null) {
if (!widget)
widget = this._container;
const sourcePoint = new Graphene.Point({x, y});
const [found, targetPoint] =
widget.compute_point(this._window, sourcePoint);
if (!found)
return [0, 0];
return [targetPoint.x, targetPoint.y];
}
coordinatesLocalToGlobal(x, y, widget = null) {
const [X, Y] = this.coordinatesLocalToWindow(x, y, widget);
return [X + this._x, Y + this._y];
}
coordinatesBelongToThisGrid(X, Y) {
const checkRectangle =
new Gdk.Rectangle(
{
x: X,
y: Y,
width: 1,
height: 1,
}
);
return this.gridGlobalRectangle.intersect(checkRectangle)[0];
}
coordinatesBelongToThisGridWindow(X, Y) {
const checkRectangle =
new Gdk.Rectangle(
{
x: X,
y: Y,
width: 1,
height: 1,
}
);
return this.windowGlobalRectangle.intersect(checkRectangle)[0];
}
getGlobaltoLocalRectangle(gdkRectangle) {
const [X, Y] =
this._coordinatesGlobalToLocal(gdkRectangle.x, gdkRectangle.y);
return new Gdk.Rectangle(
{
x: X,
y: Y,
width: gdkRectangle.width,
height: gdkRectangle.height,
}
);
}
getCoordinatesOfGridContaining(X, Y, globalCoordinates = false) {
// returns the local or global coordinates if requested,
// of the local grid rectangle top left vertex that contains x, y
if (this.coordinatesBelongToThisGrid(X, Y)) {
const [x, y] = this._coordinatesGlobalToLocal(X, Y);
if (globalCoordinates) {
const a =
this._elementWidth *
Math.floor((x / this._elementWidth) + 0.5);
const b =
this._elementHeight *
Math.floor((y / this._elementHeight) + 0.5);
return this.coordinatesLocalToGlobal(a, b);
} else {
return this._getGridLocalCoordinates(x, y);
}
} else {
return null;
}
}
// Functions to query and set grid use by Icons and files
_fileAtColumnRow(column, row) {
// only works for grid placement of icons,
// with free placements there maybe multiple fileItems per grid
const setOfFileItemsOnGridNumber =
this._gridStatus.get(row * this._maxColumns + column);
if (!this.Prefs.freePositionIcons && setOfFileItemsOnGridNumber.size) {
for (const fileItem of setOfFileItemsOnGridNumber.keys())
return fileItem;
}
return null;
}
_fileAt(x, y) {
if (!this.Prefs.freePositionIcons) {
const [column, row] = this._getColumnRowFromLocal(x, y);
return this._fileAtColumnRow(column, row);
}
const widgetAtPointer =
this._container.pick(x, y, Gtk.PickFlags.GTK_PICK_DEFAULT);
if (widgetAtPointer === this._container)
return null;
let fileItemFound = null;
for (const fileItem of this._fileItems.keys()) {
const [widgetX, widgetY] =
this._coordinatesWidgetToWidget(
x, y,
this._container,
fileItem.container
);
if (widgetX === 0 && widgetY === 0)
continue;
const localWidget =
fileItem.container.pick(
widgetX,
widgetY,
Gtk.PickFlags.GTK_PICK_DEFAULT
);
if (localWidget === widgetAtPointer) {
fileItemFound = fileItem;
break;
}
}
return fileItemFound;
}
isAvailable() {
// Returns true if there is an available slot in the grid
let isFree = false;
for (const [, setOfFileItemsOnGridNumber] of this._gridStatus.entries()
) {
if (!setOfFileItemsOnGridNumber.size) {
isFree = true;
break;
}
}
return isFree;
}
_setUseColumnRowOverlappingThis(fileItem, column, row, X, Y) {
this._setGridUse(column, row, fileItem);
const Xr = X + this._elementWidth - 2;
const Yr = Y + this._elementHeight - 2;
const [xr, yr] = this._coordinatesGlobalToLocal(Xr, Yr);
const [bottomRightColumn, bottomRightRow] =
this._getColumnRowFromLocal(xr, yr);
if (bottomRightColumn !== column &&
bottomRightRow !== row) {
this._setGridUse(bottomRightColumn, bottomRightRow, fileItem);
this._setGridUse(column, bottomRightRow, fileItem);
this._setGridUse(bottomRightColumn, row, fileItem);
return;
}
if (bottomRightColumn === column && bottomRightRow !== row) {
this._setGridUse(column, bottomRightRow, fileItem);
return;
}
if (bottomRightColumn !== column && bottomRightRow === row)
this._setGridUse(bottomRightColumn, row, fileItem);
}
_isEmptyAt(column, row) {
// returns if grid at column row has a file or not
const setOfFileItemsOnGridNumber =
this._gridStatus.get(row * this._maxColumns + column);
return setOfFileItemsOnGridNumber.size === 0;
}
_gridInUse(x, y) {
// returns if the local grid containing local coordinates
// x, y has a file assigned.
const [placeX, placeY] = this._getColumnRowFromLocal(x, y);
return !this._isEmptyAt(placeX, placeY);
}
_setGridUse(column, row, fileItem) {
const setOfFileItemsOnGridNumber =
this._gridStatus.get(row * this._maxColumns + column);
setOfFileItemsOnGridNumber.add(fileItem);
}
_getEmptyPlaceClosestTo(x, y, coordinatesAction, reverseHorizontal) {
// returns the column row of empty grid available at global X, Y
let cornerInversion = this.Prefs.StartCorner;
if (reverseHorizontal)
cornerInversion[0] = !cornerInversion[0];
const [placeX, placeY] = this._getColumnRowFromLocal(x, y);
if (this._isEmptyAt(placeX, placeY) &&
coordinatesAction !== this.Enums.StoredCoordinates.ASSIGN)
return [placeX, placeY];
let found = false;
let resColumn = null;
let resRow = null;
let minDistance = Infinity;
let column, row;
for (let tmpColumn = 0; tmpColumn < this._maxColumns; tmpColumn++) {
if (cornerInversion[0])
column = this._maxColumns - tmpColumn - 1;
else
column = tmpColumn;
for (let tmpRow = 0; tmpRow < this._maxRows; tmpRow++) {
if (cornerInversion[1])
row = this._maxRows - tmpRow - 1;
else
row = tmpRow;
if (!this._isEmptyAt(column, row))
continue;
let proposedX = column * this._elementWidth;
let proposedY = row * this._elementHeight;
if (coordinatesAction === this.Enums.StoredCoordinates.ASSIGN)
return [column, row];
let distance =
this.DesktopIconsUtil.distanceBetweenPoints(
proposedX,
proposedY,
x, y
);
if (distance < minDistance) {
found = true;
minDistance = distance;
resColumn = column;
resRow = row;
}
}
}
if (!found)
throw new Error('No available space on the monitor');
return [resColumn, resRow];
}
// Finally the actual code that places and removes icons on the desktop
_addFileItemToGrid(fileItem, column, row, coordinatesAction) {
if (this._destroying)
return;
let [localX, localY] = this._getLocalCoordinatesForGrid(column, row);
localX += this.elementSpacing;
localY += this.elementSpacing;
this._container.put(fileItem.container, localX, localY);
this._setGridUse(column, row, fileItem);
fileItem.column = column;
fileItem.row = row;
this._fileItems.set(fileItem, [localX, localY]);
const [X, Y] = this.coordinatesLocalToGlobal(localX, localY);
fileItem.setCoordinates(
X,
Y,
this._elementWidth - 2 * this.elementSpacing,
this._elementHeight - 2 * this.elementSpacing,
this.elementSpacing,
this
);
/* If this file is new in the Desktop and hasn't yet
* fixed coordinates, store the new position to ensure
* that the next time it will be shown in the same position.
* Also store the new position if it has been moved by the user,
* and not triggered by a screen change.
*/
if ((fileItem.savedCoordinates === null) ||
(coordinatesAction === this.Enums.StoredCoordinates.OVERWRITE)) {
const [normalizedX, normalizedY] =
this.getNormalizedCoordinates(localX, localY);
const array = [X, Y, normalizedX, normalizedY, this._monitor];
fileItem.writeSavedCoordinates(array);
}
}
removeItem(fileItem) {
if (this._fileItems.has(fileItem))
this._fileItems.delete(fileItem);
this._gridStatus.forEach(
setOfFileItemsOnGridNumber =>
setOfFileItemsOnGridNumber.delete(fileItem)
);
this._container.remove(fileItem.container);
}
_placeIntoPosition(
fileItem,
X, Y,
x, y,
emptycolumn,
emptyrow,
coordinatesAction
) {
// For sanpping to grid
if (fileItem.savedCoordinates == null ||
(fileItem.savedCoordinates[0] === 0 &&
fileItem.savedCoordinates[1] === 0) ||
!this.Prefs.freePositionIcons ||
this.Prefs.keepArranged ||
this.Prefs.keepStacked
) {
this._addFileItemToGrid(
fileItem,
emptycolumn,
emptyrow,
coordinatesAction
);
return;
}
if (this._destroying)
return;
// For free placement
// Make sure the icon lands inside the grid and does not protrude out
const [currentColumn, currentRow] = this._getColumnRowFromLocal(x, y);
let translocated = false;
if (currentColumn === this._maxColumns - 1 &&
x + this._elementWidth > this._width
) {
x = this._width - this._elementWidth;
translocated = true;
}
if (currentRow === this._maxRows - 1 &&
y + this._elementHeight > this._height
) {
y = this._height - this._elementHeight;
translocated = true;
}
if (x < 0) {
x = 0;
translocated = true;
}
if (y < 0) {
y = 0;
translocated = true;
}
// recompute global coordinates from the translocatedd local coordinates
if (translocated)
[X, Y] = this.coordinatesLocalToGlobal(x, y);
this._container.put(fileItem.container, x, y);
this._fileItems.set(fileItem, [x, y]);
fileItem.setCoordinates(X,
Y,
this._elementWidth - 2 * this.elementSpacing,
this._elementHeight - 2 * this.elementSpacing,
this.elementSpacing,
this);
// set column row being used for all four vertices
this._setUseColumnRowOverlappingThis(
fileItem,
currentColumn,
currentRow,
X, Y
);
/* If this file is new in the Desktop and hasn't yet
* fixed coordinates, store the new position to ensure
* that the next time it will be shown in the same position.
* Also store the new position if it has been moved by the user,
* and not triggered by a screen change.
*/
if ((fileItem.savedCoordinates === null) ||
(coordinatesAction === this.Enums.StoredCoordinates.OVERWRITE)
) {
const [normalizedX, normalizedY] =
this.getNormalizedCoordinates(x, y);
const array = [X, Y, normalizedX, normalizedY, this._monitor];
fileItem.writeSavedCoordinates(array);
fileItem.column = null;
fileItem.row = null;
}
}
addFileItemCloseTo(fileItem, X, Y, coordinatesAction) {
const addVolumesOpposite = this.Prefs.AddVolumesOpposite;
const [x, y] = this._coordinatesGlobalToLocal(X, Y);
const [column, row] = this._getEmptyPlaceClosestTo(
x,
y,
coordinatesAction,
fileItem.isDrive && addVolumesOpposite
);
this._placeIntoPosition(
fileItem,
X, Y,
x, y,
column,
row,
coordinatesAction
);
}
makeTopLayerOnGrid(fileItem) {
if (!this.Prefs.freePositionIcons)
return;
const [x, y] = this._fileItems.get(fileItem);
this._container.remove(fileItem.container);
this._container.put(fileItem.container, x, y);
}
getNormalizedCoordinates(x, y) {
return [x / this.normalizedWidth, y / this.normalizedHeight];
}
setNormalizedCoordinates(x, y) {
const newGlobalX = x * this.normalizedWidth;
const newGlobalY = y * this.normalizedHeight;
return [newGlobalX, newGlobalY];
}
get normalizedWidth() {
return this._width;
}
get normalizedHeight() {
return this._height;
}
get monitorIndex() {
return this._monitor;
}
get index() {
return this._desktopIndex;
}
get name() {
return this._desktopName;
}
};
const GridOverlay = GObject.registerClass(
class GridOverlay extends Gtk.Widget {
constructor(grid) {
super({can_target: false});
this._grid = grid;
}
vfunc_snapshot(snapshot) {
const w = this.get_allocated_width();
const h = this.get_allocated_height();
if (w <= 0 || h <= 0)
return;
this._grid._doDrawOnGrid(snapshot);
// Ensure this widget contributes a node so the frame overwrites stale content.
const rect = new Graphene.Rect();
rect.init(0, 0, 1, 1);
snapshot.append_color(
new Gdk.RGBA({red: 0, green: 0, blue: 0, alpha: 0.001}),
rect
);
}
});
const DrawGrid = class extends DisplayGrid {
constructor(params) {
super(params);
this._drawArea = new GridOverlay(this);
this._drawArea.set_size_request(this._windowWidth, this._windowHeight);
this._sizeContainer(this._drawArea);
this._overlay.add_overlay(this._drawArea);
this._drawArea.set_can_target(false);
this._drawArea.set_visible(true);
}
resizeWindow() {
super.resizeWindow();
this._drawArea.set_size_request(this._windowWidth, this._windowHeight);
}
resizeGrid() {
super.resizeGrid();
this._drawArea.set_size_request(this._windowWidth, this._windowHeight);
this._sizeContainer(this._drawArea);
}
// Functions for drawing on the grid
highLightGridAt(x, y) {
const globalCoordinates = false;
const selected = this.getCoordinatesOfGridContaining(x, y, globalCoordinates);
this._selectedList = [selected];
this.updateOverlay();
}
unHighLightGrids() {
this._selectedList = null;
this.updateOverlay();
}
updateOverlay() {
this._drawArea.queue_draw();
}
_overlayHasContent() {
const hasRubberBand =
this._dragManager.rubberBand &&
this._dragManager.selectionRectangle;
const hasDropRects = (this._selectedList?.length ?? 0) > 0;
return hasRubberBand || hasDropRects;
}
_doDrawOnGrid(snapshot) {
this._doDrawRubberBand(snapshot);
this._doDrawDropRectangles(snapshot);
}
_doDrawRubberBand(snapshot) {
if (!this._dragManager.rubberBand ||
!this._dragManager.selectionRectangle ||
!this.gridGlobalRectangle
.intersect(this._dragManager.selectionRectangle)[0]
)
return;
const [xInit, yInit] =
this._coordinatesGlobalToLocal(
this._dragManager.x1,
this._dragManager.y1
);
const [xFin, yFin] =
this._coordinatesGlobalToLocal(
this._dragManager.x2,
this._dragManager.y2
);
const width = xFin - xInit;
const height = yFin - yInit;
const fillColor = new Gdk.RGBA({
red: this.Prefs.selectColor.red,
green: this.Prefs.selectColor.green,
blue: this.Prefs.selectColor.blue,
alpha: 0.15,
});
const outlineColor = new Gdk.RGBA({
red: this.Prefs.selectColor.red,
green: this.Prefs.selectColor.green,
blue: this.Prefs.selectColor.blue,
alpha: 1.0,
});
this._roundedRectangleDraw(
xInit,
yInit,
width,
height,
snapshot,
fillColor,
outlineColor
);
}
_doDrawDropRectangles(snapshot) {
if (!this.Prefs.showDropPlace || this._selectedList === null)
return;
const fillColor = new Gdk.RGBA({
red: 1.0 - this.Prefs.selectColor.red,
green: 1.0 - this.Prefs.selectColor.green,
blue: 1.0 - this.Prefs.selectColor.blue,
alpha: 0.4,
});
const outlineColor = new Gdk.RGBA({
red: 1.0 - this.Prefs.selectColor.red,
green: 1.0 - this.Prefs.selectColor.green,
blue: 1.0 - this.Prefs.selectColor.blue,
alpha: 1.0,
});
for (const [x, y] of this._selectedList) {
this._rectangleDraw(
x, y,
this._elementWidth,
this._elementHeight,
snapshot,
fillColor,
outlineColor
);
}
}
_rectangleDraw(x, y, width, height, snapshot, fillColor, outlineColor) {
const rect = new Graphene.Rect();
rect.init(x + 0.5, y + 0.5, width, height);
snapshot.append_color(fillColor, rect);
const rr = new Gsk.RoundedRect();
const zero = new Graphene.Size();
zero.init(0, 0);
rr.init(rect, zero, zero, zero, zero);
snapshot.append_border(
rr,
[0.5, 0.5, 0.5, 0.5],
[outlineColor, outlineColor, outlineColor, outlineColor]
);
}
_roundedRectangleDraw(x, y, width, height, snapshot, fillColor, outlineColor) {
const cornerRadius = 5;
const isSquare = width === height;
const tooLarge = cornerRadius * 2 > Math.min(width, height);
const useSquareCorners = cornerRadius <= 0 || isSquare || tooLarge;
const radius =
useSquareCorners
? 0
: Math.min(cornerRadius, width / 2, height / 2);
const rect = new Graphene.Rect();
rect.init(x, y, width, height);
const size = new Graphene.Size();
size.init(radius, radius);
const rr = new Gsk.RoundedRect();
rr.init(rect, size, size, size, size);
if (radius > 0) {
snapshot.push_rounded_clip(rr);
snapshot.append_color(fillColor, rect);
snapshot.pop();
} else {
snapshot.append_color(fillColor, rect);
}
snapshot.append_border(
rr,
[1.0, 1.0, 1.0, 1.0],
[outlineColor, outlineColor, outlineColor, outlineColor]
);
}
};
const ControlGrid = class extends DrawGrid {
constructor(params) {
super(params);
this._addDragControllers();
}
_addDragControllers() {
// Bubble-phase controller: delivers key events to DesktopManager for actions
this._eventKey = Gtk.EventControllerKey.new();
this._eventKey.set_propagation_phase(Gtk.PropagationPhase.BUBBLE);
this._window.add_controller(this._eventKey);
// Capture-phase controller: only caches modifier state, does not invoke actions
this._eventKeyState = Gtk.EventControllerKey.new();
this._eventKeyState.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
this._window.add_controller(this._eventKeyState);
this._eventKey.connect(
'key-pressed',
this._onKeyPress.bind(this)
);
this._eventKeyState.connect(
'key-pressed',
this._onModifierUpdate.bind(this)
);
this._eventKeyState.connect(
'key-released',
this._onModifierClear.bind(this)
);
this._eventMotion = Gtk.EventControllerMotion.new();
this._eventMotion.set_propagation_phase(Gtk.PropagationPhase.BUBBLE);
this._container.add_controller(this._eventMotion);
this._eventMotion.connect(
'motion',
(actor, x, y) => {
if (!this._dragManager.rubberBand)
return false;
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
this._dragManager.onMotion(X, Y);
return false;
}
);
this._buttonClick = new Gtk.GestureClick({button: 0});
this._buttonClick.set_propagation_phase(Gtk.PropagationPhase.BUBBLE);
this._container.add_controller(this._buttonClick);
this._buttonLongClick = new Gtk.GestureLongPress({button: 0});
this._buttonLongClick.set_propagation_phase(Gtk.PropagationPhase.BUBBLE);
this._container.add_controller(this._buttonLongClick);
this._buttonClick.set_exclusive(true);
this._buttonLongClick.set_exclusive(true);
this._buttonClick.group(this._buttonLongClick);
this._longHandled = false;
this._buttonLongClick.connect('pressed', (actor, x, y) => {
this._longHandled = true;
this._doGestureLongPress(actor, x, y);
});
this._buttonLongClick.connect('cancelled', _actor => {
this._longHandled = false;
});
this._buttonClick.connect('pressed', (actor, nPress, x, y) => {
this._doGesturePress(actor, nPress, x, y);
});
this._buttonClick.connect('released', (actor, nPress, x, y) => {
if (this._longHandled)
this._longHandled = false;
this._doGestureRelease(actor, nPress, x, y, this);
});
this._setDropDestination(this._container);
this._setDragSource(this._container);
}
_onKeyPress(actor, keyval, keycode, state) {
this._desktopManager.onKeyPress(
keyval,
keycode,
state,
this
);
}
_onModifierUpdate(_actor, _keyval, _keycode, state) {
this._desktopManager.updateModifierState(state);
}
_onModifierClear() {
this._desktopManager.clearModifierState();
}
_doGesturePress(actor, nPress, x, y) {
if (this._desktopManager.closePopUps())
return;
const button = actor.get_current_button();
const state = this._buttonClick.get_current_event_state();
const isCtrl = (state & Gdk.ModifierType.CONTROL_MASK) !== 0;
const isShift = (state & Gdk.ModifierType.SHIFT_MASK) !== 0;
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const clickItem = this._fileAt(x, y);
if (clickItem && this._clickItemClickable(clickItem, X, Y)) {
clickItem
._onPressButton(actor, nPress, X, Y, x, y, isShift, isCtrl);
return;
}
this._desktopManager
.onPressButton(X, Y, x, y, button, isShift, isCtrl, this);
}
async _doGestureRelease(actor, nPress, x, y, grid) {
const button = actor.get_current_button();
const state = this._buttonClick.get_current_event_state();
const isCtrl = (state & Gdk.ModifierType.CONTROL_MASK) !== 0;
const isShift = (state & Gdk.ModifierType.SHIFT_MASK) !== 0;
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const clickItem = this._fileAt(x, y);
const clickItemClickable = this._clickItemClickable(clickItem, X, Y);
if (clickItemClickable && !this._dragManager.rubberBand) {
clickItem._onReleaseButton(
actor, nPress, X, Y, x, y, isShift, isCtrl);
return;
}
this._dragManager.onReleaseButton(this);
await this._desktopManager
.onReleaseButton(X, Y, x, y, button, isShift, isCtrl, grid)
.catch(logError);
}
_doGestureLongPress(actor, x, y) {
const button = actor.get_current_button();
const state = this._buttonClick.get_current_event_state();
const isCtrl = (state & Gdk.ModifierType.CONTROL_MASK) !== 0;
const isShift = (state & Gdk.ModifierType.SHIFT_MASK) !== 0;
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const clickItem = this._fileAt(x, y);
const clickItemClickable = this._clickItemClickable(clickItem, X, Y);
if (clickItemClickable) {
clickItem
._onLongPressButton(actor, X, Y, x, y, isShift, isCtrl);
return;
}
this._desktopManager
.onLongPressButton(X, Y, x, y, button, isShift, isCtrl, this);
}
_clickItemClickable(clickedItem, X, Y) {
if (!clickedItem)
return false;
const clickRectangle =
new Gdk.Rectangle({x: X, y: Y, width: 1, height: 1});
return clickRectangle.intersect(clickedItem.iconRectangle)[0] ||
clickRectangle.intersect(clickedItem.labelRectangle)[0];
}
_setDropDestination(widget) {
this.gridDropController = new Gtk.DropTargetAsync();
this.gridDropController.set_actions(
Gdk.DragAction.MOVE |
Gdk.DragAction.COPY |
Gdk.DragAction.ASK
);
const desktopAcceptFormats =
Gdk.ContentFormats.new(this.Enums.DndTargetInfo.MIME_TYPES);
const fileItemAcceptFormats =
Gdk.ContentFormats.new([
this.Enums.DndTargetInfo.GNOME_ICON_LIST,
this.Enums.DndTargetInfo.URI_LIST,
]);
const desktopMoveIconsFormat =
Gdk.ContentFormats.new([this.Enums.DndTargetInfo.DING_ICON_LIST]);
const textDropFormat =
Gdk.ContentFormats.new([this.Enums.DndTargetInfo.TEXT_PLAIN]);
const oldNautilusDropFormat =
Gdk.ContentFormats.new([this.Enums.DndTargetInfo.GNOME_ICON_LIST]);
this.gridDropController.set_formats(desktopAcceptFormats);
let acceptFormat = null;
let dropData = null;
this.gridDropController.connect(
'accept',
(actor, drop) => {
if (drop.get_formats().match(desktopAcceptFormats))
return true;
else
return false;
}
);
this.gridDropController.connect(
'drag-enter',
(actor, drop) => {
this.localDrag = true;
drop.status(
Gdk.DragAction.COPY |
Gdk.DragAction.MOVE |
Gdk.DragAction.LINK,
Gdk.DragAction.MOVE
);
return Gdk.DragAction.MOVE;
}
);
this.gridDropController.connect(
'drag-motion',
(actor, drop, x, y) => {
let desktopDropZone = false;
let fileItemDropZone = false;
const fileItem = this._fileAt(x, y);
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const dropRectangle =
new Gdk.Rectangle({x: X, y: Y, width: 1, height: 1});
const desktopMove =
drop.get_formats().match(desktopMoveIconsFormat);
const filesMove =
drop.get_formats().match(fileItemAcceptFormats);
if (fileItem) {
if (!this.Prefs.freePositionIcons)
fileItemDropZone = true;
else if (dropRectangle
.intersect(fileItem.iconRectangle)[0] ||
dropRectangle
.intersect(fileItem.labelRectangle)[0])
fileItemDropZone = true;
if (desktopMove && fileItem._hasToRouteDragToGrid())
fileItemDropZone = false;
}
desktopDropZone = !fileItemDropZone;
this.receiveMotion(x, y, false);
if (fileItemDropZone && !fileItem.dropCapable)
return false;
if (fileItemDropZone && fileItem.dropCapable) {
if (!filesMove)
return false;
if (fileItem._fileExtra !==
this.Enums.FileType.EXTERNAL_DRIVE)
return Gdk.DragAction.MOVE;
if (fileItem._fileExtra ===
this.Enums.FileType.EXTERNAL_DRIVE)
return Gdk.DragAction.COPY;
}
if (desktopDropZone) {
if (desktopMove) {
if (this.Prefs.keepArranged ||
this.Prefs.keepStacked) {
if (this.Prefs.sortSpecialFolders)
return false;
else if (this._desktopManager
.getCurrentSelection()
?.filter(f => !f.isSpecial).length >= 1)
return false;
}
}
return Gdk.DragAction.MOVE;
}
return false;
});
this.gridDropController.connect('drag-leave', () => {
this.localDrag = false;
this._receiveLeave();
});
this.gridDropController.connect('drop', (actor, drop, x, y) => {
const event = {
'parentWindow': this._window,
'timestamp': Gdk.CURRENT_TIME,
};
let desktopDropZone = false;
let fileItemDropZone = false;
const fileItem = this._fileAt(x, y);
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const dropRectangle =
new Gdk.Rectangle({x: X, y: Y, width: 1, height: 1});
const desktopMove =
drop.get_formats().match(desktopMoveIconsFormat);
const filesMove =
drop.get_formats().match(fileItemAcceptFormats);
const oldNautilusMove =
drop.get_formats().match(oldNautilusDropFormat);
let readFormat = Gdk.FileList.$gtype;
if (fileItem) {
if (!this.Prefs.freePositionIcons)
fileItemDropZone = true;
else if (dropRectangle.intersect(fileItem.iconRectangle)[0] ||
dropRectangle.intersect(fileItem.labelRectangle)[0])
fileItemDropZone = true;
if (desktopMove && fileItem._hasToRouteDragToGrid())
fileItemDropZone = false;
}
desktopDropZone = !fileItemDropZone;
const textDrop =
drop.get_formats().match(textDropFormat) &&
!desktopMove &&
!filesMove;
if (textDrop) {
acceptFormat = this.Enums.DndTargetInfo.TEXT_PLAIN;
readFormat = String.$gtype;
}
if (desktopMove)
acceptFormat = this.Enums.DndTargetInfo.DING_ICON_LIST;
if (filesMove && !desktopMove) {
if (oldNautilusMove) {
acceptFormat = this.Enums.DndTargetInfo.GNOME_ICON_LIST;
readFormat = String.$gtype;
} else {
acceptFormat = this.Enums.DndTargetInfo.URI_LIST;
readFormat = String.$gtype;
}
}
let gdkDropAction = drop.get_actions();
if (!Gdk.DragAction.is_unique(gdkDropAction)) {
if (this._using_X11 &&
(gdkDropAction >=
(Gdk.DragAction.COPY | Gdk.DragAction.MOVE)))
gdkDropAction = Gdk.DragAction.MOVE;
else if (gdkDropAction >
(Gdk.DragAction.COPY | Gdk.DragAction.MOVE))
gdkDropAction = Gdk.DragAction.ASK;
}
let gdkReturnAction = Gdk.DragAction.COPY;
if (desktopMove &&
desktopDropZone &&
(gdkDropAction === Gdk.DragAction.MOVE)
) {
let [xOrigin, yOrigin] =
this._dragManager.dragItem.getCoordinates()
.slice(0, 3);
this._dragManager.doMoveWithDragAndDrop(xOrigin, yOrigin, X, Y);
this._receiveLeave();
drop.finish(gdkDropAction);
return true;
}
try {
drop.read_value_async(
readFormat,
GLib.PRIORITY_DEFAULT,
null,
async (dropactor, result) => {
dropData = dropactor.read_value_finish(result);
if (!dropData || !acceptFormat) {
drop.finish(0);
this._receiveLeave();
return false;
}
if (dropData && textDrop) {
gdkReturnAction = Gdk.DragAction.COPY;
this._dragManager.onTextDrop(dropData, [X, Y]);
drop.finish(gdkReturnAction);
this._receiveLeave();
return true;
}
gdkReturnAction =
await this._completeDrop(
X, Y,
x, y,
drop,
dropData,
gdkDropAction,
fileItem,
acceptFormat,
fileItemDropZone,
desktopDropZone,
desktopMove,
filesMove,
textDrop,
event
).catch(e => console.error(e));
if (gdkReturnAction) {
drop.finish(gdkReturnAction);
this._receiveLeave();
return true;
} else {
drop.finish(0);
this._receiveLeave();
return false;
}
}
);
} catch (e) {
console.error(e);
drop.finish(0);
this._receiveLeave();
}
return false;
});
widget.add_controller(this.gridDropController);
this.gridDropControllerMotion = new Gtk.DropControllerMotion();
this.gridDropControllerMotion.connect(
'motion',
(actor, x, y) => {
if (!this.gridDropControllerMotion.is_pointer) {
const fileItem = this._fileAt(x, y);
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const pointerRectangle =
new Gdk.Rectangle({x: X, y: Y, width: 1, height: 1});
if (fileItem && fileItem.dropCapable) {
this._dragManager.unHighLightDropTarget();
if (!this.Prefs.freePositionIcons)
fileItem.highLightDropTarget();
else if (
pointerRectangle
.intersect(fileItem.iconRectangle)[0] ||
pointerRectangle
.intersect(fileItem.labelRectangle)[0])
fileItem.highLightDropTarget();
}
if (fileItem && (fileItem.isDirectory || fileItem.isDrive))
this._startSpringLoadedTimer(fileItem);
} else {
this._dragManager.unHighLightDropTarget();
this._stopSpringLoadedTimer();
}
});
widget.add_controller(this.gridDropControllerMotion);
}
async _completeDrop(
X, Y,
x, y,
drop,
dropData,
gdkDropAction,
fileItem,
acceptFormat,
fileItemDropZone,
desktopDropZone,
desktopMove,
filesMove,
textDrop,
event
) {
let returnAction = Gdk.DragAction.COPY;
const localDrop = !!drop.get_drag();
if (fileItemDropZone && (desktopMove || filesMove)) {
returnAction =
await fileItem.receiveDrop(
X, Y,
x, y,
dropData,
acceptFormat,
gdkDropAction,
localDrop,
event,
this._dragManager.dragItem
).catch(e => console.error(e));
return returnAction;
}
if (desktopDropZone && (desktopMove || filesMove)) {
returnAction = await this._receiveDrop(
x, y,
dropData,
acceptFormat,
gdkDropAction,
localDrop,
event,
this._dragManager.dragItem
).catch(e => console.error(e));
return returnAction;
}
// Finally if all above does not work, catchall-
return false;
}
_setDragSource(widget) {
const widgetDragController = Gtk.DragSource.new();
let clickItem;
widgetDragController.set_actions(
Gdk.DragAction.MOVE | Gdk.DragAction.COPY | Gdk.DragAction.ASK);
widgetDragController.connect(
'prepare',
// eslint-disable-next-line consistent-return
(actor, x, y) => {
const draggedItem = this._fileAt(x, y);
if (draggedItem && !this._dragManager.rubberBand) {
clickItem = draggedItem;
const [a, b] =
this._coordinatesWidgetToWidget(
x, y,
this._container,
clickItem._icon
)
.map(f => Math.floor(Math.max(f)));
this._dragManager.localDragOffset = [a, b];
const dragIcon = this._createStackedDragIcon(clickItem);
widgetDragController.set_icon(dragIcon, a, b);
clickItem.dragSourceOffset = [a, b];
this._loadDragData();
if (this.contentProvider)
return this.contentProvider;
}
}
);
widgetDragController.connect('drag-begin', () => {
this._dragManager.onReleaseButton(this);
this._dragManager.onDragBegin(clickItem);
});
widgetDragController.connect(
'drag-cancel',
async (actor, drag, reason) => {
if (reason === Gdk.DragCancelReason.NO_TARGET ||
reason === Gdk.DragCancelReason.ERROR) {
const gnomedropDetected =
await this._dragManager.gnomeShellDrag
?.completeGnomeShellDrop()
.catch(e => console.error(e));
if (gnomedropDetected)
return true;
else
return false;
} else {
return false;
}
}
);
widgetDragController.connect('drag-end', () => {
this._dragManager.onDragEnd();
this._dragManager.selected(clickItem, this.Enums.Selection.RELEASE);
});
widget.add_controller(widgetDragController);
}
_loadDragData() {
this.contentProvider = null;
const textCoder = new TextEncoder();
const uriList =
this._dragManager.fillDragDataGet(
this.Enums.DndTargetInfo.DING_ICON_LIST);
if (!uriList)
return;
const encodedUriList = textCoder.encode(uriList);
const dingContentProvider =
Gdk.ContentProvider.new_for_bytes(
this.Enums.DndTargetInfo.DING_ICON_LIST,
encodedUriList
);
if (this._desktopManager.checkIfSpecialFilesAreSelected()) {
this.contentProvider = dingContentProvider;
return;
}
const gnomeUriList =
this._dragManager.fillDragDataGet(
this.Enums.DndTargetInfo.GNOME_ICON_LIST);
if (!gnomeUriList)
return;
const gnomeContentProvider =
Gdk.ContentProvider.new_for_bytes(
this.Enums.DndTargetInfo.GNOME_ICON_LIST,
textCoder.encode(gnomeUriList)
);
const textPathList =
this._dragManager.fillDragDataGet(
this.Enums.DndTargetInfo.TEXT_PLAIN
);
if (!textPathList)
return;
const encodedPathList = textCoder.encode(textPathList);
const textUriListContentProvider =
Gdk.ContentProvider.new_for_bytes(
this.Enums.DndTargetInfo.URI_LIST,
encodedUriList
);
const textListContentProvider =
Gdk.ContentProvider.new_for_bytes(
this.Enums.DndTargetInfo.TEXT_PLAIN,
encodedPathList
);
const textUtf8ListContentProvider =
Gdk.ContentProvider.new_for_bytes(
this.Enums.DndTargetInfo.TEXT_PLAIN_UTF8,
encodedPathList
);
this.contentProvider = Gdk.ContentProvider.new_union([
dingContentProvider,
gnomeContentProvider,
textUriListContentProvider,
textListContentProvider,
textUtf8ListContentProvider,
]);
}
// The following code is translated from Nautilus C to Javascript
// to form the similar stack of items
_createStackedDragIcon(draggedItem) {
const selectionArray = this._desktopManager.getCurrentSelection();
selectionArray.sort(
// eslint-disable-next-line no-nested-ternary
(a, b) => a.uri === draggedItem.uri
? -1
: b.uri === draggedItem.uri
? 1
: 0
);
const dragIconArray = selectionArray.map(f => f._icon.get_paintable());
const numberOfIcons = dragIconArray.length;
const dragIcon = Gtk.Snapshot.new();
/* A wide shadow for the pile of icons gives a sense of floating. */
const stackShadow =
{
color: {red: 0, green: 0, blue: 0, alpha: 0.15},
dx: 0,
dy: 2,
radius: 10,
};
/* A slight shadow swhich makes each icon in the stack look separate. */
const iconShadow =
{
color: {red: 0, green: 0, blue: 0, alpha: 0.30},
dx: 0,
dy: 1,
radius: 1,
};
let xOffset = numberOfIcons % 2 === 1 ? 6 : -6;
let yOffset;
switch (numberOfIcons) {
case 1:
yOffset = 0;
break;
case 2:
yOffset = 10;
break;
case 3:
yOffset = 6;
break;
default:
yOffset = 4;
}
dragIcon.translate(
new Graphene.Point(
{
x: 10 + (xOffset / 2),
y: yOffset * numberOfIcons,
}
)
);
const shadow = new Gsk.Shadow(stackShadow);
dragIcon.push_shadow([shadow]);
dragIconArray.reverse().forEach(
paintableWidget => {
const w = paintableWidget.get_intrinsic_width();
const h = paintableWidget.get_intrinsic_height();
const X = Math.floor((this.Prefs.IconSize - w) / 2);
const Y = Math.floor((this.Prefs.IconSize - h) / 2);
dragIcon.translate(
new Graphene.Point(
{
x: -xOffset,
y: -yOffset,
}
)
);
xOffset = -xOffset;
dragIcon.translate(new Graphene.Point({x: X, y: Y}));
dragIcon.push_shadow([new Gsk.Shadow(iconShadow)]);
paintableWidget.snapshot(dragIcon, w, h);
dragIcon.pop();
dragIcon.translate(new Graphene.Point({x: -X, y: -Y}));
}
);
dragIcon.pop();
return dragIcon.to_paintable(null);
}
_receiveLeave() {
this._stopSpringLoadedTimer();
this._window.queue_draw();
this._dragManager.onDragLeave();
}
receiveLeave() {
this._receiveLeave();
}
receiveMotion(x, y, global) {
let X;
let Y;
if (!global) {
x = this._elementWidth * Math.floor(x / this._elementWidth);
y = this._elementHeight * Math.floor(y / this._elementHeight);
[X, Y] = this.coordinatesLocalToGlobal(x, y);
}
this._dragManager.onDragMotion(X, Y);
}
async _receiveDrop(
x, y,
selection,
info,
gdkDropAction,
localDrop,
event,
dragItem
) {
x = this._elementWidth * Math.floor(x / this._elementWidth);
y = this._elementHeight * Math.floor(y / this._elementHeight);
const [X, Y] = this.coordinatesLocalToGlobal(x, y);
const returnAction =
await this._dragManager
.onDragDataReceived(
X, Y,
x, y,
selection,
info,
gdkDropAction,
localDrop,
event,
dragItem
)
.catch(e => console.error(e));
return returnAction;
}
refreshDrag(selectedList, ox, oy) {
if (!this.Prefs.showDropPlace)
return;
if (selectedList === null) {
this._selectedList = null;
this.updateOverlay();
return;
}
let newSelectedList = [];
for (let [x, y] of selectedList) {
x += this._elementWidth / 2;
y += this._elementHeight / 2;
x += ox;
y += oy;
const r = this.getCoordinatesOfGridContaining(x, y);
if (r &&
!isNaN(r[0]) &&
!isNaN(r[1]) &&
(!this._gridInUse(r[0], r[1]) ||
this._fileAt(r[0], r[1])?.isSelected)
)
newSelectedList.push(r);
}
if (newSelectedList.length === 0) {
if (this._selectedList !== null) {
this._selectedList = null;
this.updateOverlay();
}
return;
}
if (this._selectedList !== null) {
if ((newSelectedList[0][0] === this._selectedList[0][0]) &&
(newSelectedList[0][1] === this._selectedList[0][1])
)
return;
}
this._selectedList = newSelectedList;
this.updateOverlay();
}
_startSpringLoadedTimer(fileItem) {
if (!this.Prefs.openFolderOnDndHover || this.directoryOpenTimer)
return;
if (this._dragManager.dragItem?.uri === fileItem.uri)
return;
this.directoryOpenTimer =
GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
this.Enums.DND_HOVER_TIMEOUT,
() => {
const context =
Gdk.Display.get_default()
.get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
try {
Gio.AppInfo.launch_default_for_uri(
fileItem.uri,
context
);
} catch (e) {
console.error(e, `Error opening ${fileItem.uri}` +
` in GNOME Files: ${e.message}`);
}
this.directoryOpenTimer = 0;
return GLib.SOURCE_REMOVE;
}
);
}
_stopSpringLoadedTimer() {
if (this.directoryOpenTimer)
GLib.Source.remove(this.directoryOpenTimer);
this.directoryOpenTimer = 0;
}
};
/* A Picture that can translate itself at paint time (render-only) */
const OffsetPicture = GObject.registerClass({
Properties: {
'tx': GObject.ParamSpec.double('tx', 'tx', 'translate x',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.EXPLICIT_NOTIFY,
-1e6, 1e6, 0.0),
'ty': GObject.ParamSpec.double('ty', 'ty', 'translate y',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.EXPLICIT_NOTIFY,
-1e6, 1e6, 0.0),
'scale': GObject.ParamSpec.double('scale', '', '',
GObject.ParamFlags.READWRITE, 0.5, 2.0, 1.0),
'pivot-x': GObject.ParamSpec.double('pivot-x', '', '',
GObject.ParamFlags.READWRITE, -1e6, 1e6, 0),
'pivot-y': GObject.ParamSpec.double('pivot-y', '', '',
GObject.ParamFlags.READWRITE, -1e6, 1e6, 0),
},
}, class OffsetPicture extends Gtk.Picture {
constructor(props = {}) {
super(
Object.assign({
hexpand: false,
vexpand: false,
halign: Gtk.Align.START,
valign: Gtk.Align.START,
can_target: false,
},
props)
);
this._tx = 0.0;
this._ty = 0.0;
this._scale = 1.0;
this._pivot_x = 0.0;
this._pivot_y = 0.0;
}
get tx() {
return this._tx;
}
set tx(v) {
v = Number(v);
if (v !== this._tx) {
this._tx = v;
this.notify('tx');
this.queue_draw();
}
}
get ty() {
return this._ty;
}
set ty(v) {
v = Number(v);
if (v !== this._ty) {
this._ty = v;
this.notify('ty');
this.queue_draw();
}
}
get scale() {
return this._scale;
}
set scale(v) {
v = Number(v);
if (v !== this._scale) {
this._scale = v;
this.notify('scale');
this.queue_draw();
}
}
get pivot_x() {
return this._pivot_x;
}
set pivot_x(v) {
v = Number(v);
if (v !== this._pivot_x) {
this._pivot_x = v;
this.notify('pivot-x');
this.queue_draw();
}
}
get pivot_y() {
return this._pivot_y;
}
set pivot_y(v) {
v = Number(v);
if (v !== this._pivot_y) {
this._pivot_y = v;
this.notify('pivot-y');
this.queue_draw();
}
}
// eslint-disable-next-line no-unused-vars
vfunc_snapshot(snapshot) {
const a = this.get_allocation();
if (a.width <= 0 || a.height <= 0)
return;
snapshot.save();
try {
const rect = new Graphene.Rect();
rect.init(0, 0, a.width, a.height);
snapshot.push_clip(rect);
try {
snapshot.translate(
new Graphene.Point({x: this._tx, y: this._ty}));
snapshot.translate(
new Graphene.Point({x: this.pivot_x, y: this.pivot_y}));
snapshot.scale(this.scale, this.scale);
snapshot.translate(
new Graphene.Point({x: -this.pivot_x, y: -this.pivot_y}));
super.vfunc_snapshot(snapshot);
} finally {
snapshot.pop();
}
} finally {
snapshot.restore();
}
}
});
// Adds an auxiliary fixed layer that can sit above/below the icon grid.
const WidgetGrid = class extends ControlGrid {
constructor(params) {
super(params);
this._selectedWidget = null; // instanceId
this._draggedWidget = null; // instanceId
this.widgetGridEnabled = false;
this._gridSize = this.Enums.WIDGET_GRID_SIZE;
this._widgetContainer = new Gtk.Fixed();
this._rootFixed.put(this._widgetContainer, 0, 0);
this.resizeGrid();
this._widgetContainer.set_name('widget-container');
this._widgetContainerOnTop = true;
this.lowerWidgetContainer();
this._longPressActive = false;
const drag = new Gtk.GestureDrag();
drag.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
this._widgetContainer.add_controller(drag);
// Click gesture: used only to track selection + click radius
const click = new Gtk.GestureClick({button: 0});
click.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
this._widgetContainer.add_controller(click);
const contextClick = new Gtk.GestureClick({button: 3});
contextClick.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
this._widgetContainer.add_controller(contextClick);
const longPress = new Gtk.GestureLongPress();
longPress.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
this._widgetContainer.add_controller(longPress);
longPress.group(drag);
const settings = Gtk.Settings.get_default();
if (settings) {
const longPressTime = settings.gtk_long_press_time; // ms
const doubleClickTime = settings.gtk_double_click_time; // ms
if (longPressTime && doubleClickTime) {
let factor = doubleClickTime / longPressTime;
longPress.set_delay_factor(factor);
}
}
drag.connect('drag-begin', this._onWidgetDragBegin.bind(this));
drag.connect('drag-update', this._onWidgetDragUpdate.bind(this));
drag.connect('drag-end', this._onWidgetDragEnd.bind(this));
click.connect('pressed', this._onClick.bind(this));
click.connect('released', this._onClickRelease.bind(this));
contextClick.connect('pressed', this._onWidgetContextMenu.bind(this));
longPress.connect('pressed', this._onWidgetLongPress.bind(this));
longPress
.connect('cancelled', this._onWidgetLongPressCancelled.bind(this));
}
get widgetContainer() {
return this._widgetContainer;
}
isWidgetContainerOnTop() {
return this._widgetContainerOnTop;
}
raiseWidgetContainer() {
this._setWidgetContainerLayer(true);
}
lowerWidgetContainer() {
this._setWidgetContainerLayer(false);
}
setWidgetContainerOnTop(onTop = true) {
this._setWidgetContainerLayer(onTop);
}
toggleWidgetLayer() {
this.setWidgetContainerOnTop(!this._widgetContainerOnTop);
}
resizeWindow() {
super.resizeWindow();
this._widgetContainer.set_size_request(
this._width,
this._height
);
this._sizeContainer(this._widgetContainer);
}
resizeGrid() {
super.resizeGrid();
this._widgetContainer.set_size_request(
this._width,
this._height
);
this._sizeContainer(this._widgetContainer);
}
_setWidgetContainerLayer(onTop) {
if (onTop === this._widgetContainerOnTop)
return;
this._widgetContainerOnTop = onTop;
if (onTop) {
// Widgets above icons (edit mode)
// Draw order: icons (bottom), widgets (top)
// Reorder without unparenting:
// place widgetContainer after container in _rootFixed
this._widgetContainer.insert_after(this._rootFixed, this._container);
this._widgetContainer.add_css_class('widgets-on-top');
// Input: widget layer active, icons inert
this._container.set_can_target(false);
this._widgetContainer.set_can_target(true);
this._desktopManager.unselectAll();
this._mainapp.activate_action('textEntryOff', null);
this._mainapp.set_accels_for_action(
'app.lowerWidgetLayer',
['Escape']
);
} else {
// Icons above widgets (normal mode)
// Draw order: widgets (bottom), icons (top)
// Reorder the other way: container after widgetContainer
this._container.insert_after(this._rootFixed, this._widgetContainer);
this._widgetContainer.remove_css_class('widgets-on-top');
// Input: icons active, widget layer background only
this._container.set_can_target(true);
this._widgetContainer.set_can_target(false);
this._desktopManager.widgetManager?.clearSelectedInstance();
this._mainapp.set_accels_for_action('app.lowerWidgetLayer', []);
this._mainapp.activate_action('textEntryOn', null);
}
this._desktopManager.widgetManager
?.handleWidgetContainerLayerChange(this.monitorIndex, this._widgetContainerOnTop);
}
_onWidgetContextMenu(gesture, _nPress, x, y) {
if (!this._widgetContainerOnTop)
return;
if (this._findWidgetAt(x, y))
return;
gesture.set_state(Gtk.EventSequenceState.CLAIMED);
const menu = new Gio.Menu();
menu.append(_('Back to Desktop'), 'app.lowerWidgetLayer');
menu.append(_('Toggle Widget Grid'), 'app.toggleWidgetGrid');
menu.append(_('Add Widget...'), 'app.addWidget');
const popover = Gtk.PopoverMenu.new_from_model(menu);
popover.set_parent(this._widgetContainer);
popover.set_pointing_to(new Gdk.Rectangle({x, y, width: 1, height: 1}));
popover.set_has_arrow(false);
popover.popup();
popover.connect('closed', () => {
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
popover.unparent();
return GLib.SOURCE_REMOVE;
});
});
}
_onKeyPress(actor, keyval, keycode, state) {
if (this._widgetContainerOnTop)
return true;
return super._onKeyPress(actor, keyval, keycode, state);
}
_onWidgetLongPress(gesture, x, y) {
this._longPressActive = true;
this._onWidgetDragBegin(gesture, x, y);
}
_onWidgetLongPressCancelled(_gesture) {
this._longPressActive = false;
}
_onWidgetDragBegin(gesture, startX, startY) {
this._dragStartX = startX;
this._dragStartY = startY;
this._draggedWidget = this._findWidgetAt(startX, startY);
this._dragPointerOffsetX = 0;
this._dragPointerOffsetY = 0;
if (!this._draggedWidget ||
this._isWidgetChromeActor(this._draggedWidget)) {
this._longPressActive = false;
gesture.set_state(Gtk.EventSequenceState.DENIED);
return;
}
// Require a long-press before we actually claim the drag.
// This lets normal short clicks go through to the WebView / Gtk.Button.
if (!this._longPressActive) {
// Dont drag, let the sequence fall through to children.
this._draggedWidget = null;
return;
}
gesture.set_state(Gtk.EventSequenceState.CLAIMED);
const instanceId = this._draggedWidget.widgetInstanceId;
const frame =
this._desktopManager.widgetManager.getInstanceFrame(instanceId);
if (frame) {
this._dragPointerOffsetX = startX - frame.x;
this._dragPointerOffsetY = startY - frame.y;
}
if (this._selectedWidget === instanceId)
this._desktopManager.widgetManager.hideSelectionChromeDuringDrag();
this._setWidgetDraggingState(true);
}
_findWidgetAt(lx, ly) {
const picked =
this._widgetContainer.pick(lx, ly, Gtk.PickFlags.DEFAULT);
return this._widgetFromPickedActor(picked);
}
_widgetFromPickedActor(picked) {
if (!picked)
return null;
// We only want to return a direct child in widgetContainer,
let w = picked;
while (w && w !== this._widgetContainer) {
if (w.get_parent() === this._widgetContainer)
return w;
w = w.get_parent();
}
return null;
}
_onWidgetDragUpdate(gesture, offsetX, offsetY) {
if (!this._draggedWidget)
return;
const lx = this._dragStartX + offsetX;
const ly = this._dragStartY + offsetY;
const instanceId = this._draggedWidget.widgetInstanceId;
const [offX, offY] = this._getWidgetOffsets(instanceId);
let newLocalX = lx - offX;
let newLocalY = ly - offY;
if (this.widgetGridEnabled)
[newLocalX, newLocalY] =
this._getWidgetSnappedPosition(newLocalX, newLocalY);
this._widgetContainer.move(this._draggedWidget, newLocalX, newLocalY);
}
_getWidgetSnappedPosition(lx, ly) {
let newLocalX = Math.round(lx / this._gridSize) * this._gridSize;
let newLocalY = Math.round(ly / this._gridSize) * this._gridSize;
newLocalX = Math.max(0, Math.min(newLocalX, this._width - this._gridSize));
newLocalY = Math.max(0, Math.min(newLocalY, this._height - this._gridSize));
return [newLocalX, newLocalY];
}
_onWidgetDragEnd(gesture, offsetX, offsetY) {
if (!this._draggedWidget)
return;
const lx = this._dragStartX + offsetX;
const ly = this._dragStartY + offsetY;
const instanceId = this._draggedWidget.widgetInstanceId;
const [offX, offY] = this._getWidgetOffsets(instanceId);
let newLocalX = lx - offX;
let newLocalY = ly - offY;
if (this.widgetGridEnabled)
[newLocalX, newLocalY] =
this._getWidgetSnappedPosition(newLocalX, newLocalY);
this._desktopManager.widgetManager.setInstanceFrame(
instanceId,
newLocalX,
newLocalY
);
if (this._selectedWidget === instanceId) {
this._desktopManager.widgetManager
.updateSelectionChromePositionFor(instanceId);
}
this._setWidgetDraggingState(false);
this._draggedWidget = null;
this._dragPointerOffsetX = null;
this._dragPointerOffsetY = null;
this._longPressActive = false;
}
_setWidgetDraggingState(isDragging) {
if (!this._draggedWidget)
return;
const ctx = this._draggedWidget.get_style_context();
if (isDragging)
ctx.add_class('dragging');
else
ctx.remove_class('dragging');
}
_getWidgetOffsets(instanceId) {
const inst = this._desktopManager.widgetManager.getInstance(instanceId);
const fallbackOffsetX = inst ? inst.width / 2 : 0;
const fallbackOffsetY = inst ? inst.height / 2 : 0;
const offsetX =
typeof this._dragPointerOffsetX === 'number'
? this._dragPointerOffsetX
: fallbackOffsetX;
const offsetY =
typeof this._dragPointerOffsetY === 'number'
? this._dragPointerOffsetY
: fallbackOffsetY;
return [offsetX, offsetY];
}
_onClick(gesture, nPress, x, y) {
const widget = this._findWidgetAt(x, y);
if (!widget) {
this._selectedWidget = null;
this._desktopManager.widgetManager.selectInstance(null);
return;
}
if (this._isWidgetChromeActor(widget)) {
this._selectedWidget = null;
return;
}
const instanceId = widget.widgetInstanceId;
if (!instanceId) {
this._selectedWidget = null;
return;
}
this._selectedWidget = instanceId;
this._desktopManager.widgetManager.selectInstance(instanceId);
this.click = [x, y];
}
_onClickRelease(gesture, _nPress, x, y) {
if (!this._selectedWidget)
return;
const [clickX, clickY] = this.click ?? [x, y];
const dx = x - clickX;
const dy = y - clickY;
const dist = dx * dx + dy * dy;
const radius = 4 * 4;
const isClick = dist <= radius;
this.click = null;
if (!isClick)
return;
// At this point weve done all our selection work in _onClick or
// _onWidgetLongPress. For a real click, we now DENY the sequence
// so that the underlying actor (HTML WebView or Gtk.Button add
// widget) sees a normal click.
gesture.set_state(Gtk.EventSequenceState.DENIED);
}
_isWidgetChromeActor(actor) {
return actor.get_name?.() === 'ding-widget-close-button';
}
_doDrawOnGrid(snapshot) {
super._doDrawOnGrid(snapshot);
this._doDrawGridRectangles(snapshot);
}
_doDrawGridRectangles(snapshot) {
const enabled = this.widgetGridEnabled;
if (enabled) {
const width = this._drawArea.get_allocated_width();
const height = this._drawArea.get_allocated_height();
const gridColor = new Gdk.RGBA({red: 0.3, green: 0.3, blue: 0.3, alpha: 0.18});
for (let x = 0; x < width; x += this._gridSize) {
const rect = new Graphene.Rect();
rect.init(x + 0.5, 0, 1, height);
snapshot.append_color(gridColor, rect);
}
for (let y = 0; y < height; y += this._gridSize) {
const rect = new Graphene.Rect();
rect.init(0, y + 0.5, width, 1);
snapshot.append_color(gridColor, rect);
}
}
}
};
const DesktopGrid = class extends WidgetGrid {
constructor(params) {
super(params);
this._snapshotPic = new OffsetPicture();
this._oldMargins = null;
this._animationInProgress = false;
this._freezeDesktop = false;
this._pendingMargins = null;
this._newMargins = null;
this._tweenDelta = null;
this._reverse = 0.33; // single tuning knob for spring snappiness
// in ms
this._duration = Math.max(350, this.Enums.TRANSITIONDURATION ?? 0);
this._setupAnimations();
}
destroy() {
if (this._relayoutCoalesceSource) {
GLib.source_remove(this._relayoutCoalesceSource);
this._relayoutCoalesceSource = 0;
}
super.destroy();
}
_setupAnimations() {
this._setupSpringAnimation();
this._setupOffsetAnimation();
}
_captureSnapshotPaintable(widget) {
return new Promise(resolve => {
const width = widget.get_width();
const height = widget.get_height();
const size = new Graphene.Size({width, height});
try {
const snap = Gtk.Snapshot.new();
widget.vfunc_snapshot(snap);
resolve(snap.to_paintable(size));
} catch (e) {
logError(e);
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
try {
const snap = Gtk.Snapshot.new();
widget.vfunc_snapshot(snap);
resolve(snap.to_paintable(size));
} catch (ee) {
logError(ee);
const gdkpic =
Gtk.WidgetPaintable.new(widget).get_current_image();
resolve(gdkpic);
}
return GLib.SOURCE_REMOVE;
});
}
});
}
async displaySnapshot() {
if (this._freezeDesktop)
return;
this._freezeDesktop = true;
const snapshot = await this._captureSnapshotPaintable(this._window);
this._resetAll();
this._snapshotPic.set_paintable(snapshot);
this._oldMargins = this._getCurrentMargins();
this._overlay.add_overlay(this._snapshotPic);
this._snapshotPic.opacity = 1;
this._overlay.queue_draw();
this._rootFixed.opacity = 0;
this._rootFixed.queue_draw();
}
_getCurrentMargins() {
const margin = {
left: this._marginLeft ?? 0,
top: this._marginTop ?? 0,
right: this._marginRight ?? 0,
bottom: this._marginBottom ?? 0,
};
const contentRectangle = this._computeContentRectangle(margin);
margin.contentRectangle = contentRectangle;
return margin;
}
_computeContentRectangle(margins) {
const contentRectangle = new Gdk.Rectangle({
x: margins.left,
y: margins.top,
width: this._windowWidth - margins.left - margins.right,
height: this._windowHeight - margins.top - margins.bottom,
});
return contentRectangle;
}
_setLiveOffset(dx, dy) {
this._snapshotPic.tx = Math.round(dx);
this._snapshotPic.ty = Math.round(dy);
}
_setLiveTransform(scale, pivotx, pivoty) {
this._snapshotPic.scale = Number(scale);
this._snapshotPic.pivot_x = Math.round(pivotx);
this._snapshotPic.pivot_y = Math.round(pivoty);
}
_resetLiveTransform() {
this._setLiveTransform(1.0, 0, 0);
}
_resetLiveOffset() {
this._setLiveOffset(0, 0);
}
_resetAll() {
this._resetLiveOffset();
this._resetLiveTransform();
}
_clearOverlay(widget) {
if (widget?.get_parent() === this._overlay)
this._overlay.remove_overlay(widget);
}
_displayLive() {
this._rootFixed.opacity = 1.0;
this._snapshotPic.opacity = 0;
this._rootFixed.queue_draw();
this._resetAll();
this._clearOverlay(this._snapshotPic);
this._animationInProgress = false;
this._freezeDesktop = false;
}
_computeTweenDelta(Old, New) {
const sameShape =
Old.contentRectangle.width === New.contentRectangle.width &&
Old.contentRectangle.height === New.contentRectangle.height;
if (sameShape) {
// If the content rectangles are the same shape, we can just tween
// the top left corner of the content rectangle as the anchor
// for pixel perfect alignment of the content rectangle.
const anchor = 'topleft';
const dx = Old.left - New.left;
const dy = Old.top - New.top;
const pivotx = Old.contentRectangle.x;
const pivoty = Old.contentRectangle.y;
return {sameShape, anchor, dx, dy, pivotx, pivoty};
}
// If the content rectangles are not the same shape, or if the
// or both axis changed size, then we cannot just tween the
// top left corner of the content rectangle as the anchor.
// Instead, we need to tween the center, to account for the
// difference in aspect ratio.
const ocx = Old.contentRectangle.x + Old.contentRectangle.width / 2;
const ocy = Old.contentRectangle.y + Old.contentRectangle.height / 2;
const ncx = New.contentRectangle.x + New.contentRectangle.width / 2;
const ncy = New.contentRectangle.y + New.contentRectangle.height / 2;
const anchor = 'center';
const dx = ocx - ncx;
const dy = ocy - ncy;
const pivotx = ncx;
const pivoty = ncy;
return {sameShape, anchor, dx, dy, pivotx, pivoty};
}
requestAnimatedRelayout() {
if (this._relayoutCoalesceSource) {
GLib.source_remove(this._relayoutCoalesceSource);
this._relayoutCoalesceSource = 0;
}
// coalesce multiple relayouts within this time
const relayoutBurstMs = 100;
this._pendingMargins = this._getCurrentMargins();
this._relayoutCoalesceSource = GLib.timeout_add(
GLib.PRIORITY_DEFAULT, relayoutBurstMs, () => {
this._playRelayoutTransition(this._pendingMargins);
this._relayoutCoalesceSource = 0;
this._pendingMargins = null;
return GLib.SOURCE_REMOVE;
}
);
}
_setupSpringAnimation() {
const dampingRatio = 0.58; // < 1 => underdamped (dip then settle)
const stiffness = 250 + Math.round((1 - this._reverse) * 350);
const mass = 1.0;
const springParams =
Adw.SpringParams.new(dampingRatio, mass, stiffness);
const springTarget = Adw.CallbackAnimationTarget.new(v => {
const s = Number(v); // animates around 1.0 due to initial_velocity
this._setLiveTransform(
s, this._tweenDelta.pivotx, this._tweenDelta.pivoty
);
});
this._springAnimation = new Adw.SpringAnimation({
widget: this._overlay,
value_from: 1.0,
value_to: 1.0,
spring_params: springParams,
initial_velocity: -3.0, // negative => dip “away”, then return
epsilon: 0.001,
clamp: false,
target: springTarget,
});
}
_setupOffsetAnimation() {
const target = Adw.CallbackAnimationTarget.new(value => {
const t = Number(value); // 0.0 to 1.0
const x = Math.round(-this._tweenDelta.dx * t);
const y = Math.round(-this._tweenDelta.dy * t);
this._setLiveOffset(x, y);
this._snapshotPic.opacity = 1 - t;
// Fade in the NEW live layers only near the end
if (t > 0.8)
this._rootFixed.opacity = t;
});
this._offsetAnim = new Adw.TimedAnimation({
widget: this._overlay,
value_from: 0.0,
value_to: 1.0,
duration: this._duration,
easing: Adw.Easing.EASE_OUT_CUBIC,
target,
});
this._offsetAnim.connect('done', () => {
this._setLiveOffset(-this._tweenDelta.dx, -this._tweenDelta.dy);
// Ensure we end exactly at identity scale
if (this._moveAway) {
this._setLiveTransform(
1.0, this._tweenDelta.pivotx, this._tweenDelta.pivoty
);
}
this._displayLive();
});
}
_playRelayoutTransition(pendingMargins = null) {
if (!this.animationsEnabled || !this._freezeDesktop) {
this._displayLive();
return;
}
if (this._animationInProgress) {
this._offsetAnim.pause();
this._springAnimation.pause();
}
this._animationInProgress = true;
this._newMargins = pendingMargins ?? this._getCurrentMargins();
this._tweenDelta =
this._computeTweenDelta(this._oldMargins, this._newMargins);
const noshift = this._tweenDelta.dx === 0 && this._tweenDelta.dy === 0;
this._moveAway = !this._tweenDelta.sameShape;
if (noshift && !this._moveAway) {
// No visible change, so just end the animation
this._displayLive();
return;
}
// Initialize transform for the OLD snapshot we are animating
// - translation starts at the old position
// - scale is 1.0 (no depth change yet)
// - pivot is from tweenDelta (center for shape change, topleft otherwise)
this._setLiveOffset(0, 0);
this._setLiveTransform(1.0,
this._tweenDelta.pivotx,
this._tweenDelta.pivoty
);
this._offsetAnim.play();
if (this._moveAway)
this._springAnimation.play();
}
get animationsEnabled() {
return this.Prefs.globalAnimations;
}
};