Files
DankMaterialShell/quickshell/Modules/ControlCenter/Components/CcTileGrid.qml
T
2026-10-27 16:48:01 +00:00

142 lines
6.1 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.ControlCenter
import "../utils/sections.js" as Sections
import "../utils/widgets.js" as WidgetUtils
import "../../../Common/GridLayout.js" as GridUtils
DEditableGrid {
id: root
property var model: null
property bool live: true
property string screenName: ""
property int columns: CcMetrics.gridColumns
property real availableHeight: CcMetrics.fallbackScreenHeight - CcMetrics.maxHeightInset
readonly property int maximumRows: CcMetrics.rowCapFor(availableHeight, cellWidth - CcMetrics.gridGap)
// The detail band opens under this tile's row and pushes the rows below it down. While a band is shown the
// slots follow its animated height frame by frame instead of running their own motion, so one animation drives
// both; the section is the band's own, which it keeps until it has fully collapsed.
property string expandedSection: ""
property real bandHeight: 0
property real bandTargetHeight: 0
property bool bandMoving: false
readonly property int expandedIndex: indexForSection(expandedSection)
readonly property real bandY: slotLayout.bandY ?? 0
readonly property real baseHeight: baseLayout.totalHeight
readonly property real targetHeight: GridUtils.insertBand(baseLayout, expandedIndex, expandedSection !== "" ? bandTargetHeight : 0).totalHeight
signal expandClicked(var widgetData)
signal removeWidget(int index)
signal configRequested(int index, var widgetData, var anchor)
signal colorPickerRequested
signal lockRequested
signal powerRequested
signal settingsRequested
signal editRequested
signal accountsRequested
signal closeRequested
property bool tapToClose: false
property var runningToplevels: []
// A member drag inside a quick-tiles group never reaches the grid's own interaction state.
property bool memberDragging: false
property var swallowPoint: null
readonly property real gridHeight: layoutHeight
readonly property real cellWidth: (width + CcMetrics.gridGap) / columns
readonly property bool dragging: draggingSourceIndex >= 0 || externalItem !== null
readonly property CcTileSlot draggingSlot: tileRepeater.itemAt(draggingSourceIndex) as CcTileSlot
readonly property string hoverGroup: heldOutside && draggingSourceIndex >= 0 ? (groupAt(dragScenePoint, draggingSourceIndex)?.groupId ?? "") : ""
readonly property var savedWidgets: SettingsData.controlCenterWidgets || []
readonly property var groupIds: WidgetUtils.groupIds(savedWidgets)
readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => !WidgetUtils.inGroup(widget, groupIds) && WidgetUtils.isShown(widget) && model?.componentForWidget(widget) ? indices.concat([i]) : indices, [])
sourceItems: shownIndices.map(i => Object.assign({}, savedWidgets[i], sizeWithHiddenTwin(i)))
readonly property var baseLayout: {
const items = clamped(layoutItems);
const layout = GridUtils.packCards(items, placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true);
return GridUtils.shrinkRows(layout, items.map(item => WidgetUtils.isSmall(item, item.h)), CcMetrics.smallRowHeight);
}
// Whole pixels keep the rows crisp while they ride the band's animation.
slotLayout: GridUtils.insertBand(baseLayout, expandedIndex, Math.round(bandHeight))
holdLayout: bandMoving
gravity: true
rowStep: CcMetrics.gridStep
packer: items => GridUtils.packCells(clamped(items), placementOrder, columns, null, CcMetrics.gridStep, true).cells
placeholderRadius: draggingSlot?.small ? Theme.fullRadius(draggingSlot.width, draggingSlot.height) : (draggingSlot?.tileItem?.bodyRadius ?? Theme.fullRadius(width, CcMetrics.tileHeight))
commitHandler: items => model.setLayout(withHidden(items))
onEditModeChanged: {
if (!editMode)
memberDragging = false;
}
// A grouped member expands under its group's tile.
function indexForSection(section) {
if (section === "")
return -1;
const widget = Sections.widgetForSection(section);
const target = widget?.group ? savedWidgets.find(other => other?.id === WidgetUtils.GROUP_ID && other.instanceId === widget.group) : widget;
if (!target)
return -1;
return sourceItems.findIndex(item => item.id === target.id && (item.instanceId || "") === (target.instanceId || ""));
}
function clamped(items) {
return items.map(widget => Object.assign({}, widget, WidgetUtils.clampSize(widget, columns, maximumRows)));
}
function sizeWithHiddenTwin(index) {
const widget = savedWidgets[index];
const size = WidgetUtils.clampSize(widget, Infinity);
if (!WidgetUtils.isUnplaced(widget))
return size;
for (const neighbor of [index + 1, index - 1]) {
const twin = savedWidgets[neighbor];
if (!twin || WidgetUtils.isShown(twin) || !WidgetUtils.isUnplaced(twin))
continue;
const twinSize = WidgetUtils.clampSize(twin, Infinity);
if (twinSize.w !== size.w || twinSize.h !== size.h)
continue;
size.w += twinSize.w;
return size;
}
return size;
}
function groupAt(scenePoint, excludeIndex) {
for (let i = 0; i < tileRepeater.count; i++) {
const slot = tileRepeater.itemAt(i) as CcTileSlot;
if (i === excludeIndex || slot?.widgetData.id !== WidgetUtils.GROUP_ID)
continue;
if (slot.contains(slot.mapFromItem(null, scenePoint.x, scenePoint.y)))
return slot.tileItem;
}
return null;
}
function savedIndex(index) {
return shownIndices[index] ?? -1;
}
function withHidden(items) {
const widgets = savedWidgets.slice();
shownIndices.forEach((saved, i) => widgets[saved] = items[i]);
return widgets;
}
Repeater {
id: tileRepeater
model: root.tileModel
CcTileSlot {
grid: root
}
}
}