pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Internal import ZShell.Services import qs.Components import qs.Services Item { id: root required property int headerHeight required property ProcessesList pList readonly property alias columnList: columnList readonly property alias columnsModel: columnsData readonly property bool anyHeld: columnList.contentItem.children.some(c => c.held) z: 1 implicitHeight: headerHeight function widthForColumn(id: string, configured: real): real { for (const item of columnList.contentItem.children) { if (item.modelData !== undefined && item.modelData.id === id) return item.liveWidth > 0 ? item.liveWidth : (configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100)); } return configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100); } function fillMissingWidths() { const n = columnsData.values.length; if (n === 0 || columnList.width <= 0) return; const spacingTotal = columnList.spacing * Math.max(0, n - 1); let savedTotal = 0; const unsavedIndices = []; for (let i = 0; i < n; i++) { const w = columnsData.values[i].width; if (w > 0) savedTotal += w; else unsavedIndices.push(i); } if (unsavedIndices.length === 0) return; const available = columnList.width - spacingTotal - savedTotal; if (available <= 0) return; const each = Math.max(50, available / unsavedIndices.length); for (const idx of unsavedIndices) ProcessesColumns.resizeColumn(idx, each); } Connections { target: columnList function onWidthChanged() { root.fillMissingWidths(); } function onCountChanged() { resizeOverlay.geometryTick++; root.fillMissingWidths(); } function onContentWidthChanged() { resizeOverlay.geometryTick++; } } Connections { target: columnsData function onValuesChanged() { root.fillMissingWidths(); resizeOverlay.geometryTick++; } } Component.onCompleted: Qt.callLater(fillMissingWidths) Component.onDestruction: Cursor.clearOverride() CustomListView { id: columnList anchors.fill: parent orientation: ListView.Horizontal interactive: false spacing: Tokens.spacing.extraSmall / 2 add: Transition { Anim { from: 0 properties: "opacity" to: 1 type: Anim.DefaultEffects } } displaced: Transition { Anim { properties: "opacity" to: 1 type: Anim.DefaultEffects } Anim { properties: "x" } } move: Transition { Anim { properties: "opacity" to: 1 type: Anim.DefaultEffects } Anim { properties: "x" } } remove: Transition { Anim { to: 0 properties: "opacity" type: Anim.DefaultEffects } } model: DelegateModel { id: columnsVisual delegate: ColumnHeaderCell { parentModel: columnsVisual } model: ScriptModel { id: columnsData values: Config.dashboard.processColumns.values } } } Item { id: resizeOverlay property real gapBoost: pressed ? Tokens.spacing.small : Tokens.spacing.extraSmall property bool pressed: false property int geometryTick: 0 property int activeGapIndex: -1 anchors.fill: columnList z: 10 Component.onCompleted: Qt.callLater(() => geometryTick++) Repeater { model: Math.max(0, columnsData.values.length - 1) MouseArea { id: handleRoot required property int index readonly property bool hovered: containsMouse property ColumnHeaderCell leftCell: { const left = leftCell; if (resizing && left) return left; const tick = resizeOverlay.geometryTick; return resolveCell(true); } property ColumnHeaderCell rightCell: { const right = rightCell; if (resizing && right) return right; const tick = resizeOverlay.geometryTick; return resolveCell(false); } property bool resizing: false property real originX: 0 property real originWidth: 0 property real originRightWidth: 0 property real originAbsX: 0 function resolveCell(left: bool): ColumnHeaderCell { if (left) { // qmllint disable incompatible-type const leftItem = columnList.itemAtIndex(index); if (leftItem) return leftItem; else return null; } else { const rightItem = columnList.itemAtIndex(index + 1); if (rightItem) return rightItem; else return null; } } cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor preventStealing: true hoverEnabled: true enabled: !root.anyHeld x: { if (!leftCell) return 0; const gapCenter = leftCell.x + leftCell.width + columnList.spacing / 2; return gapCenter - width / 2; } y: 0 implicitWidth: 16 implicitHeight: columnList.height function pairAvailableWidth(rightCell) { let othersWidth = 0; for (const item of columnList.contentItem.children) { if (item.modelData === undefined || item === handleRoot.leftCell || item === rightCell) continue; othersWidth += item.width; } const spacingTotal = columnList.spacing * Math.max(0, columnsData.values.length - 1); return columnList.width - othersWidth - spacingTotal; } CustomRect { id: handle readonly property bool shouldBeVisible: handleRoot.hovered || handleRoot.pressed anchors.top: parent.top anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter implicitWidth: shouldBeVisible ? resizeOverlay.gapBoost : 0 color: Colors.palette.m3secondary radius: Tokens.rounding.full opacity: shouldBeVisible ? 1.0 : 0 Behavior on implicitWidth { Anim { type: Anim.FastSpatial } } Behavior on opacity { Anim {} } transform: Scale { origin { x: handle.implicitWidth / 2 y: handle.height / 2 } yScale: handle.shouldBeVisible ? 1 : 0.5 xScale: handle.shouldBeVisible ? 1 : 0.5 Behavior on xScale { Anim {} } Behavior on yScale { Anim {} } } } onHoveredChanged: resizeOverlay.activeGapIndex = hovered || pressed ? handleRoot.index : (resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex) onPressedChanged: { resizeOverlay.pressed = pressed; if (!pressed) resizeOverlay.activeGapIndex = hovered ? handleRoot.index : resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex; } onPressed: e => { if (!handleRoot.leftCell) return; Cursor.setOverride(Qt.ClosedHandCursor); resizing = true; originAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x; originWidth = handleRoot.leftCell.implicitWidth; originRightWidth = handleRoot.rightCell ? handleRoot.rightCell.implicitWidth : 0; } onPositionChanged: e => { if (!resizing || !handleRoot.leftCell) return; const nowAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x; let newLeftWidth = Math.max(50, originWidth + (nowAbsX - originAbsX)); const rightCell = handleRoot.rightCell; let roundedLeft = Math.round(newLeftWidth); if (rightCell) { const available = Math.round(handleRoot.pairAvailableWidth(rightCell)); const maxLeftWidth = Math.max(50, available - 50); roundedLeft = Math.min(roundedLeft, maxLeftWidth); rightCell.liveWidth = Math.max(50, available - roundedLeft); } handleRoot.leftCell.liveWidth = roundedLeft; } onReleased: { Cursor.clearOverride(); resizing = false; const cell = handleRoot.leftCell; const rightCell = handleRoot.rightCell; if (cell.liveWidth !== 0) ProcessesColumns.resizeColumn(cell.DelegateModel.itemsIndex, cell.liveWidth); if (rightCell.liveWidth !== 0) ProcessesColumns.resizeColumn(rightCell.DelegateModel.itemsIndex, rightCell.liveWidth); if (cell || rightCell) { cell.liveWidth = 0; rightCell.liveWidth = 0; } resizeOverlay.geometryTick++; } } } } component ColumnHeaderCell: Item { id: cell required property var modelData required property int index required property DelegateModel parentModel property int leftRadius: pushSide !== 1 && DelegateModel.itemsIndex === 0 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall property int rightRadius: pushSide !== -1 && DelegateModel.itemsIndex === parentModel.count - 1 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall property real radiusLerpProg property bool held property bool moved: false property int pressIndex: -1 property point pressPos property real lastMoveX: 0 property real liveWidth: 0 readonly property bool isActiveSort: Processes.sortField === (ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0) readonly property int pushSide: { const gapIdx = resizeOverlay.activeGapIndex; if (gapIdx < 0) return 0; const myIdx = cell.DelegateModel.itemsIndex; if (myIdx === gapIdx) return -1; if (myIdx === gapIdx + 1) return 1; return 0; } property real gapBoost: resizeOverlay.gapBoost readonly property real gapShrink: gapBoost / 2 property real gapProg: pushSide !== 0 ? 1 : 0 property real gapX: pushSide === 1 ? 1 : 0 function lerpRadius(a: real, b: real): real { return a + (b - a) * radiusLerpProg; } Behavior on gapBoost { Anim { type: Anim.FastSpatial } } Behavior on gapX { Anim { type: Anim.FastSpatial } } Behavior on gapProg { Anim { type: Anim.FastSpatial } } implicitHeight: root.headerHeight implicitWidth: liveWidth > 0 ? liveWidth : (modelData.width > 0 ? modelData.width : (ProcessesColumns.defFor(modelData.id)?.width ?? 100)) z: held || returnAnim.running ? 1 : 0 states: State { name: "held" PropertyChanges { cellBg.color: Colors.palette.m3primaryContainer elevation.opacity: 1 placeholder.opacity: 0.1 cell.radiusLerpProg: 1 title.color: Colors.palette.m3onPrimaryContainer removeBtn.inactiveOnColor: Colors.palette.m3onPrimaryContainer } } Behavior on leftRadius { Anim { type: Anim.DefaultEffects } } Behavior on rightRadius { Anim { type: Anim.DefaultEffects } } transitions: Transition { Anim { properties: "opacity,radiusLerpProg" type: Anim.SlowEffects } PropertyAction { properties: "color,inactiveOnColor" } } state: held ? "held" : "" CustomRect { id: placeholder anchors.fill: parent color: Colors.palette.m3primary opacity: 0 topLeftRadius: cell.leftRadius bottomLeftRadius: cell.leftRadius topRightRadius: cell.rightRadius bottomRightRadius: cell.rightRadius } DragHandler { id: dragHandler property real centerOffset: 0 enabled: cell.modelData.id !== ProcessesColumns.pinnedColumnId target: null xAxis.enabled: true dragThreshold: 10 grabPermissions: PointerHandler.CanTakeOverFromItems onActiveChanged: { if (active) { Cursor.setOverride(Qt.ClosedHandCursor); returnAnim.stop(); cell.pressIndex = cell.DelegateModel.itemsIndex; cell.lastMoveX = cell.x; cell.moved = true; cell.held = true; const p = centroid.position; cell.pressPos = Qt.point(p.x, p.y); dragHandler.centerOffset = p.x - cell.width / 2; cellContent.x = Qt.binding(() => { const maxOvershoot = Tokens.padding.extraLarge; const xDiff = cell.x - cell.lastMoveX; const x = dragHandler.centroid.position.x - cell.pressPos.x - xDiff; const absX = cell.mapToItem(columnList.contentItem, x, 0).x; const maxX = columnList.contentWidth - cell.width; if (absX < 0) return x - absX - ProcessesColumns.dampOvershoot(-absX, maxOvershoot); if (absX > maxX) return x - absX + maxX + ProcessesColumns.dampOvershoot(absX - maxX, maxOvershoot); return x; }); cellContent.y = Qt.binding(() => { const maxOvershoot = Tokens.padding.extraLargeIncreased; const y = dragHandler.centroid.position.y - cell.pressPos.y; return ProcessesColumns.dampOvershoot(Math.abs(y), maxOvershoot) * Math.sign(y); }); } else { Cursor.clearOverride(); if (cell.held) { cellContent.x = cellContent.x; cellContent.y = cellContent.y; cell.held = false; returnAnim.start(); if (cell.DelegateModel.itemsIndex !== cell.pressIndex) { ProcessesColumns.moveColumns(cell.pressIndex, cell.DelegateModel.itemsIndex); } } } } onCentroidChanged: { if (!active) return; const p = centroid.position; cell.lastMoveX = cell.x; const cursorAbsX = cell.mapToItem(columnList.contentItem, p.x, 0).x; const centerAbsX = cursorAbsX - dragHandler.centerOffset; const movingRight = p.x > cell.pressPos.x; const grabbedRight = dragHandler.centerOffset > 0; const useCursor = movingRight === grabbedRight; const refX = useCursor ? cursorAbsX : centerAbsX; const myIndex = cell.DelegateModel.itemsIndex; const rightItem = columnList.itemAtIndex(myIndex + 1); if (rightItem) { const rightMid = rightItem.x + rightItem.width / 2; if (refX > rightMid) { columnsVisual.items.move(myIndex, myIndex + 1); return; } } const leftItem = columnList.itemAtIndex(myIndex - 1); if (leftItem) { const leftMid = leftItem.x + leftItem.width / 2; const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId; if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) { columnsVisual.items.move(myIndex, myIndex - 1); } } } } Anim { id: returnAnim properties: "x,y" target: cellContent to: 0 } Item { id: cellVisual x: cell.gapShrink * cell.gapX y: 0 width: cell.width - cell.gapShrink * cell.gapProg height: cell.height Item { id: cellContent width: cellVisual.width height: cellVisual.height readonly property int topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2) readonly property int topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2) readonly property int bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2) readonly property int bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2) Elevation { id: elevation anchors.fill: parent level: 3 opacity: 0 topLeftRadius: parent.topLeftRadius topRightRadius: parent.topRightRadius bottomLeftRadius: parent.bottomLeftRadius bottomRightRadius: parent.bottomRightRadius } CustomRect { id: cellBg anchors.fill: parent color: cell.isActiveSort ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2) topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2) bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2) bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2) StateLayer { id: layer color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface manualHoverOverride: cell.held onClicked: { Processes.sortBy(ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0); root.pList.positionViewAtBeginning(); } } } Item { anchors.fill: parent anchors.leftMargin: Tokens.padding.medium anchors.rightMargin: 12 CustomText { id: title anchors.left: parent.left anchors.right: removeBtn.visible ? removeBtn.left : parent.right anchors.rightMargin: Tokens.spacing.extraSmall anchors.verticalCenter: parent.verticalCenter color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant elide: CustomText.ElideRight text: ProcessesColumns.defFor(cell.modelData.id)?.title ?? cell.modelData.id } IconButton { id: removeBtn anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter type: IconButton.Text icon: "close" font: Tokens.font.icon.small inactiveOnColor: Colors.palette.m3onPrimary visible: cell.modelData.id !== ProcessesColumns.pinnedColumnId onClicked: ProcessesColumns.removeColumn(cell.DelegateModel.itemsIndex) } MaterialIcon { anchors.right: removeBtn.visible ? removeBtn.left : parent.right anchors.verticalCenter: parent.verticalCenter text: "keyboard_arrow_down" opacity: cell.isActiveSort ? 1 : 0 visible: opacity > 0 rotation: !Processes.sortAscending && opacity > 0 ? 180 : 0 color: Colors.palette.m3onPrimary Behavior on rotation { Anim {} } Behavior on opacity { Anim {} } } } } } } component HiddenColumnItem: MenuItem { required property var modelData text: modelData.title } }