Files

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
}
}
}
}