610 lines
18 KiB
JavaScript
610 lines
18 KiB
JavaScript
import Clutter from 'gi://Clutter';
|
|
import GLib from 'gi://GLib';
|
|
import GObject from 'gi://GObject';
|
|
import St from 'gi://St';
|
|
|
|
import {ArcMenuManager} from './arcmenuManager.js';
|
|
import * as MW from './menuWidgets.js';
|
|
|
|
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
|
|
import * as Params from 'resource:///org/gnome/shell/misc/params.js';
|
|
|
|
export const DragLocation = {
|
|
INVALID: 0,
|
|
START_EDGE: 1,
|
|
ON_ICON: 2,
|
|
END_EDGE: 3,
|
|
EMPTY_SPACE: 4,
|
|
TOP_EDGE: 5,
|
|
BOTTOM_EDGE: 6,
|
|
};
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
function swap(value, length) {
|
|
return length - value - 1;
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
function animateIconPosition(icon, box, nChangedIcons) {
|
|
if (!icon.has_allocation() || icon.allocation.equal(box) || icon.opacity === 0) {
|
|
icon.allocate(box);
|
|
return false;
|
|
}
|
|
|
|
icon.save_easing_state();
|
|
icon.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
|
|
icon.set_easing_delay(nChangedIcons * 10);
|
|
|
|
icon.allocate(box);
|
|
|
|
icon.restore_easing_state();
|
|
|
|
return true;
|
|
}
|
|
|
|
export const IconGridLayout = GObject.registerClass({
|
|
Properties: {
|
|
'column-spacing': GObject.ParamSpec.int('column-spacing',
|
|
'Column spacing', 'Column spacing',
|
|
GObject.ParamFlags.READWRITE,
|
|
0, GLib.MAXINT32, 0),
|
|
'row-spacing': GObject.ParamSpec.int('row-spacing',
|
|
'Row spacing', 'Row spacing',
|
|
GObject.ParamFlags.READWRITE,
|
|
0, GLib.MAXINT32, 0),
|
|
'columns': GObject.ParamSpec.int('columns',
|
|
'Columns', 'Columns',
|
|
GObject.ParamFlags.READWRITE,
|
|
1, GLib.MAXINT32, 1),
|
|
'force-columns': GObject.ParamSpec.int('force-columns',
|
|
'Force Columns', 'Force Columns',
|
|
GObject.ParamFlags.READWRITE,
|
|
0, GLib.MAXINT32, 0),
|
|
'halign': GObject.ParamSpec.enum('halign',
|
|
'Horizontal page align',
|
|
'Horizontal page align',
|
|
GObject.ParamFlags.READWRITE,
|
|
Clutter.ActorAlign.$gtype,
|
|
Clutter.ActorAlign.FILL),
|
|
'first-row-align': GObject.ParamSpec.enum('first-row-align',
|
|
'First row align', 'First row align',
|
|
GObject.ParamFlags.READWRITE,
|
|
Clutter.ActorAlign.$gtype,
|
|
Clutter.ActorAlign.CENTER),
|
|
|
|
},
|
|
}, class IconGridLayout extends Clutter.LayoutManager {
|
|
_init(params = {}) {
|
|
super._init(params);
|
|
|
|
this._sizeChanged = false;
|
|
this._width = 0;
|
|
this._height = 0;
|
|
this._items = new Map();
|
|
this._children = [];
|
|
this._childrenMaxSize = null;
|
|
}
|
|
|
|
_getChildrenMaxSize() {
|
|
if (!this._childrenMaxSize) {
|
|
let minWidth = 0;
|
|
let minHeight = 0;
|
|
|
|
this._children.forEach(item => {
|
|
const childMinHeight = item.get_preferred_height(-1)[1];
|
|
const childMinWidth = item.get_preferred_width(-1)[1];
|
|
|
|
minWidth = Math.max(minWidth, childMinWidth);
|
|
minHeight = Math.max(minHeight, childMinHeight);
|
|
});
|
|
|
|
this._childrenMaxSize = [minWidth, minHeight];
|
|
}
|
|
return this._childrenMaxSize;
|
|
}
|
|
|
|
_calculateSpacing(width, childWidth) {
|
|
if (this.halign !== Clutter.ActorAlign.CENTER)
|
|
return 0;
|
|
|
|
let columns = 0;
|
|
if (this.firstRowAlign === Clutter.ActorAlign.CENTER) {
|
|
const visibleChildren = this._children.filter(child => child.visible);
|
|
// if the amount of visiblechildren is less than the amount of columns
|
|
// set columns to visiblechildren.length in order to center the items
|
|
columns = visibleChildren.length < this.columns ? visibleChildren.length : this.columns;
|
|
} else {
|
|
columns = this.columns;
|
|
}
|
|
|
|
const nColumns = columns;
|
|
const usedWidth = childWidth * nColumns;
|
|
const columnSpacing = this.columnSpacing * (nColumns - 1);
|
|
|
|
const emptyHSpace = width - usedWidth - columnSpacing;
|
|
const leftEmptySpace = Math.floor(emptyHSpace / 2);
|
|
|
|
return leftEmptySpace;
|
|
}
|
|
|
|
_unlinkItem(item) {
|
|
const itemData = this._items.get(item);
|
|
|
|
item.disconnect(itemData.destroyId);
|
|
item.disconnect(itemData.visibleId);
|
|
item.disconnect(itemData.queueRelayoutId);
|
|
|
|
this._items.delete(item);
|
|
}
|
|
|
|
_removeItemData(item) {
|
|
this._unlinkItem(item);
|
|
|
|
const itemIndex = this._children.indexOf(item);
|
|
this._children.splice(itemIndex, 1);
|
|
}
|
|
|
|
_addItem(item, index) {
|
|
if (index === -1)
|
|
index = this._children.length;
|
|
|
|
this._items.set(item, {
|
|
actor: item,
|
|
destroyId: item.connect('destroy', () => this._removeItemData(item)),
|
|
visibleId: item.connect('notify::visible', () => {
|
|
this._shouldEaseItems = true;
|
|
}),
|
|
queueRelayoutId: item.connect('queue-relayout', () => {
|
|
this._childrenMaxSize = null;
|
|
}),
|
|
});
|
|
|
|
this._children.splice(index, 0, item);
|
|
}
|
|
|
|
vfunc_set_container(container) {
|
|
this._container = container;
|
|
}
|
|
|
|
vfunc_get_preferred_width() {
|
|
return [5, this._width];
|
|
}
|
|
|
|
vfunc_get_preferred_height() {
|
|
const children = this._children;
|
|
const totalColumns = this.columns;
|
|
|
|
let minRowHeight = 0;
|
|
let natRowHeight = 0;
|
|
let natHeight = 0;
|
|
let firstRow = true;
|
|
let column = 0;
|
|
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
const child = children[i];
|
|
if (!child.visible)
|
|
continue;
|
|
|
|
const isSeparator = child instanceof MW.ArcMenuSeparator;
|
|
const [childMinHeight, childNatHeight] = child.get_preferred_height(-1);
|
|
|
|
minRowHeight = Math.max(minRowHeight, childMinHeight);
|
|
natRowHeight = Math.max(natRowHeight, childNatHeight);
|
|
|
|
const newRow = column % totalColumns === 0;
|
|
if (firstRow && newRow) {
|
|
firstRow = false;
|
|
natHeight = natRowHeight; // + this.rowSpacing + PADDING;
|
|
natRowHeight = 0;
|
|
} else if (isSeparator) {
|
|
natHeight += childNatHeight + this.rowSpacing;
|
|
natRowHeight = 0;
|
|
} else if (newRow) {
|
|
natHeight += natRowHeight + this.rowSpacing;
|
|
natRowHeight = 0;
|
|
}
|
|
|
|
column++;
|
|
if (column === this.columns || isSeparator)
|
|
column = 0;
|
|
}
|
|
|
|
this._height = natHeight;
|
|
return [natHeight, minRowHeight];
|
|
}
|
|
|
|
vfunc_allocate() {
|
|
const children = this._children;
|
|
const shouldEaseItems = this._shouldEaseItems;
|
|
const sizeChanged = this._sizeChanged;
|
|
const isRtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
|
|
|
|
const [childWidth, childHeight_] = this._getChildrenMaxSize();
|
|
const xOffset = this._calculateSpacing(this._width, childWidth);
|
|
|
|
const childBox = new Clutter.ActorBox();
|
|
|
|
let nChangedIcons = 0;
|
|
let rowHeight = 0;
|
|
let y = 0;
|
|
let column = 0;
|
|
let naturalWidth = 0;
|
|
let naturalHeight = 0;
|
|
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
const child = children[i];
|
|
|
|
const isSeparator = child instanceof MW.ArcMenuSeparator;
|
|
const xFill = child.x_align === Clutter.ActorAlign.FILL;
|
|
|
|
if (isRtl)
|
|
column = swap(column, this.columns);
|
|
|
|
const newRow = column % this.columns === 0;
|
|
|
|
[,, naturalWidth, naturalHeight] = child.get_preferred_size();
|
|
|
|
if (isSeparator)
|
|
naturalWidth = this._width;
|
|
else if (xFill)
|
|
naturalWidth = this._width / this.columns;
|
|
|
|
let x;
|
|
if (isSeparator)
|
|
x = 0;
|
|
else
|
|
x = xOffset + column * (naturalWidth + this.columnSpacing);
|
|
|
|
// The first item in a row will determine the row height
|
|
// add previous child naturalHeight offset
|
|
if (isSeparator || newRow)
|
|
y += rowHeight;
|
|
|
|
rowHeight = naturalHeight + this.rowSpacing;
|
|
|
|
childBox.set_origin(Math.floor(x), Math.floor(y));
|
|
childBox.set_size(naturalWidth, naturalHeight);
|
|
|
|
if (!shouldEaseItems || sizeChanged)
|
|
child.allocate(childBox);
|
|
else if (animateIconPosition(child, childBox, nChangedIcons))
|
|
nChangedIcons++;
|
|
|
|
column++;
|
|
|
|
if (column === this.columns || isSeparator)
|
|
column = 0;
|
|
}
|
|
|
|
this._sizeChanged = false;
|
|
this._shouldEaseItems = false;
|
|
}
|
|
|
|
addItem(item, index = -1) {
|
|
if (this._items.has(item))
|
|
throw new Error(`Item ${item} already added to IconGridLayout`);
|
|
|
|
if (!this._container)
|
|
return;
|
|
|
|
this._shouldEaseItems = true;
|
|
|
|
this._container.add_child(item);
|
|
this._addItem(item, index);
|
|
}
|
|
|
|
appendChild(item) {
|
|
this.addItem(item);
|
|
}
|
|
|
|
moveItem(item, newPosition) {
|
|
if (!this._items.has(item))
|
|
throw new Error(`Item ${item} is not part of the IconGridLayout`);
|
|
|
|
this._shouldEaseItems = true;
|
|
|
|
this._removeItemData(item);
|
|
|
|
this._addItem(item, newPosition);
|
|
}
|
|
|
|
removeItem(item) {
|
|
if (!this._items.has(item))
|
|
throw new Error(`Item ${item} is not part of the IconGridLayout`);
|
|
|
|
if (!this._container)
|
|
return;
|
|
|
|
this._shouldEaseItems = true;
|
|
|
|
this._container.remove_child(item);
|
|
this._removeItemData(item);
|
|
}
|
|
|
|
removeAllItems() {
|
|
for (let i = this._children.length - 1; i >= 0; --i) {
|
|
const item = this._children[i];
|
|
this.removeItem(item);
|
|
}
|
|
}
|
|
|
|
getItemPosition(item) {
|
|
if (!this._items.has(item))
|
|
return -1;
|
|
|
|
return this._children.indexOf(item);
|
|
}
|
|
|
|
getItemAt(position) {
|
|
if (position < 0 || position >= this._children.length)
|
|
return null;
|
|
|
|
return this._children[position];
|
|
}
|
|
|
|
getDropTarget(x, y) {
|
|
const emptySpace = 5;
|
|
|
|
const isRtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
|
|
|
|
const gridWidth = this._width;
|
|
const gridHeight = this._height;
|
|
|
|
const inTopEmptySpace = y < -emptySpace;
|
|
const inLeftEmptySpace = x < -emptySpace;
|
|
const inRightEmptySpace = x > emptySpace + gridWidth;
|
|
const inBottomEmptySpace = y > emptySpace + gridHeight;
|
|
|
|
if (inTopEmptySpace || inBottomEmptySpace || inRightEmptySpace || inLeftEmptySpace)
|
|
return [0, DragLocation.INVALID];
|
|
|
|
const halfHSpacing = this.rowSpacing / 2;
|
|
const halfVSpacing = this.columnSpacing / 2;
|
|
const nRows = Math.ceil(this.nChildren / this.columns);
|
|
const singleColumnGrid = this.columns === 1;
|
|
|
|
for (let i = 0; i < this._children.length; i++) {
|
|
const item = this._children[i];
|
|
|
|
// skip hidden item. the item current being dragged.
|
|
if (item.opacity === 0 || !item.visible)
|
|
continue;
|
|
|
|
const childBox = item.allocation;
|
|
|
|
const firstInRow = i % this.columns === 0;
|
|
const lastInRow = i % this.columns === this.columns - 1;
|
|
const firstInColumn = Math.floor(i / this.columns) === 0;
|
|
const lastInColumn = Math.floor(i / this.columns) === nRows - 1;
|
|
|
|
// Check icon boundaries
|
|
if (!singleColumnGrid && ((inLeftEmptySpace && firstInRow) ||
|
|
(inRightEmptySpace && lastInRow))) {
|
|
if (y < childBox.y1 - halfVSpacing ||
|
|
y > childBox.y2 + halfVSpacing)
|
|
continue;
|
|
} else if (singleColumnGrid && ((inTopEmptySpace && firstInColumn) ||
|
|
(inBottomEmptySpace && lastInColumn))) {
|
|
if (x < childBox.x1 - halfHSpacing ||
|
|
x > childBox.x2 + halfHSpacing)
|
|
continue;
|
|
} else {
|
|
// eslint-disable-next-line no-lonely-if
|
|
if (x < childBox.x1 - halfHSpacing ||
|
|
x > childBox.x2 + halfHSpacing ||
|
|
y < childBox.y1 - halfVSpacing ||
|
|
y > childBox.y2 + halfVSpacing)
|
|
continue;
|
|
}
|
|
|
|
const leftDividerLeeway = Math.round(item.get_preferred_width(-1)[1] / 5);
|
|
const rightDividerLeeway = Math.round(item.get_preferred_width(-1)[1] / 5);
|
|
const topDividerLeeway = Math.round(item.get_preferred_height(-1)[1] / 5);
|
|
const bottomDividerLeeway = Math.round(item.get_preferred_height(-1)[1] / 5);
|
|
|
|
let dragLocation;
|
|
if (!singleColumnGrid && x < childBox.x1 + leftDividerLeeway)
|
|
dragLocation = DragLocation.START_EDGE;
|
|
else if (!singleColumnGrid && x > childBox.x2 - rightDividerLeeway)
|
|
dragLocation = DragLocation.END_EDGE;
|
|
else if (singleColumnGrid && y < childBox.y1 + topDividerLeeway)
|
|
dragLocation = DragLocation.TOP_EDGE;
|
|
else if (singleColumnGrid && y > childBox.y2 - bottomDividerLeeway)
|
|
dragLocation = DragLocation.BOTTOM_EDGE;
|
|
else
|
|
dragLocation = DragLocation.ON_ICON;
|
|
|
|
if (isRtl) {
|
|
if (dragLocation === DragLocation.START_EDGE)
|
|
dragLocation = DragLocation.END_EDGE;
|
|
else if (dragLocation === DragLocation.END_EDGE)
|
|
dragLocation = DragLocation.START_EDGE;
|
|
}
|
|
|
|
return [i, dragLocation];
|
|
}
|
|
|
|
return [-1, DragLocation.EMPTY_SPACE];
|
|
}
|
|
|
|
adaptToSize(width) {
|
|
if (this._width === width)
|
|
return;
|
|
|
|
this._width = width;
|
|
this._sizeChanged = true;
|
|
}
|
|
|
|
getChildren() {
|
|
return this._children;
|
|
}
|
|
|
|
get nChildren() {
|
|
return this._children.length;
|
|
}
|
|
});
|
|
|
|
export const IconGrid = GObject.registerClass(
|
|
class IconGrid extends St.Widget {
|
|
_init(layoutParams = {}) {
|
|
const acceptDrop = layoutParams.accept_drop;
|
|
delete layoutParams.accept_drop;
|
|
|
|
layoutParams = Params.parse(layoutParams, {
|
|
columns: 1,
|
|
column_spacing: 0,
|
|
row_spacing: 0,
|
|
force_columns: 0,
|
|
halign: Clutter.ActorAlign.FILL,
|
|
first_row_align: Clutter.ActorAlign.CENTER,
|
|
});
|
|
|
|
const layoutManager = new IconGridLayout(layoutParams);
|
|
|
|
super._init({
|
|
layoutManager,
|
|
x_expand: true,
|
|
x_align: Clutter.ActorAlign.FILL,
|
|
});
|
|
|
|
// only need acceptDrop for the main pinned-apps grid
|
|
if (acceptDrop) {
|
|
// DND requires this to be set
|
|
this._delegate = this;
|
|
}
|
|
}
|
|
|
|
_canAccept(source) {
|
|
if (!(source instanceof MW.DraggableMenuItem))
|
|
return false;
|
|
|
|
if (this.contains(source))
|
|
return false;
|
|
|
|
if (!source.folderId)
|
|
return false;
|
|
|
|
const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack();
|
|
for (let i = 0; i < pinnedAppsList.length; i++) {
|
|
if (pinnedAppsList[i].id === source.pinnedAppData.id)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
handleDragOver(source, _actor, _x, _y) {
|
|
if (!this._canAccept(source))
|
|
return DND.DragMotionResult.CONTINUE;
|
|
|
|
return DND.DragMotionResult.MOVE_DROP;
|
|
}
|
|
|
|
acceptDrop(source, _actor, _x, _y) {
|
|
if (!this._canAccept(source))
|
|
return false;
|
|
|
|
const sourceData = source.pinnedAppData;
|
|
|
|
source.cancelActions();
|
|
|
|
// remove app from folder pinned app list
|
|
const parent = source.get_parent();
|
|
const layoutManager = parent.layout_manager;
|
|
const index = layoutManager.getItemPosition(source);
|
|
|
|
const folderSettings = source.folderSettings;
|
|
|
|
const sourceParentChildren = layoutManager.getChildren();
|
|
const folderPinnedApps = [];
|
|
for (let i = 0; i < sourceParentChildren.length; i++)
|
|
folderPinnedApps.push(sourceParentChildren[i].pinnedAppData);
|
|
|
|
folderPinnedApps.splice(index, 1);
|
|
folderSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', folderPinnedApps));
|
|
|
|
// add app to main pinned apps
|
|
const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack();
|
|
pinnedAppsList.push(sourceData);
|
|
ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {Clutter.ActorAlign} alignment
|
|
*/
|
|
set halign(alignment) {
|
|
this.layout_manager.halign = alignment;
|
|
this.queue_relayout();
|
|
}
|
|
|
|
/**
|
|
* @param {Clutter.ActorAlign} columnSpacing
|
|
*/
|
|
set column_spacing(columnSpacing) {
|
|
this.layout_manager.column_spacing = columnSpacing;
|
|
this.queue_relayout();
|
|
}
|
|
|
|
/**
|
|
* @param {number} rowSpacing
|
|
*/
|
|
set row_spacing(rowSpacing) {
|
|
this.layout_manager.row_spacing = rowSpacing;
|
|
this.queue_relayout();
|
|
}
|
|
|
|
vfunc_allocate(box) {
|
|
const [width, height_] = box.get_size();
|
|
this.layout_manager.adaptToSize(width);
|
|
super.vfunc_allocate(box);
|
|
}
|
|
|
|
addItem(item, index = -1) {
|
|
this.layout_manager.addItem(item, index);
|
|
this.queue_relayout();
|
|
}
|
|
|
|
appendItem(item) {
|
|
this.layout_manager.addItem(item, -1);
|
|
this.queue_relayout();
|
|
}
|
|
|
|
moveItem(item, newPosition) {
|
|
this.layout_manager.moveItem(item, newPosition);
|
|
this.queue_relayout();
|
|
}
|
|
|
|
removeItem(item) {
|
|
if (!this.contains(item))
|
|
return;
|
|
|
|
this.layout_manager.removeItem(item);
|
|
}
|
|
|
|
removeAllItems() {
|
|
this.layout_manager.removeAllItems();
|
|
}
|
|
|
|
setColumns(columns) {
|
|
this.layout_manager.columns = columns;
|
|
this.queue_relayout();
|
|
}
|
|
|
|
getItemPosition(item) {
|
|
if (!this.contains(item))
|
|
return -1;
|
|
|
|
return this.layout_manager.getItemPosition(item);
|
|
}
|
|
|
|
getItemAt(position) {
|
|
return this.layout_manager.getItemAt(position);
|
|
}
|
|
|
|
getDropTarget(x, y) {
|
|
return this.layout_manager.getDropTarget(x, y);
|
|
}
|
|
});
|