mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
286 lines
11 KiB
QML
286 lines
11 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Common
|
|
import qs.Modules.ControlCenter
|
|
import qs.DCommon.Widgets
|
|
import "../utils/widgets.js" as WidgetUtils
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var widgetData: ({})
|
|
property var widgetDef: null
|
|
property var host: null
|
|
property bool live: true
|
|
property bool interactive: true
|
|
property real columns: 4
|
|
property real rows: 1
|
|
property bool compact: false
|
|
|
|
readonly property string groupId: widgetData?.instanceId ?? ""
|
|
// Not `bare`: the grid's tile loader writes that name into any tile that has it.
|
|
readonly property bool membersBare: widgetData?.background === false
|
|
readonly property real gap: membersBare ? 0 : CcMetrics.quickTileGap
|
|
readonly property bool editMode: host?.editMode ?? false
|
|
readonly property var members: WidgetUtils.groupMembers(SettingsData.controlCenterWidgets || [], groupId).filter(member => !!host?.model?.componentForWidget(member.widget))
|
|
readonly property bool receiving: groupId !== "" && host?.hoverGroup === groupId
|
|
// Members keep their own input in edit mode so they can be dragged out instead of moving the group.
|
|
readonly property Item passthrough: memberRow
|
|
readonly property real bodyRadius: Theme.fullRadius(width, Math.min(height, CcMetrics.tileHeight))
|
|
readonly property bool memberDragging: liftedIndex >= 0
|
|
|
|
property int liftedIndex: -1
|
|
property real liftedWidth: 0
|
|
property point liftPoint: Qt.point(0, 0)
|
|
property int dropSlot: -1
|
|
readonly property int incomingSlot: receiving ? slotAt(mapFromItem(null, host.dragScenePoint.x, host.dragScenePoint.y).x) : -1
|
|
readonly property int gapSlot: liftedIndex >= 0 ? dropSlot : incomingSlot
|
|
readonly property real gapWidth: liftedIndex >= 0 ? liftedWidth : (membersBare ? CcMetrics.quickBareWidth : CcMetrics.iconBoxSize)
|
|
|
|
width: parent?.width ?? 0
|
|
height: parent?.height ?? 0
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
function others() {
|
|
const items = [];
|
|
for (let i = 0; i < memberRepeater.count; i++) {
|
|
const item = memberRepeater.itemAt(i);
|
|
if (item && item.modelData.index !== liftedIndex)
|
|
items.push(item);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// Measured against the row as it sits without a gap, so opening the gap cannot move the target.
|
|
function slotAt(x) {
|
|
const items = others();
|
|
const total = items.reduce((sum, item) => sum + item.ownWidth + gap, 0) - gap;
|
|
const along = I18n.isRtl ? width - x : x;
|
|
let start = (width - total) / 2;
|
|
for (let k = 0; k < items.length; k++) {
|
|
if (along < start + items[k].ownWidth / 2)
|
|
return k;
|
|
start += items[k].ownWidth + gap;
|
|
}
|
|
return items.length;
|
|
}
|
|
|
|
function savedIndexAtSlot(slot) {
|
|
return others()[slot]?.modelData.index ?? -1;
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (liftedIndex >= 0)
|
|
host.memberDragging = false;
|
|
}
|
|
|
|
function lift(member, scenePoint) {
|
|
liftedWidth = member.ownWidth;
|
|
liftedIndex = member.modelData.index;
|
|
host.memberDragging = true;
|
|
moveLifted(member, scenePoint);
|
|
}
|
|
|
|
function moveLifted(member, scenePoint) {
|
|
const point = mapFromItem(null, scenePoint.x, scenePoint.y);
|
|
liftPoint = Qt.point(point.x - member.grabOffset.x, point.y - member.grabOffset.y);
|
|
if (contains(point)) {
|
|
dropSlot = slotAt(point.x);
|
|
host.clearExternal();
|
|
return;
|
|
}
|
|
dropSlot = -1;
|
|
const size = WidgetUtils.clampSize(member.modelData.widget, host.columns, host.maximumRows);
|
|
const gridPoint = host.mapFromItem(null, scenePoint.x, scenePoint.y);
|
|
host.previewExternal({
|
|
"id": member.modelData.widget.id,
|
|
"w": size.w,
|
|
"h": size.h
|
|
}, gridPoint.x - host.cellWidth * size.w / 2, gridPoint.y - host.slotLayout.rowUnit * size.h / 2);
|
|
}
|
|
|
|
function drop(member) {
|
|
const savedIndex = member.modelData.index;
|
|
const slot = dropSlot;
|
|
const before = savedIndexAtSlot(slot);
|
|
const position = members.findIndex(other => other.index === savedIndex);
|
|
const unchanged = before === (members[position + 1]?.index ?? -1);
|
|
liftedIndex = -1;
|
|
dropSlot = -1;
|
|
host.memberDragging = false;
|
|
if (slot >= 0) {
|
|
host.clearExternal();
|
|
if (!unchanged)
|
|
Qt.callLater(() => WidgetUtils.moveToGroup(savedIndex, groupId, before));
|
|
return;
|
|
}
|
|
if (!host.externalItem)
|
|
return;
|
|
const placed = host.committedItems();
|
|
const cell = placed.pop();
|
|
const widgets = WidgetUtils.placeFromGroup(host.withHidden(placed), savedIndex, cell.col, cell.row);
|
|
Qt.callLater(() => {
|
|
WidgetUtils.setLayout(widgets);
|
|
host.cancelInteraction();
|
|
});
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: root.bodyRadius
|
|
color: root.receiving ? Theme.withAlpha(Theme.primary, Theme.stateLayerDrag) : "transparent"
|
|
visible: root.editMode
|
|
}
|
|
|
|
StyledText {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingS
|
|
text: I18n.tr("Drag small tiles in to group them", "control center quick tiles widget description")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
wrapMode: Text.WordWrap
|
|
elide: Text.ElideRight
|
|
visible: root.editMode && root.members.length === 0 && root.gapSlot < 0
|
|
}
|
|
|
|
Row {
|
|
id: memberRow
|
|
|
|
// Leaves the bottom corners to the edit chrome's resize grip.
|
|
readonly property real cornerSize: Theme.minimumTouchTargetSize / 2
|
|
|
|
// Every member carries its trailing gap, which sits on the far side in RTL.
|
|
x: (root.width - Math.max(0, implicitWidth - root.gap)) / 2 - (I18n.isRtl ? root.gap : 0)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: CcMetrics.iconBoxSize
|
|
containmentMask: QtObject {
|
|
function contains(point: point): bool {
|
|
if (point.x < 0 || point.y < 0 || point.x >= memberRow.width || point.y >= memberRow.height)
|
|
return false;
|
|
const local = memberRow.mapToItem(root, point.x, point.y);
|
|
return local.y < root.height - memberRow.cornerSize || (local.x >= memberRow.cornerSize && local.x < root.width - memberRow.cornerSize);
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
id: memberRepeater
|
|
|
|
model: root.members
|
|
|
|
Item {
|
|
id: member
|
|
|
|
required property var modelData
|
|
required property int index
|
|
readonly property bool lifted: root.liftedIndex === modelData.index
|
|
readonly property int position: index - (root.liftedIndex >= 0 && index > root.members.findIndex(other => other.index === root.liftedIndex) ? 1 : 0)
|
|
readonly property bool gapBefore: !lifted && root.gapSlot === position
|
|
readonly property real ownWidth: loader.item?.quickWidth ?? CcMetrics.iconBoxSize
|
|
property point grabOffset: Qt.point(0, 0)
|
|
|
|
width: (gapBefore ? root.gapWidth + root.gap : 0) + (lifted ? 0 : ownWidth + root.gap)
|
|
height: memberRow.height
|
|
z: lifted ? 1 : 0
|
|
|
|
Behavior on width {
|
|
enabled: CcMetrics.animationsEnabled && (root.memberDragging || root.receiving)
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveFastSpatial
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
x: I18n.isRtl ? member.ownWidth + root.gap * 2 : 0
|
|
width: root.gapWidth
|
|
height: parent.height
|
|
radius: Theme.fullRadius(width, height)
|
|
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
visible: member.gapBefore
|
|
}
|
|
|
|
Item {
|
|
id: body
|
|
|
|
x: {
|
|
if (member.lifted)
|
|
return root.liftPoint.x - memberRow.x - member.x;
|
|
if (I18n.isRtl)
|
|
return root.gap;
|
|
return member.gapBefore ? root.gapWidth + root.gap : 0;
|
|
}
|
|
y: member.lifted ? root.liftPoint.y - memberRow.y : 0
|
|
width: member.ownWidth
|
|
height: memberRow.height
|
|
|
|
Behavior on x {
|
|
enabled: CcMetrics.animationsEnabled && !member.lifted
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveFastSpatial
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
|
}
|
|
}
|
|
|
|
CcTileLoader {
|
|
id: loader
|
|
|
|
anchors.fill: parent
|
|
grid: root.host
|
|
widgetData: member.modelData.widget
|
|
savedIndex: member.modelData.index
|
|
compact: true
|
|
small: true
|
|
bare: root.membersBare
|
|
}
|
|
|
|
DragHandler {
|
|
id: drag
|
|
|
|
target: null
|
|
enabled: root.editMode
|
|
cursorShape: Qt.ClosedHandCursor
|
|
onActiveChanged: {
|
|
if (active) {
|
|
member.grabOffset = centroid.pressPosition;
|
|
root.lift(member, centroid.scenePosition);
|
|
return;
|
|
}
|
|
if (member.lifted)
|
|
root.drop(member);
|
|
}
|
|
onCentroidChanged: {
|
|
if (active)
|
|
root.moveLifted(member, centroid.scenePosition);
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
enabled: root.editMode
|
|
cursorShape: member.lifted ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: root.gapSlot >= 0 && root.gapSlot === root.members.length - (root.liftedIndex >= 0 ? 1 : 0) ? root.gapWidth + root.gap : 0
|
|
height: memberRow.height
|
|
|
|
Rectangle {
|
|
x: I18n.isRtl ? root.gap : 0
|
|
width: root.gapWidth
|
|
height: parent.height
|
|
radius: Theme.fullRadius(width, height)
|
|
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
|
visible: parent.width > 0
|
|
}
|
|
}
|
|
}
|
|
}
|