mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 16:24:00 -04:00
258 lines
9.3 KiB
QML
258 lines
9.3 KiB
QML
import QtQuick
|
|
import qs.Common
|
|
import "../Common/GridLayout.js" as GridUtils
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property bool editMode: false
|
|
property var sourceItems: []
|
|
property var slotLayout: ({
|
|
"slots": [],
|
|
"totalHeight": 0
|
|
})
|
|
property real minimumHeight: 0
|
|
property real contentPadding: 0
|
|
property real placeholderRadius: Theme.cornerRadiusXL
|
|
property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
|
property bool animateLayout: false
|
|
property int draggingSourceIndex: -1
|
|
property var dragCell: null
|
|
property var sizePreview: null
|
|
property var pinnedCells: null
|
|
property real heldHeight: 0
|
|
property real pinnedHeight: 0
|
|
property point dragScenePoint: Qt.point(0, 0)
|
|
// Takes (sourceIndex, scenePoint, rect) with the dragged item's unsnapped rect; true while it hovers a target
|
|
// outside the grid, which holds the grid still.
|
|
property var dragsOutside: null
|
|
property bool heldOutside: false
|
|
// Takes (sourceIndex, scenePoint) and returns true when something outside the grid claims the dropped item.
|
|
// The grid then stays mid-drag until that commit replaces the item.
|
|
property var dropHandler: null
|
|
// An item hovering from outside, placed first so the grid makes room for it.
|
|
property var externalItem: null
|
|
// A callback, not a signal: Qt 6.12 hands signal array parameters over as a V4Sequence that fails Array.isArray.
|
|
property var commitHandler: null
|
|
// The layout compacts upward, so a drop has to account for the hole the dragged item leaves.
|
|
property bool gravity: false
|
|
// Takes the items with the dragged one at its target and returns their packed cells, so a drop can tell
|
|
// whether gravity would lift the dragged item back above where it was dropped.
|
|
property var packer: null
|
|
property real rowStep: 1
|
|
readonly property alias tileModel: tiles
|
|
readonly property bool interacting: draggingSourceIndex >= 0 || sizePreview !== null || externalItem !== null
|
|
readonly property real layoutHeight: interacting ? Math.max(heldHeight, slotLayout.totalHeight) : slotLayout.totalHeight
|
|
readonly property int interactingIndex: draggingSourceIndex >= 0 ? draggingSourceIndex : (sizePreview?.index ?? -1)
|
|
readonly property var placementOrder: {
|
|
const order = sourceItems.map((item, i) => i);
|
|
if (externalItem)
|
|
return [sourceItems.length].concat(order);
|
|
if (interactingIndex < 0)
|
|
return order;
|
|
order.splice(interactingIndex, 1);
|
|
order.unshift(interactingIndex);
|
|
return order;
|
|
}
|
|
readonly property var pinnedItems: pinnedCells ? GridUtils.placedItems(sourceItems, pinnedCells) : sourceItems
|
|
readonly property var layoutItems: {
|
|
const items = externalItem ? pinnedItems.concat([externalItem]) : pinnedItems;
|
|
const changes = sizePreview?.changes ?? dragCell;
|
|
if (interactingIndex < 0 || !changes)
|
|
return items;
|
|
const changed = items.map((item, i) => i === interactingIndex ? Object.assign({}, item, changes) : item);
|
|
return gravity && dragCell && draggingSourceIndex >= 0 ? GridUtils.dropInto(changed, pinnedCells, draggingSourceIndex, dragCell, packer, rowStep) : changed;
|
|
}
|
|
|
|
implicitHeight: Math.max(minimumHeight, layoutHeight) + contentPadding * 2
|
|
height: implicitHeight
|
|
|
|
function pin() {
|
|
pinnedCells = slotLayout.slots.slice();
|
|
heldHeight = slotLayout.totalHeight;
|
|
pinnedHeight = heldHeight;
|
|
}
|
|
|
|
function cancelInteraction() {
|
|
draggingSourceIndex = -1;
|
|
dragCell = null;
|
|
sizePreview = null;
|
|
externalItem = null;
|
|
heldOutside = false;
|
|
pinnedCells = null;
|
|
heldHeight = 0;
|
|
}
|
|
|
|
function committedItems() {
|
|
return GridUtils.placedItems(layoutItems, slotLayout.slots);
|
|
}
|
|
|
|
function syncTiles() {
|
|
animateLayout = false;
|
|
cancelInteraction();
|
|
const seen = {};
|
|
const keys = sourceItems.map(item => {
|
|
const base = JSON.stringify([item.id || "", item.instanceId || ""]);
|
|
const count = seen[base] || 0;
|
|
seen[base] = count + 1;
|
|
return base + "#" + count;
|
|
});
|
|
for (let i = tiles.count - 1; i >= 0; i--) {
|
|
if (keys.indexOf(tiles.get(i).key) < 0)
|
|
tiles.remove(i);
|
|
}
|
|
for (let p = 0; p < keys.length; p++) {
|
|
const json = JSON.stringify(sourceItems[p]);
|
|
let current = -1;
|
|
for (let j = p; j < tiles.count; j++) {
|
|
if (tiles.get(j).key !== keys[p])
|
|
continue;
|
|
current = j;
|
|
break;
|
|
}
|
|
if (current < 0) {
|
|
tiles.insert(p, {
|
|
"key": keys[p],
|
|
"json": json
|
|
});
|
|
continue;
|
|
}
|
|
if (current !== p)
|
|
tiles.move(current, p, 1);
|
|
if (tiles.get(p).json !== json)
|
|
tiles.setProperty(p, "json", json);
|
|
}
|
|
}
|
|
|
|
function beginDrag(sourceIndex) {
|
|
if (!editMode || interacting || !sourceItems[sourceIndex])
|
|
return;
|
|
pin();
|
|
draggingSourceIndex = sourceIndex;
|
|
}
|
|
|
|
function updateDragTarget(x, y) {
|
|
const slot = draggingSourceIndex >= 0 ? slotLayout.slots[draggingSourceIndex] : null;
|
|
if (!slot)
|
|
return;
|
|
heldOutside = dragsOutside?.(draggingSourceIndex, dragScenePoint, Qt.rect(x - contentPadding, y - contentPadding, slot.w, slot.h)) ?? false;
|
|
if (heldOutside) {
|
|
dragCell = null;
|
|
return;
|
|
}
|
|
const cell = GridUtils.cellAt(slotLayout, x - contentPadding, y - contentPadding, slot.cols, slot.rows);
|
|
if (dragCell && dragCell.col === cell.col && dragCell.row === cell.row)
|
|
return;
|
|
dragCell = cell;
|
|
}
|
|
|
|
function endDrag() {
|
|
if (draggingSourceIndex < 0)
|
|
return;
|
|
if (dropHandler?.(draggingSourceIndex, dragScenePoint))
|
|
return;
|
|
const items = committedItems();
|
|
const moved = pinnedCells.some((cell, i) => cell && (cell.col !== items[i].col || cell.row !== items[i].row));
|
|
cancelInteraction();
|
|
if (moved)
|
|
commitHandler?.(items);
|
|
}
|
|
|
|
function previewExternal(item, x, y) {
|
|
if (!editMode || draggingSourceIndex >= 0 || sizePreview)
|
|
return;
|
|
if (!pinnedCells)
|
|
pin();
|
|
const cell = GridUtils.cellAt(slotLayout, x - contentPadding, y - contentPadding, item.w, item.h);
|
|
if (externalItem?.col === cell.col && externalItem?.row === cell.row)
|
|
return;
|
|
externalItem = Object.assign({}, item, cell);
|
|
}
|
|
|
|
function clearExternal() {
|
|
if (externalItem)
|
|
cancelInteraction();
|
|
}
|
|
|
|
function previewSize(index, changes) {
|
|
if (!editMode || draggingSourceIndex >= 0 || !sourceItems[index])
|
|
return;
|
|
if (sizePreview?.index === index && JSON.stringify(sizePreview.changes) === JSON.stringify(changes))
|
|
return;
|
|
if (!pinnedCells)
|
|
pin();
|
|
sizePreview = {
|
|
"index": index,
|
|
"changes": changes
|
|
};
|
|
}
|
|
|
|
function commitSize() {
|
|
const preview = sizePreview;
|
|
if (!preview)
|
|
return;
|
|
const item = sourceItems[preview.index];
|
|
const changed = item && Object.keys(preview.changes).some(key => item[key] !== preview.changes[key]);
|
|
const items = committedItems();
|
|
if (changed)
|
|
commitHandler?.(items);
|
|
cancelInteraction();
|
|
}
|
|
|
|
function commitChange(index, changes) {
|
|
previewSize(index, changes);
|
|
commitSize();
|
|
}
|
|
|
|
onSourceItemsChanged: syncTiles()
|
|
onSlotLayoutChanged: {
|
|
if (interacting)
|
|
heldHeight = Math.max(heldHeight, slotLayout.totalHeight);
|
|
}
|
|
Component.onCompleted: syncTiles()
|
|
onEditModeChanged: {
|
|
if (!editMode)
|
|
cancelInteraction();
|
|
}
|
|
|
|
Timer {
|
|
interval: 0
|
|
running: root.width > 0 && !root.animateLayout
|
|
onTriggered: root.animateLayout = true
|
|
}
|
|
|
|
ListModel {
|
|
id: tiles
|
|
}
|
|
|
|
Rectangle {
|
|
readonly property var slot: root.slotLayout.slots[root.draggingSourceIndex >= 0 ? root.draggingSourceIndex : root.externalItem ? root.sourceItems.length : -1] ?? null
|
|
|
|
visible: slot !== null && !root.heldOutside
|
|
x: slot ? slot.x + root.contentPadding : 0
|
|
y: slot ? slot.y + root.contentPadding : 0
|
|
width: slot ? slot.w : 0
|
|
height: slot ? slot.h : 0
|
|
radius: root.placeholderRadius
|
|
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
|
|
Behavior on x {
|
|
enabled: root.animationsEnabled
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveFastSpatial
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
|
}
|
|
}
|
|
|
|
Behavior on y {
|
|
enabled: root.animationsEnabled
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveFastSpatial
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
|
}
|
|
}
|
|
}
|
|
}
|