C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 45s
666 lines
17 KiB
QML
666 lines
17 KiB
QML
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
|
|
}
|
|
}
|