3142 lines
97 KiB
JavaScript
3142 lines
97 KiB
JavaScript
/* 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) {
|
||
// Don’t 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 we’ve 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;
|
||
}
|
||
};
|