more unfinished changes, process tab for dashboard
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var columnDefs: [
|
||||
{
|
||||
id: "command",
|
||||
title: qsTr("Command"),
|
||||
width: 200,
|
||||
field: 3
|
||||
},
|
||||
{
|
||||
id: "pid",
|
||||
title: qsTr("PID"),
|
||||
width: 60,
|
||||
field: 1
|
||||
},
|
||||
{
|
||||
id: "cpuUsage",
|
||||
title: qsTr("CPU %"),
|
||||
width: 70,
|
||||
field: 7
|
||||
},
|
||||
{
|
||||
id: "memoryUsage",
|
||||
title: qsTr("Memory"),
|
||||
width: 90,
|
||||
field: 8
|
||||
},
|
||||
{
|
||||
id: "elapsed",
|
||||
title: qsTr("Elapsed"),
|
||||
width: 80,
|
||||
field: 9
|
||||
},
|
||||
{
|
||||
id: "ppid",
|
||||
title: qsTr("PPID"),
|
||||
width: 60,
|
||||
field: 2
|
||||
},
|
||||
{
|
||||
id: "user",
|
||||
title: qsTr("User"),
|
||||
width: 100,
|
||||
field: 11
|
||||
},
|
||||
{
|
||||
id: "uid",
|
||||
title: qsTr("UID"),
|
||||
width: 60,
|
||||
field: 10
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
title: qsTr("State"),
|
||||
width: 60,
|
||||
field: 5
|
||||
},
|
||||
{
|
||||
id: "sleeping",
|
||||
title: qsTr("Sleeping"),
|
||||
width: 80,
|
||||
field: 6
|
||||
},
|
||||
{
|
||||
id: "commandLine",
|
||||
title: qsTr("Command line"),
|
||||
width: 240,
|
||||
field: 4
|
||||
},
|
||||
{
|
||||
id: "ioReadBytes",
|
||||
title: qsTr("IO read"),
|
||||
width: 90,
|
||||
field: 12
|
||||
},
|
||||
{
|
||||
id: "ioWriteBytes",
|
||||
title: qsTr("IO write"),
|
||||
width: 90,
|
||||
field: 13
|
||||
}
|
||||
]
|
||||
|
||||
readonly property string pinnedColumnId: "command"
|
||||
|
||||
readonly property var hiddenColumns: {
|
||||
const visible = Config.dashboard.processColumns.values.map(e => e.id);
|
||||
return root.columnDefs.filter(d => !visible.includes(d.id));
|
||||
}
|
||||
|
||||
function pinnedIndex(): int {
|
||||
const values = Config.dashboard.processColumns.values;
|
||||
for (let i = 0; i < values.length; i++)
|
||||
if (values[i].id === root.pinnedColumnId)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function ensurePinnedFirst() {
|
||||
const i = root.pinnedIndex();
|
||||
if (i > 0)
|
||||
Qt.callLater(() => {
|
||||
const idx = root.pinnedIndex();
|
||||
if (idx > 0)
|
||||
Config.dashboard.processColumns.move(idx, 0);
|
||||
});
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(ensurePinnedFirst)
|
||||
|
||||
function defFor(id: string): var {
|
||||
for (const def of root.columnDefs)
|
||||
if (def.id === id)
|
||||
return def;
|
||||
return null;
|
||||
}
|
||||
|
||||
function addColumn(id: string) {
|
||||
const def = root.defFor(id);
|
||||
Config.dashboard.processColumns.insert({
|
||||
id: id,
|
||||
width: def ? def.width : 100
|
||||
});
|
||||
}
|
||||
|
||||
function removeColumn(index: int) {
|
||||
const entry = Config.dashboard.processColumns.at(index);
|
||||
if (entry && entry.id === root.pinnedColumnId)
|
||||
return;
|
||||
Config.dashboard.processColumns.remove(index);
|
||||
}
|
||||
|
||||
function moveColumns(from: int, to: int) {
|
||||
const list = Config.dashboard.processColumns;
|
||||
const entry = list.at(from);
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry.id === root.pinnedColumnId)
|
||||
return;
|
||||
if (to === 0 && list.at(0)?.id === root.pinnedColumnId)
|
||||
return;
|
||||
|
||||
list.move(from, to);
|
||||
|
||||
const i = root.pinnedIndex();
|
||||
if (i > 0)
|
||||
list.move(i, 0);
|
||||
}
|
||||
|
||||
function resizeColumn(index: int, width: real) {
|
||||
const entry = Config.dashboard.processColumns.at(index);
|
||||
if (entry)
|
||||
entry.width = Math.max(50, width);
|
||||
}
|
||||
|
||||
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
|
||||
return overshoot / (1 + overshoot / maxOvershoot);
|
||||
}
|
||||
|
||||
function formatCell(id: string, value: var): string {
|
||||
switch (id) {
|
||||
case "cpuUsage":
|
||||
return value.toFixed(1) + "%";
|
||||
case "memoryUsage":
|
||||
return root.formatBytes(value * 1024);
|
||||
case "ioReadBytes":
|
||||
case "ioWriteBytes":
|
||||
return root.formatBytes(value);
|
||||
case "elapsed":
|
||||
return root.formatElapsed(value);
|
||||
case "sleeping":
|
||||
return value ? qsTr("Yes") : qsTr("No");
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: var): string {
|
||||
if (bytes === undefined || bytes === null || bytes <= 0)
|
||||
return "0 B";
|
||||
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
||||
let v = Number(bytes);
|
||||
let i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
++i;
|
||||
}
|
||||
return (i === 0 ? v.toFixed(0) : v.toFixed(1)) + " " + units[i];
|
||||
}
|
||||
|
||||
function formatElapsed(secs: var): string {
|
||||
const s = Math.max(0, Math.floor(Number(secs)));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const ss = s % 60;
|
||||
if (h > 0)
|
||||
return h + "h " + m + "m";
|
||||
if (m > 0)
|
||||
return m + "m " + ss + "s";
|
||||
return ss + "s";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: columnsData
|
||||
function onValuesChanged() {
|
||||
root.fillMissingWidths();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(fillMissingWidths)
|
||||
Component.onDestruction: Cursor.clearOverride()
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomListView {
|
||||
id: columnList
|
||||
|
||||
Layout.fillWidth: true
|
||||
orientation: ListView.Horizontal
|
||||
interactive: false
|
||||
spacing: parent.spacing
|
||||
implicitHeight: root.headerHeight
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: addColumnBtn
|
||||
|
||||
type: IconButton.Text
|
||||
icon: "add"
|
||||
font: Tokens.font.icon.medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: ProcessesColumns.hiddenColumns.length > 0
|
||||
onClicked: addMenu.expanded = true
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: hiddenColumnList
|
||||
|
||||
model: ProcessesColumns.hiddenColumns
|
||||
|
||||
HiddenColumnItem {}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: addMenu
|
||||
|
||||
attachTo: addColumnBtn
|
||||
items: hiddenColumnList.instances
|
||||
onItemSelected: item => ProcessesColumns.addColumn(item.modelData.id)
|
||||
}
|
||||
}
|
||||
|
||||
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: rowLayout
|
||||
z: 10
|
||||
|
||||
Connections {
|
||||
target: columnsData
|
||||
function onValuesChanged() {
|
||||
resizeOverlay.geometryTick++;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: columnList
|
||||
function onCountChanged() {
|
||||
resizeOverlay.geometryTick++;
|
||||
}
|
||||
function onContentWidthChanged() {
|
||||
resizeOverlay.geometryTick++;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
resizeOverlay.geometryTick++;
|
||||
Qt.callLater(() => resizeOverlay.geometryTick++);
|
||||
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);
|
||||
resizeOverlay.geometryTick++;
|
||||
Qt.callLater(() => resizeOverlay.geometryTick++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
required property ProcessesHeader columnHeader
|
||||
required property int rowHeight
|
||||
property real rowSpacing: Tokens.spacing.small
|
||||
|
||||
model: Processes.list
|
||||
spacing: root.rowSpacing
|
||||
clip: true
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
delegate: CustomRect {
|
||||
id: processRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
id: rowHover
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomClippingRect {
|
||||
id: iconClip
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: Tokens.padding.medium
|
||||
implicitWidth: height
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
implicitSize: height
|
||||
source: Quickshell.iconPath(processRow.modelData.iconName, "application-x-executable")
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: cellRepeater
|
||||
|
||||
model: root.columnHeader.columnsModel
|
||||
|
||||
Item {
|
||||
id: rowCell
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
implicitWidth: root.columnHeader.widthForColumn(modelData.id, modelData.width) - (index === 0 ? iconClip.implicitWidth + iconClip.Layout.margins * 2 + rowLayout.spacing : 0)
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
Layout.preferredWidth: implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
CustomRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.leftMargin: -(rowLayout.spacing)
|
||||
|
||||
implicitWidth: 2
|
||||
color: Colors.palette.m3outlineVariant
|
||||
visible: rowCell.index !== 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.leftMargin: rowCell.index !== 0 ? Tokens.padding.medium : 0
|
||||
anchors.rightMargin: Tokens.padding.medium
|
||||
implicitHeight: mainText.implicitHeight + subText.implicitHeight + mainText.anchors.margins * 2 + subText.anchors.topMargin
|
||||
|
||||
CustomText {
|
||||
id: mainText
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
elide: CustomText.ElideRight
|
||||
text: {
|
||||
const value = processRow.modelData[rowCell.modelData.id];
|
||||
return value === undefined ? "" : ProcessesColumns.formatCell(rowCell.modelData.id, value);
|
||||
}
|
||||
|
||||
states: State {
|
||||
name: "centerAnchored"
|
||||
when: !subText.visible
|
||||
|
||||
AnchorChanges {
|
||||
target: mainText
|
||||
anchors.top: undefined
|
||||
anchors.verticalCenter: layout.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subText
|
||||
anchors.top: mainText.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: rowCell.modelData.id === "command"
|
||||
elide: CustomText.ElideRight
|
||||
text: rowCell.modelData.id === "command" ? processRow.modelData["commandLine"] : ""
|
||||
font: Tokens.font.label.small
|
||||
color: Colors.palette.m3outline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
UsageCard {
|
||||
accent: Colors.palette.m3primary
|
||||
buffer: ResourceUsage.cpuBuffer
|
||||
icon: "memory"
|
||||
label: qsTr("CPU")
|
||||
values: ResourceUsage.cpuInfo
|
||||
}
|
||||
|
||||
UsageCard {
|
||||
accent: Colors.palette.m3tertiary
|
||||
buffer: ResourceUsage.memoryBuffer
|
||||
icon: "memory_alt"
|
||||
label: qsTr("Memory")
|
||||
values: ResourceUsage.memoryInfo
|
||||
}
|
||||
}
|
||||
|
||||
component UsageCard: CustomRect {
|
||||
id: card
|
||||
|
||||
property color accent
|
||||
required property var buffer
|
||||
property string icon
|
||||
property string label
|
||||
property list<var> values
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
color: card.accent
|
||||
text: card.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: card.label
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: graphWrapper
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: infoColumn.implicitHeight
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: infoColumn.left
|
||||
|
||||
SparklineItem {
|
||||
id: sparkline
|
||||
|
||||
anchors.fill: parent
|
||||
historyLength: ResourceUsage.historyLength
|
||||
line1: card.buffer
|
||||
line1Color: card.accent
|
||||
line1FillAlpha: 0.15
|
||||
maxValue: 1.0
|
||||
slideProgress: 1
|
||||
|
||||
Connections {
|
||||
function onValuesChanged(): void {
|
||||
slideAnim.restart();
|
||||
}
|
||||
|
||||
target: card.buffer
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: slideAnim
|
||||
|
||||
duration: Config.dashboard.resourceUpdateInterval
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
property: "slideProgress"
|
||||
target: sparkline
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
// "Collecting data" placeholder
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.palette.m3outline
|
||||
text: qsTr("Collecting data...")
|
||||
visible: card.buffer.count < 2
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: infoColumn
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: {
|
||||
const prefWidth = graphWrapper.width / 3;
|
||||
const minWidth = graphWrapper.width / 4;
|
||||
if (prefWidth < implicitWidth)
|
||||
return implicitWidth;
|
||||
else if (prefWidth >= implicitWidth * 2)
|
||||
return minWidth;
|
||||
else
|
||||
return prefWidth;
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: card.values
|
||||
|
||||
Item {
|
||||
id: moreInfo
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.leftMargin: Tokens.spacing.medium
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: label.implicitWidth + value.implicitWidth + label.anchors.margins * 2
|
||||
implicitHeight: label.implicitHeight
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: moreInfo.modelData.label
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: value
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: card.accent
|
||||
text: moreInfo.modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user