1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-26 14:32:52 -05:00

widgets: add spacer, divider, tweak interface

This commit is contained in:
bbedward
2025-08-02 13:10:39 -04:00
parent 2e85494236
commit 21c40b58bc
47 changed files with 2660 additions and 2205 deletions

View File

@@ -78,15 +78,15 @@ Item {
}
if (searchQuery.length === 0)
apps = apps.sort(function(a, b) {
var aId = a.id || (a.execString || a.exec || "");
var bId = b.id || (b.execString || b.exec || "");
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
if (aUsage !== bUsage)
return bUsage - aUsage;
var aId = a.id || (a.execString || a.exec || "");
var bId = b.id || (b.execString || b.exec || "");
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
if (aUsage !== bUsage)
return bUsage - aUsage;
return (a.name || "").localeCompare(b.name || "");
});
return (a.name || "").localeCompare(b.name || "");
});
// Convert to model format and populate
apps.forEach((app) => {

View File

@@ -10,9 +10,9 @@ import qs.Widgets
Column {
id: root
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : ""
width: parent.width
spacing: Theme.spacingM
@@ -50,21 +50,27 @@ Column {
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
let sinks = []
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sinks = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream) continue
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink) {
sinks.push(node)
}
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
sinks.push(node);
}
return sinks
return sinks;
}
Rectangle {
@@ -119,7 +125,9 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
@@ -131,8 +139,12 @@ Column {
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSink = modelData;
}
}
}
}
}
}

View File

@@ -10,9 +10,9 @@ import qs.Widgets
Column {
id: root
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : ""
width: parent.width
spacing: Theme.spacingM
@@ -50,21 +50,27 @@ Column {
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
let sources = []
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sources = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream) continue
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor")) {
sources.push(node)
}
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor"))
sources.push(node);
}
return sources
return sources;
}
Rectangle {
@@ -117,7 +123,9 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
@@ -129,8 +137,12 @@ Column {
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSource = modelData;
}
}
}
}
}
}

View File

@@ -9,10 +9,10 @@ import qs.Widgets
Column {
id: root
property real micLevel: Math.min(100, (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0)
property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
@@ -40,8 +40,10 @@ Column {
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted;
}
}
}
Item {
@@ -74,7 +76,9 @@ Column {
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
Rectangle {
@@ -90,16 +94,9 @@ Column {
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Rectangle {
id: micTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
@@ -111,24 +108,38 @@ Column {
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted) || micMouseArea.isDragging
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: Math.round(root.micLevel) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
MouseArea {
@@ -197,6 +208,7 @@ Column {
micMouseArea.isDragging = false;
}
}
}
DankIcon {
@@ -205,5 +217,7 @@ Column {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}

View File

@@ -10,7 +10,7 @@ import qs.Widgets
Column {
id: root
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
@@ -34,6 +34,7 @@ Column {
Rectangle {
id: scanButton
width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadius
@@ -61,6 +62,7 @@ Column {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
@@ -70,12 +72,14 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter) {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
}
}
}
}
Rectangle {
@@ -88,6 +92,7 @@ Column {
Column {
id: noteColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
@@ -110,6 +115,7 @@ Column {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
@@ -119,14 +125,16 @@ Column {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Repeater {
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
});
@@ -213,8 +221,10 @@ Column {
text: {
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return BluetoothService.getSignalStrength(modelData);
}
font.pixelSize: Theme.fontSizeSmall
@@ -242,9 +252,13 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
@@ -292,11 +306,12 @@ Column {
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData) {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
MouseArea {
@@ -308,12 +323,14 @@ Column {
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData) {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
Column {
@@ -322,11 +339,10 @@ Column {
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return availableCount === 0;
}
@@ -347,6 +363,7 @@ Column {
to: 360
duration: 2000
}
}
StyledText {
@@ -356,6 +373,7 @@ Column {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
@@ -364,6 +382,7 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
StyledText {
@@ -373,15 +392,15 @@ Column {
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return availableCount === 0 && !BluetoothService.adapter.discovering;
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}

View File

@@ -10,11 +10,11 @@ import qs.Widgets
Rectangle {
id: root
property var deviceData: null
property bool menuVisible: false
property var parentItem
function show(x, y) {
const menuWidth = 160;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
@@ -27,14 +27,14 @@ Rectangle {
root.visible = true;
root.menuVisible = true;
}
function hide() {
root.menuVisible = false;
Qt.callLater(() => {
root.visible = false;
});
}
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
@@ -45,7 +45,7 @@ Rectangle {
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
@@ -56,26 +56,26 @@ Rectangle {
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
size: Theme.iconSize - 2
@@ -83,7 +83,7 @@ Rectangle {
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
@@ -91,60 +91,63 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData) {
if (root.deviceData.connected) {
if (root.deviceData.connected)
root.deviceData.disconnect();
} else {
else
BluetoothService.connectDeviceWithTrust(root.deviceData);
}
}
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
@@ -152,7 +155,7 @@ Rectangle {
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
@@ -160,42 +163,49 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData) {
if (root.deviceData)
root.deviceData.forget();
}
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

View File

@@ -10,7 +10,7 @@ import qs.Widgets
Rectangle {
id: root
width: parent.width
height: 60
radius: Theme.cornerRadius
@@ -47,7 +47,9 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
@@ -57,9 +59,10 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter) {
if (BluetoothService.adapter)
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
}
}
}
}
}

View File

@@ -10,9 +10,9 @@ import qs.Widgets
Column {
id: root
property var bluetoothContextMenuWindow
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
@@ -84,8 +84,11 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
@@ -126,7 +129,9 @@ Column {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
@@ -138,13 +143,15 @@ Column {
enabled: !BluetoothService.isDeviceBusy(modelData)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
onClicked: {
if (modelData.connected) {
if (modelData.connected)
modelData.disconnect();
} else {
else
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
}
}

View File

@@ -5,9 +5,9 @@ import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter.Bluetooth
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Bluetooth
Item {
id: bluetoothTab
@@ -22,18 +22,23 @@ Item {
width: parent.width
spacing: Theme.spacingL
BluetoothToggle { }
BluetoothToggle {
}
PairedDevicesList {
bluetoothContextMenuWindow: bluetoothContextMenuWindow
}
AvailableDevicesList { }
AvailableDevicesList {
}
}
}
BluetoothContextMenu {
id: bluetoothContextMenuWindow
parentItem: bluetoothTab
}
@@ -52,5 +57,7 @@ Item {
onClicked: {
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,25 +4,27 @@ import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Modules
import qs.Services
import qs.Widgets
ScrollView {
id: displayTab
clip: true
property var brightnessDebounceTimer: Timer {
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight
repeat: false
property var brightnessDebounceTimer
brightnessDebounceTimer: Timer {
property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight
repeat: false
onTriggered: {
console.log("Debounce timer fired, setting brightness to:", pendingValue);
BrightnessService.setBrightness(pendingValue);
}
}
clip: true
Column {
width: parent.width
spacing: Theme.spacingL

View File

@@ -9,13 +9,14 @@ import qs.Widgets
Rectangle {
id: ethernetCard
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
}
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
@@ -47,6 +48,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
StyledText {
@@ -56,11 +58,13 @@ Rectangle {
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
// Loading spinner for preference changes
DankIcon {
id: ethernetLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
@@ -69,7 +73,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet"
z: 10
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
@@ -79,11 +83,13 @@ Rectangle {
duration: 1000
loops: Animation.Infinite
}
}
// Ethernet toggle switch (matching WiFi style)
DankToggle {
id: ethernetToggle
checked: NetworkService.ethernetConnected
enabled: true
anchors.right: parent.right
@@ -97,6 +103,7 @@ Rectangle {
// MouseArea for network preference (excluding toggle area)
MouseArea {
id: ethernetPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
@@ -118,5 +125,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

@@ -9,25 +9,31 @@ import qs.Widgets
Rectangle {
id: wifiCard
property var refreshTimer
function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) {
case "excellent": return "wifi";
case "good": return "wifi_2_bar";
case "fair": return "wifi_1_bar";
case "poor": return "signal_wifi_0_bar";
default: return "wifi";
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "signal_wifi_0_bar";
default:
return "wifi";
}
}
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
}
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
@@ -47,13 +53,12 @@ Rectangle {
DankIcon {
name: {
if (!NetworkService.wifiEnabled) {
if (!NetworkService.wifiEnabled)
return "wifi_off";
} else if (NetworkService.currentWifiSSID !== "") {
else if (NetworkService.currentWifiSSID !== "")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength);
} else {
else
return "wifi";
}
}
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
@@ -62,13 +67,12 @@ Rectangle {
StyledText {
text: {
if (!NetworkService.wifiEnabled) {
if (!NetworkService.wifiEnabled)
return "WiFi is off";
} else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID) {
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID || "Connected";
} else {
else
return "Not Connected";
}
}
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
@@ -76,28 +80,30 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
StyledText {
text: {
if (!NetworkService.wifiEnabled) {
if (!NetworkService.wifiEnabled)
return "Turn on WiFi to see networks";
} else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID) {
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.wifiIP || "Connected";
} else {
else
return "Select a network below";
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
// Loading spinner for preference changes
DankIcon {
id: wifiLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
@@ -106,7 +112,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi"
z: 10
RotationAnimation {
target: wifiLoadingSpinner
property: "rotation"
@@ -116,11 +122,13 @@ Rectangle {
duration: 1000
loops: Animation.Infinite
}
}
// WiFi toggle switch
DankToggle {
id: wifiToggle
checked: NetworkService.wifiEnabled
enabled: true
toggling: NetworkService.wifiToggling
@@ -140,15 +148,16 @@ Rectangle {
NetworkService.refreshNetworkStatus();
}
NetworkService.toggleWifiRadio();
if (refreshTimer) {
if (refreshTimer)
refreshTimer.triggered = true;
}
}
}
// MouseArea for network preference (excluding toggle area)
MouseArea {
id: wifiPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
@@ -170,5 +179,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

@@ -19,20 +19,16 @@ Rectangle {
function show(x, y) {
const menuWidth = 160;
wifiContextMenuWindow.visible = true;
Qt.callLater(() => {
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y + 4;
finalX = Math.max(Theme.spacingS, Math.min(finalX, parentItem.width - menuWidth - Theme.spacingS));
finalY = Math.max(Theme.spacingS, Math.min(finalY, parentItem.height - menuHeight - Theme.spacingS));
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
finalY = y - menuHeight - 4;
finalY = Math.max(Theme.spacingS, finalY);
}
wifiContextMenuWindow.x = finalX;
wifiContextMenuWindow.y = finalY;
wifiContextMenuWindow.menuVisible = true;
@@ -56,7 +52,6 @@ Rectangle {
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Component.onCompleted: {
menuVisible = false;
visible = false;
@@ -76,6 +71,7 @@ Rectangle {
Column {
id: wifiMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
@@ -108,10 +104,12 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -142,7 +140,9 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Separator
@@ -158,6 +158,7 @@ Rectangle {
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
// Forget Network option (only for saved networks)
@@ -189,17 +190,19 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData) {
if (wifiContextMenuWindow.networkData)
NetworkService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid);
}
wifiContextMenuWindow.hide();
}
}
@@ -209,7 +212,9 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Network Info option
@@ -240,17 +245,19 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: infoWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData && networkInfoModalRef) {
if (wifiContextMenuWindow.networkData && networkInfoModalRef)
networkInfoModalRef.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData);
}
wifiContextMenuWindow.hide();
}
}
@@ -260,8 +267,11 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
@@ -269,6 +279,7 @@ Rectangle {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
@@ -276,5 +287,7 @@ Rectangle {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

View File

@@ -9,21 +9,26 @@ import qs.Widgets
Column {
id: root
property var wifiContextMenuWindow
property var sortedWifiNetworks
property var wifiPasswordModalRef
function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) {
case "excellent": return "wifi";
case "good": return "wifi_2_bar";
case "fair": return "wifi_1_bar";
case "poor": return "signal_wifi_0_bar";
default: return "wifi";
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "signal_wifi_0_bar";
default:
return "wifi";
}
}
anchors.top: parent.top
anchors.topMargin: 100
anchors.left: parent.left
@@ -31,7 +36,7 @@ Column {
anchors.bottom: parent.bottom
visible: NetworkService.wifiEnabled
spacing: Theme.spacingS
// Available Networks Section with refresh button (spanning version)
Row {
width: parent.width
@@ -59,6 +64,7 @@ Column {
DankIcon {
id: refreshIconSpan
anchors.centerIn: parent
name: "refresh"
size: Theme.iconSize - 6
@@ -80,11 +86,14 @@ Column {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
id: refreshAreaSpan
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -96,9 +105,11 @@ Column {
}
}
}
}
}
// Scrollable networks container
Flickable {
width: parent.width
@@ -109,12 +120,13 @@ Column {
boundsBehavior: Flickable.DragAndOvershootBounds
flickDeceleration: 8000
maximumFlickVelocity: 15000
Column {
id: spanningNetworksColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
@@ -129,11 +141,12 @@ Column {
Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
// Signal strength icon
DankIcon {
id: signalIcon2
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: getWiFiSignalIcon(modelData.signalStrength)
@@ -164,29 +177,37 @@ Column {
text: {
if (modelData.connected)
return "Connected";
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return "Connecting...";
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return "Invalid password";
if (modelData.saved)
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
return modelData.secured ? "Secured" : "Open";
}
font.pixelSize: Theme.fontSizeSmall - 1
color: {
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return Theme.primary;
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
elide: Text.ElideRight
}
}
// Right side icons
Row {
id: rightIcons2
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
@@ -203,6 +224,7 @@ Column {
// Context menu button
Rectangle {
id: wifiMenuButton
width: 24
height: 24
radius: 12
@@ -218,6 +240,7 @@ Column {
MouseArea {
id: wifiMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -226,7 +249,6 @@ Column {
let buttonCenter = wifiMenuButtonArea.width / 2;
let buttonBottom = wifiMenuButtonArea.height;
let globalPos = wifiMenuButtonArea.mapToItem(wifiContextMenuWindow.parentItem, buttonCenter, buttonBottom);
Qt.callLater(() => {
wifiContextMenuWindow.show(globalPos.x, globalPos.y);
});
@@ -237,20 +259,25 @@ Column {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
MouseArea {
id: networkArea2
anchors.fill: parent
anchors.rightMargin: 32 // Exclude menu button area
anchors.rightMargin: 32 // Exclude menu button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected)
return;
return ;
if (modelData.saved) {
NetworkService.connectToWifi(modelData.ssid);
@@ -265,12 +292,17 @@ Column {
}
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}

View File

@@ -25,6 +25,7 @@ Item {
DankActionButton {
id: doNotDisturbButton
iconName: Prefs.doNotDisturb ? "notifications_off" : "notifications"
iconColor: Prefs.doNotDisturb ? Theme.error : Theme.surfaceText
buttonSize: 28
@@ -62,13 +63,18 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadiusLarge

View File

@@ -12,9 +12,50 @@ PanelWindow {
required property var notificationData
required property string notificationId
readonly property bool hasValidData: notificationData && notificationData.notification
property int screenY: 0
property bool exiting: false
property bool _isDestroying: false
property bool _finalized: false
signal entered()
signal exitFinished()
function startExit() {
if (exiting || _isDestroying)
return ;
exiting = true;
exitAnim.restart();
exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications)
NotificationService.removeFromVisibleNotifications(win.notificationData);
}
function forceExit() {
if (_isDestroying)
return ;
_isDestroying = true;
exiting = true;
visible = false;
exitWatchdog.stop();
finalizeExit("forced");
}
function finalizeExit(reason) {
if (_finalized)
return ;
_finalized = true;
_isDestroying = true;
exitWatchdog.stop();
wrapperConn.enabled = false;
wrapperConn.target = null;
win.exitFinished();
}
visible: hasValidData
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
@@ -22,6 +63,41 @@ PanelWindow {
color: "transparent"
implicitWidth: 400
implicitHeight: 122
onScreenYChanged: margins.top = Theme.barHeight + 4 + screenY
onHasValidDataChanged: {
if (!hasValidData && !exiting && !_isDestroying) {
console.warn("NotificationPopup: Data became invalid, forcing exit");
forceExit();
}
}
Component.onCompleted: {
if (hasValidData) {
Qt.callLater(() => {
return enterX.restart();
});
} else {
console.warn("NotificationPopup created with invalid data");
forceExit();
}
}
onNotificationDataChanged: {
if (!_isDestroying) {
wrapperConn.target = win.notificationData || null;
notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null;
}
}
onEntered: {
if (!_isDestroying)
enterDelay.start();
}
Component.onDestruction: {
_isDestroying = true;
exitWatchdog.stop();
if (notificationData && notificationData.timer)
notificationData.timer.stop();
}
anchors {
top: true
@@ -33,37 +109,11 @@ PanelWindow {
right: 12
}
property int screenY: 0
onScreenYChanged: margins.top = Theme.barHeight + 4 + screenY
Behavior on screenY {
id: screenYAnim
enabled: !exiting && !_isDestroying
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
property bool exiting: false
property bool _isDestroying: false
property bool _finalized: false
signal entered()
signal exitFinished()
onHasValidDataChanged: {
if (!hasValidData && !exiting && !_isDestroying) {
console.warn("NotificationPopup: Data became invalid, forcing exit");
forceExit();
}
}
Item {
id: content
anchors.fill: parent
visible: win.hasValidData
transform: Translate { id: tx; x: Anims.slidePx }
layer.enabled: (enterX.running || exitAnim.running)
layer.smooth: true
@@ -80,6 +130,7 @@ PanelWindow {
Rectangle {
id: shadowLayer1
anchors.fill: parent
anchors.margins: -3
color: "transparent"
@@ -91,6 +142,7 @@ PanelWindow {
Rectangle {
id: shadowLayer2
anchors.fill: parent
anchors.margins: -2
color: "transparent"
@@ -102,6 +154,7 @@ PanelWindow {
Rectangle {
id: shadowLayer3
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
@@ -133,11 +186,14 @@ PanelWindow {
position: 0.021
color: "transparent"
}
}
}
Item {
id: notificationContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -148,6 +204,7 @@ PanelWindow {
Rectangle {
id: iconContainer
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
width: 55
@@ -161,12 +218,14 @@ PanelWindow {
IconImage {
id: iconImage
anchors.fill: parent
anchors.margins: 2
asynchronous: true
source: {
if (!notificationData) return "";
if (!notificationData)
return "";
if (parent.hasNotificationImage)
return notificationData.cleanImage || "";
@@ -193,10 +252,12 @@ PanelWindow {
font.weight: Font.Bold
color: Theme.primaryText
}
}
Rectangle {
id: textContainer
anchors.left: iconContainer.right
anchors.leftMargin: 12
anchors.right: parent.right
@@ -219,7 +280,9 @@ PanelWindow {
StyledText {
width: parent.width
text: {
if (!notificationData) return "";
if (!notificationData)
return "";
const appName = notificationData.appName || "";
const timeStr = notificationData.timeStr || "";
if (timeStr.length > 0)
@@ -255,22 +318,29 @@ PanelWindow {
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: (link) => Qt.openUrlExternally(link)
onLinkActivated: (link) => {
return Qt.openUrlExternally(link);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
}
}
DankActionButton {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 12
@@ -282,6 +352,7 @@ PanelWindow {
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
@@ -292,20 +363,21 @@ PanelWindow {
anchors.bottomMargin: 8
spacing: 8
z: 20
Repeater {
model: notificationData ? (notificationData.actions || []) : []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: actionText
text: modelData.text || ""
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -313,7 +385,7 @@ PanelWindow {
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
@@ -322,20 +394,24 @@ PanelWindow {
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
if (modelData && modelData.invoke)
modelData.invoke();
}
if (notificationData && !win.exiting) {
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
}
}
}
}
Rectangle {
id: clearButton
property bool isHovered: false
anchors.right: parent.right
@@ -350,6 +426,7 @@ PanelWindow {
StyledText {
id: clearText
text: "Clear"
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -365,15 +442,17 @@ PanelWindow {
onEntered: clearButton.isHovered = true
onExited: clearButton.isHovered = false
onClicked: {
if (notificationData && !win.exiting) {
if (notificationData && !win.exiting)
NotificationService.dismissNotification(notificationData);
}
}
}
}
MouseArea {
id: cardHoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
@@ -382,153 +461,146 @@ PanelWindow {
onEntered: {
if (notificationData && notificationData.timer)
notificationData.timer.stop();
}
onExited: {
if (notificationData && notificationData.popup && notificationData.timer)
notificationData.timer.restart();
}
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
}
transform: Translate {
id: tx
x: Anims.slidePx
}
}
NumberAnimation {
id: enterX
target: tx; property: "x"; from: Anims.slidePx; to: 0
target: tx
property: "x"
from: Anims.slidePx
to: 0
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
onStopped: if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) win.entered();
onStopped: {
if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) {
win.entered();
}
}
}
ParallelAnimation {
id: exitAnim
PropertyAnimation {
target: tx; property: "x"; from: 0; to: Anims.slidePx
onStopped: finalizeExit("animStopped")
PropertyAnimation {
target: tx
property: "x"
from: 0
to: Anims.slidePx
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: content; property: "opacity"; from: 1; to: 0
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardAccel
}
NumberAnimation {
target: content; property: "scale"; from: 1; to: 0.98
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
onStopped: finalizeExit("animStopped")
}
Component.onCompleted: {
if (hasValidData) {
Qt.callLater(() => enterX.restart())
} else {
console.warn("NotificationPopup created with invalid data");
forceExit();
}
}
Connections {
id: wrapperConn
function onPopupChanged() {
if (!win.notificationData || win._isDestroying)
return ;
if (!win.notificationData.popup && !win.exiting)
startExit();
}
target: win.notificationData || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
function onPopupChanged() {
if (!win.notificationData || win._isDestroying) return;
if (!win.notificationData.popup && !win.exiting) {
startExit();
}
}
}
Connections {
id: notificationConn
function onDropped() {
if (!win._isDestroying && !win.exiting)
forceExit();
}
target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
function onDropped() {
if (!win._isDestroying && !win.exiting) {
forceExit();
}
}
}
onNotificationDataChanged: {
if (!_isDestroying) {
wrapperConn.target = win.notificationData || null;
notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null;
}
}
Timer {
id: enterDelay
interval: 160
repeat: false
onTriggered: {
if (notificationData && notificationData.timer && !exiting && !_isDestroying)
notificationData.timer.start();
}
}
onEntered: {
if (!_isDestroying) enterDelay.start();
}
function startExit() {
if (exiting || _isDestroying) return;
exiting = true;
exitAnim.restart();
exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications) {
NotificationService.removeFromVisibleNotifications(win.notificationData);
}
}
function forceExit() {
if (_isDestroying) return;
_isDestroying = true;
exiting = true;
visible = false;
exitWatchdog.stop();
finalizeExit("forced");
}
function finalizeExit(reason) {
if (_finalized) return;
_finalized = true;
_isDestroying = true;
exitWatchdog.stop();
wrapperConn.enabled = false;
wrapperConn.target = null;
win.exitFinished();
}
Timer {
Timer {
id: exitWatchdog
interval: 600
repeat: false
onTriggered: finalizeExit("watchdog")
}
Component.onDestruction: {
_isDestroying = true;
exitWatchdog.stop();
if (notificationData && notificationData.timer) {
notificationData.timer.stop();
Behavior on screenY {
id: screenYAnim
enabled: !exiting && !_isDestroying
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
}

View File

@@ -5,61 +5,113 @@ import qs.Services
QtObject {
id: manager
property var modelData
property int topMargin: 0
property int baseNotificationHeight: 120
property int maxTargetNotifications: 3
property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var popupWindows: [] // strong refs to windows (live until exitFinished)
// Track destroying windows to prevent duplicate cleanup
property var destroyingWindows: new Set()
// Factory
property Component popupComponent: Component {
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onEntered: manager._onPopupEntered(this)
onExitFinished: manager._onPopupExitFinished(this)
}
}
property Connections notificationConnections: Connections {
target: NotificationService
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications);
}
target: NotificationService
}
// Smart sweeper that only runs when needed
property Timer sweeper
sweeper: Timer {
interval: 2000
running: false // Not running by default
repeat: true
onTriggered: {
let toRemove = [];
for (let p of popupWindows) {
if (!p) {
toRemove.push(p);
continue;
}
// Check for various zombie conditions
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying);
if (isZombie) {
console.warn("Sweeper found zombie window, cleaning up");
toRemove.push(p);
// Try to force cleanup
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
try {
p.destroy();
} catch (e) {
console.warn("Error destroying zombie:", e);
}
}
}
}
// Remove all zombies from array
if (toRemove.length > 0) {
for (let zombie of toRemove) {
const i = popupWindows.indexOf(zombie);
if (i !== -1)
popupWindows.splice(i, 1);
}
popupWindows = popupWindows.slice();
// Recompact after cleanup
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY;
});
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
}
// Stop the timer if no windows remain
if (popupWindows.length === 0)
sweeper.stop();
}
}
function _hasWindowFor(w) {
return popupWindows.some(p => {
return popupWindows.some((p) => {
// More robust check for valid windows
return p &&
p.notificationData === w &&
!p._isDestroying &&
p.status !== Component.Null;
return p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null;
});
}
function _isValidWindow(p) {
return p &&
p.status !== Component.Null &&
!p._isDestroying &&
p.hasValidData;
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
}
function _sync(newWrappers) {
// Add new notifications
for (let w of newWrappers) {
if (w && !_hasWindowFor(w)) {
if (w && !_hasWindowFor(w))
insertNewestAtTop(w);
}
}
// Remove old notifications
for (let p of popupWindows.slice()) {
if (!_isValidWindow(p)) continue;
if (!_isValidWindow(p))
continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
p.notificationData.removedByLimit = true;
p.notificationData.popup = false;
@@ -71,73 +123,67 @@ QtObject {
function insertNewestAtTop(wrapper) {
if (!wrapper) {
console.warn("insertNewestAtTop: wrapper is null");
return;
return ;
}
// Shift live, non-exiting windows down *now*
for (let p of popupWindows) {
if (!_isValidWindow(p)) continue;
if (p.exiting) continue;
if (!_isValidWindow(p))
continue;
if (p.exiting)
continue;
p.screenY = p.screenY + baseNotificationHeight;
}
// Create the new top window at fixed Y
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "";
const win = popupComponent.createObject(null, {
notificationData: wrapper,
notificationId: notificationId,
screenY: topMargin,
screen: manager.modelData
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
});
if (!win) {
console.warn("Popup create failed");
return;
if (!win) {
console.warn("Popup create failed");
return ;
}
// Validate the window was created properly
if (!win.hasValidData) {
console.warn("Popup created with invalid data, destroying");
win.destroy();
return;
return ;
}
popupWindows.push(win);
// Start sweeper if it's not running
if (!sweeper.running) {
if (!sweeper.running)
sweeper.start();
}
_maybeStartOverflow();
}
// Overflow: keep one extra (slot #4), then ask bottom to exit gracefully
function _active() {
return popupWindows.filter(p => {
return _isValidWindow(p) &&
p.notificationData &&
p.notificationData.popup &&
!p.exiting;
function _active() {
return popupWindows.filter((p) => {
return _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting;
});
}
function _bottom() {
let b = null, maxY = -1;
for (let p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY;
b = p;
if (p.screenY > maxY) {
maxY = p.screenY;
b = p;
}
}
return b;
}
function _maybeStartOverflow() {
const activeWindows = _active();
if (activeWindows.length <= maxTargetNotifications + 1) return;
if (activeWindows.length <= maxTargetNotifications + 1)
return ;
const b = _bottom();
if (b && !b.exiting) {
// Tell the popup to animate out (don't destroy here)
@@ -148,34 +194,32 @@ QtObject {
// After entrance, you may kick overflow (optional)
function _onPopupEntered(p) {
if (_isValidWindow(p)) {
if (_isValidWindow(p))
_maybeStartOverflow();
}
}
// Primary cleanup path (after the popup finishes its exit)
function _onPopupExitFinished(p) {
if (!p) return;
if (!p)
return ;
// Prevent duplicate cleanup
const windowId = p.toString();
if (destroyingWindows.has(windowId)) {
return;
}
if (destroyingWindows.has(windowId))
return ;
destroyingWindows.add(windowId);
// Remove from popupWindows
const i = popupWindows.indexOf(p);
if (i !== -1) {
if (i !== -1) {
popupWindows.splice(i, 1);
popupWindows = popupWindows.slice();
popupWindows = popupWindows.slice();
}
// Release the wrapper
if (NotificationService.releaseWrapper && p.notificationData) {
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData);
}
// Schedule destruction
Qt.callLater(() => {
if (p && p.destroy) {
@@ -190,103 +234,40 @@ QtObject {
destroyingWindows.delete(windowId);
});
});
// Compact survivors (only live, non-exiting)
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY;
});
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
_maybeStartOverflow();
}
// Smart sweeper that only runs when needed
property Timer sweeper: Timer {
interval: 2000
running: false // Not running by default
repeat: true
onTriggered: {
let toRemove = [];
for (let p of popupWindows) {
if (!p) {
toRemove.push(p);
continue;
}
// Check for various zombie conditions
const isZombie =
p.status === Component.Null ||
(!p.visible && !p.exiting) ||
(!p.notificationData && !p._isDestroying) ||
(!p.hasValidData && !p._isDestroying);
if (isZombie) {
console.warn("Sweeper found zombie window, cleaning up");
toRemove.push(p);
// Try to force cleanup
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
try {
p.destroy();
} catch (e) {
console.warn("Error destroying zombie:", e);
}
}
}
}
// Remove all zombies from array
if (toRemove.length > 0) {
for (let zombie of toRemove) {
const i = popupWindows.indexOf(zombie);
if (i !== -1) {
popupWindows.splice(i, 1);
}
}
popupWindows = popupWindows.slice();
// Recompact after cleanup
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
}
// Stop the timer if no windows remain
if (popupWindows.length === 0) {
sweeper.stop();
}
}
}
// Watch for changes to popup windows array
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start();
} else if (popupWindows.length === 0 && sweeper.running) {
sweeper.stop();
}
}
// Emergency cleanup function
function cleanupAllWindows() {
sweeper.stop();
for (let p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit) p.forceExit();
else if (p.destroy) p.destroy();
if (p.forceExit)
p.forceExit();
else if (p.destroy)
p.destroy();
} catch (e) {
console.warn("Error during emergency cleanup:", e);
}
}
}
popupWindows = [];
destroyingWindows.clear();
destroyingWindows.clear();
}
}
// Watch for changes to popup windows array
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running)
sweeper.start();
else if (popupWindows.length === 0 && sweeper.running)
sweeper.stop();
}
}

View File

@@ -5,21 +5,6 @@ import qs.Services
import qs.Widgets
Column {
anchors.fill: parent
spacing: Theme.spacingM
Component.onCompleted: {
SysMonitorService.addRef();
SysMonitorService.addRef();
// Trigger immediate updates for both services
SysMonitorService.updateAllStats();
}
Component.onDestruction: {
SysMonitorService.removeRef();
SysMonitorService.removeRef();
}
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s";
@@ -40,6 +25,19 @@ Column {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
}
anchors.fill: parent
spacing: Theme.spacingM
Component.onCompleted: {
SysMonitorService.addRef();
SysMonitorService.addRef();
// Trigger immediate updates for both services
SysMonitorService.updateAllStats();
}
Component.onDestruction: {
SysMonitorService.removeRef();
SysMonitorService.removeRef();
}
Rectangle {
width: parent.width
height: 200
@@ -279,9 +277,7 @@ Column {
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ?
SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalSwapKB) :
"No swap configured"
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
@@ -309,10 +305,16 @@ Column {
height: parent.height
radius: parent.radius
color: {
if (!SysMonitorService.totalSwapKB) return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
if (!SysMonitorService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
const usage = SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB;
if (usage > 0.9) return Theme.error;
if (usage > 0.7) return Theme.warning;
if (usage > 0.9)
return Theme.error;
if (usage > 0.7)
return Theme.warning;
return Theme.info;
}
@@ -320,8 +322,11 @@ Column {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {

View File

@@ -12,24 +12,20 @@ Popup {
property var processData: null
function show(x, y) {
if (!processContextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay) {
if (!processContextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay)
processContextMenu.parent = Overlay.overlay;
}
const menuWidth = 180;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
const screenWidth = Screen.width;
const screenHeight = Screen.height;
let finalX = x;
let finalY = y;
if (x + menuWidth > screenWidth - 20) {
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth;
}
if (y + menuHeight > screenHeight - 20) {
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight;
}
processContextMenu.x = Math.max(20, finalX);
processContextMenu.y = Math.max(20, finalY);
@@ -41,29 +37,29 @@ Popup {
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: menuContent
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
@@ -71,6 +67,7 @@ Popup {
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
@@ -93,16 +90,18 @@ Popup {
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
if (processContextMenu.processData)
Quickshell.execDetached(["wl-copy", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
}
}
Rectangle {
@@ -123,6 +122,7 @@ Popup {
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -134,6 +134,7 @@ Popup {
processContextMenu.close();
}
}
}
Rectangle {
@@ -148,6 +149,7 @@ Popup {
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
@@ -170,17 +172,19 @@ Popup {
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData) {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
}
}
Rectangle {
@@ -203,18 +207,23 @@ Popup {
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData) {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
}
processContextMenu.close();
}
}
}
}
}
}
}

View File

@@ -144,10 +144,10 @@ Rectangle {
if (process && process.memoryKB > 1024 * 1024)
return Theme.error;
if (process && process.memoryKB > 512 * 1024)
if (process && process.memoryKB > 512 * 1024)
return Theme.warning;
return Theme.surfaceText;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
@@ -212,4 +212,4 @@ Rectangle {
}
}
}

View File

@@ -7,9 +7,9 @@ import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
import qs.Modules.ProcessList
PanelWindow {
id: processListPopout
@@ -20,9 +20,9 @@ PanelWindow {
function hide() {
isVisible = false;
// Close any open context menus
if (processContextMenu.visible) {
if (processContextMenu.visible)
processContextMenu.close();
}
}
function show() {
@@ -37,11 +37,6 @@ PanelWindow {
}
visible: isVisible
Ref {
service: SysMonitorService
}
implicitWidth: 600
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
@@ -49,6 +44,10 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
Ref {
service: SysMonitorService
}
anchors {
top: true
left: true
@@ -61,58 +60,58 @@ PanelWindow {
onClicked: function(mouse) {
// Only close if click is outside the content loader
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width ||
localPos.y < 0 || localPos.y > contentLoader.height) {
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0 || localPos.y > contentLoader.height)
processListPopout.hide();
}
}
}
Loader {
id: contentLoader
asynchronous: true
active: processListPopout.isVisible
readonly property real targetWidth: Math.min(600, Screen.width - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(600, Screen.height - Theme.barHeight - Theme.spacingS * 2)
asynchronous: true
active: processListPopout.isVisible
width: targetWidth
height: targetHeight
y: Theme.barHeight + Theme.spacingXS
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
// GPU-accelerated scale + opacity animation
opacity: processListPopout.isVisible ? 1 : 0
scale: processListPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: dropdownContent
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
// Remove layer rendering for better performance
antialiasing: true
smooth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
@@ -128,9 +127,11 @@ PanelWindow {
SystemOverview {
id: systemOverview
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
}
Rectangle {
@@ -146,12 +147,17 @@ PanelWindow {
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
}
}
ProcessContextMenu {
id: processContextMenu
}
}
}

View File

@@ -6,12 +6,12 @@ import qs.Widgets
Column {
id: root
property var contextMenu: null
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
@@ -31,19 +31,20 @@ Column {
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Process"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "name" ? 1.0 : 0.7
opacity: SysMonitorService.sortBy === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -53,10 +54,14 @@ Column {
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
@@ -74,12 +79,13 @@ Column {
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "cpu" ? 1.0 : 0.7
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -89,10 +95,14 @@ Column {
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
@@ -110,12 +120,13 @@ Column {
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "memory" ? 1.0 : 0.7
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -125,10 +136,14 @@ Column {
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
@@ -139,20 +154,21 @@ Column {
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "pid" ? 1.0 : 0.7
opacity: SysMonitorService.sortBy === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -162,10 +178,14 @@ Column {
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
@@ -214,6 +234,43 @@ Column {
property real stableY: 0
property bool isUserScrolling: false
property bool isScrollBarDragging: false
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
property string keyRoleName: "pid"
property var _anchorKey: undefined
property real _anchorOffset: 0
function captureAnchor() {
const y = contentY + 1;
const idx = indexAt(0, y);
if (idx < 0 || !model || idx >= model.length)
return ;
_anchorKey = model[idx][keyRoleName];
const it = itemAtIndex(idx);
_anchorOffset = it ? (y - it.y) : 0;
}
function restoreAnchor() {
Qt.callLater(function() {
if (_anchorKey === undefined || !model)
return ;
var i = -1;
for (var j = 0; j < model.length; ++j) {
if (model[j][keyRoleName] === _anchorKey) {
i = j;
break;
}
}
if (i < 0)
return ;
positionViewAtIndex(i, ListView.Beginning);
const maxY = Math.max(0, contentHeight - height);
contentY = Math.max(0, Math.min(maxY, contentY + _anchorOffset - 1));
});
}
width: parent.width
height: parent.height - columnHeaders.height
@@ -223,18 +280,37 @@ Column {
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 1500
maximumFlickVelocity: 2000
onMovementStarted: isUserScrolling = true
onMovementEnded: {
isUserScrolling = false
if (contentY > 40) {
stableY = contentY
}
isUserScrolling = false;
if (contentY > 40)
stableY = contentY;
}
onContentYChanged: {
if (!isUserScrolling && !isScrollBarDragging && visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
contentY = stableY;
}
onModelChanged: {
if (model && model.length > 0 && !isUserScrolling && stableY > 40)
// Preserve scroll position when model updates
Qt.callLater(function() {
contentY = stableY;
});
}
onContentYChanged: {
if (!isUserScrolling && !isScrollBarDragging && visible && stableY > 40 && Math.abs(contentY - stableY) > 10) {
contentY = stableY
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * processListView.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, processListView.contentHeight - processListView.height);
processListView.contentY = Math.max(0, Math.min(maxY, processListView.contentY - dy * processListView.wheelMultiplier));
ev.accepted = true;
}
}
@@ -243,74 +319,22 @@ Column {
contextMenu: root.contextMenu
}
ScrollBar.vertical: ScrollBar {
ScrollBar.vertical: ScrollBar {
id: verticalScrollBar
policy: ScrollBar.AsNeeded
policy: ScrollBar.AsNeeded
onPressedChanged: {
processListView.isScrollBarDragging = pressed
if (!pressed && processListView.contentY > 40) {
processListView.stableY = processListView.contentY
}
}
}
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff }
processListView.isScrollBarDragging = pressed;
if (!pressed && processListView.contentY > 40)
processListView.stableY = processListView.contentY;
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0
? ev.pixelDelta.y
: (ev.angleDelta.y / 120) * processListView.wheelBaseStep;
if (ev.inverted) dy = -dy;
const maxY = Math.max(0, processListView.contentHeight - processListView.height);
processListView.contentY = Math.max(0, Math.min(maxY,
processListView.contentY - dy * processListView.wheelMultiplier));
ev.accepted = true;
}
}
property string keyRoleName: "pid"
property var _anchorKey: undefined
property real _anchorOffset: 0
function captureAnchor() {
const y = contentY + 1;
const idx = indexAt(0, y);
if (idx < 0 || !model || idx >= model.length) return;
_anchorKey = model[idx][keyRoleName];
const it = itemAtIndex(idx);
_anchorOffset = it ? (y - it.y) : 0;
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
function restoreAnchor() {
Qt.callLater(function() {
if (_anchorKey === undefined || !model) return;
var i = -1;
for (var j = 0; j < model.length; ++j) {
if (model[j][keyRoleName] === _anchorKey) { i = j; break; }
}
if (i < 0) return;
positionViewAtIndex(i, ListView.Beginning);
const maxY = Math.max(0, contentHeight - height);
contentY = Math.max(0, Math.min(maxY, contentY + _anchorOffset - 1));
});
}
onModelChanged: {
if (model && model.length > 0 && !isUserScrolling && stableY > 40) {
// Preserve scroll position when model updates
Qt.callLater(function() {
contentY = stableY
})
}
}
}
}
}

View File

@@ -1,15 +1,16 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Modules.ProcessList
import qs.Services
ColumnLayout {
id: processesTab
property var contextMenu: null
anchors.fill: parent
spacing: Theme.spacingM
property var contextMenu: null
SystemOverview {
Layout.fillWidth: true
@@ -24,4 +25,5 @@ ColumnLayout {
ProcessContextMenu {
id: localContextMenu
}
}
}

View File

@@ -6,11 +6,9 @@ import qs.Widgets
Row {
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}

View File

@@ -9,11 +9,9 @@ ScrollView {
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
@@ -31,6 +29,7 @@ ScrollView {
Column {
id: systemInfoColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -108,6 +107,7 @@ ScrollView {
Column {
id: hardwareColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -133,6 +133,7 @@ ScrollView {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
@@ -169,7 +170,9 @@ ScrollView {
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
@@ -182,6 +185,7 @@ ScrollView {
Column {
id: memoryColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -207,6 +211,7 @@ ScrollView {
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
@@ -234,7 +239,9 @@ ScrollView {
width: parent.width
height: Theme.fontSizeSmall + Theme.spacingXS
}
}
}
}
@@ -243,7 +250,6 @@ ScrollView {
}
Rectangle {
width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL
@@ -253,6 +259,7 @@ ScrollView {
Column {
id: storageColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -281,181 +288,178 @@ ScrollView {
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Repeater {
id: diskMountRepeater
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
model: SysMonitorService.diskMounts
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: diskMouseArea
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
anchors.fill: parent
hoverEnabled: true
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: SysMonitorService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: {
const percent = parseInt(modelData.percent);
if (percent > 90)
return Theme.error;
if (percent > 75)
return Theme.warning;
return Theme.surfaceText;
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: {
const percent = parseInt(modelData.percent);
if (percent > 90)
return Theme.error;
if (percent > 75)
return Theme.warning;
return Theme.surfaceText;
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
@@ -464,6 +468,8 @@ ScrollView {
}
}
}
}

View File

@@ -109,11 +109,10 @@ ScrollView {
description: "Select system font family"
currentValue: {
// Always show the font name in parentheses for clarity
if (Prefs.fontFamily === Prefs.defaultFontFamily) {
if (Prefs.fontFamily === Prefs.defaultFontFamily)
return "Default (" + Prefs.defaultFontFamily + ")";
} else {
else
return Prefs.fontFamily || "Default (" + Prefs.defaultFontFamily + ")";
}
}
enableFuzzySearch: true
popupWidthOffset: 100
@@ -757,18 +756,17 @@ ScrollView {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
// System App Theming Section
StyledRect {
@@ -816,9 +814,9 @@ ScrollView {
checked: Colors.gtkThemingEnabled && Prefs.gtkThemingEnabled
onToggled: function(checked) {
Prefs.setGtkThemingEnabled(checked);
if (checked && Theme.isDynamicTheme) {
if (checked && Theme.isDynamicTheme)
Colors.generateGtkThemes();
}
}
}
@@ -830,9 +828,9 @@ ScrollView {
checked: Colors.qtThemingEnabled && Prefs.qtThemingEnabled
onToggled: function(checked) {
Prefs.setQtThemingEnabled(checked);
if (checked && Theme.isDynamicTheme) {
if (checked && Theme.isDynamicTheme)
Colors.generateQtThemes();
}
}
}

View File

@@ -6,257 +6,324 @@ import qs.Widgets
ScrollView {
id: widgetsTab
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
property var baseWidgetDefinitions: [
{
id: "launcherButton",
text: "App Launcher",
description: "Quick access to application launcher",
icon: "apps",
enabled: true
},
{
id: "workspaceSwitcher",
text: "Workspace Switcher",
description: "Shows current workspace and allows switching",
icon: "view_module",
enabled: true
},
{
id: "focusedWindow",
text: "Focused Window",
description: "Display currently focused application title",
icon: "window",
enabled: true
},
{
id: "clock",
text: "Clock",
description: "Current time and date display",
icon: "schedule",
enabled: true
},
{
id: "weather",
text: "Weather Widget",
description: "Current weather conditions and temperature",
icon: "wb_sunny",
enabled: true
},
{
id: "music",
text: "Media Controls",
description: "Control currently playing media",
icon: "music_note",
enabled: true
},
{
id: "clipboard",
text: "Clipboard Manager",
description: "Access clipboard history",
icon: "content_paste",
enabled: true
},
{
id: "systemResources",
text: "System Resources",
description: "CPU and memory usage indicators",
icon: "memory",
enabled: true
},
{
id: "systemTray",
text: "System Tray",
description: "System notification area icons",
icon: "notifications",
enabled: true
},
{
id: "controlCenterButton",
text: "Control Center",
description: "Access to system controls and settings",
icon: "settings",
enabled: true
},
{
id: "notificationButton",
text: "Notification Center",
description: "Access to notifications and do not disturb",
icon: "notifications",
enabled: true
},
{
id: "battery",
text: "Battery",
description: "Battery level and power management",
icon: "battery_std",
enabled: true
},
{
id: "spacer",
text: "Spacer",
description: "Empty space to separate widgets",
icon: "more_horiz",
enabled: true
},
{
id: "separator",
text: "Separator",
description: "Visual divider between widgets",
icon: "remove",
enabled: true
}
]
property var baseWidgetDefinitions: [{
"id": "launcherButton",
"text": "App Launcher",
"description": "Quick access to application launcher",
"icon": "apps",
"enabled": true
}, {
"id": "workspaceSwitcher",
"text": "Workspace Switcher",
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",
"description": "Display currently focused application title",
"icon": "window",
"enabled": true
}, {
"id": "clock",
"text": "Clock",
"description": "Current time and date display",
"icon": "schedule",
"enabled": true
}, {
"id": "weather",
"text": "Weather Widget",
"description": "Current weather conditions and temperature",
"icon": "wb_sunny",
"enabled": true
}, {
"id": "music",
"text": "Media Controls",
"description": "Control currently playing media",
"icon": "music_note",
"enabled": true
}, {
"id": "clipboard",
"text": "Clipboard Manager",
"description": "Access clipboard history",
"icon": "content_paste",
"enabled": true
}, {
"id": "systemResources",
"text": "System Resources",
"description": "CPU and memory usage indicators",
"icon": "memory",
"enabled": true
}, {
"id": "systemTray",
"text": "System Tray",
"description": "System notification area icons",
"icon": "notifications",
"enabled": true
}, {
"id": "controlCenterButton",
"text": "Control Center",
"description": "Access to system controls and settings",
"icon": "settings",
"enabled": true
}, {
"id": "notificationButton",
"text": "Notification Center",
"description": "Access to notifications and do not disturb",
"icon": "notifications",
"enabled": true
}, {
"id": "battery",
"text": "Battery",
"description": "Battery level and power management",
"icon": "battery_std",
"enabled": true
}, {
"id": "spacer",
"text": "Spacer",
"description": "Customizable empty space",
"icon": "more_horiz",
"enabled": true
}, {
"id": "separator",
"text": "Separator",
"description": "Visual divider between widgets",
"icon": "remove",
"enabled": true
}]
// Default widget configurations for each section (with enabled states)
property var defaultLeftWidgets: [
{id: "launcherButton", enabled: true},
{id: "workspaceSwitcher", enabled: true},
{id: "focusedWindow", enabled: true}
]
property var defaultCenterWidgets: [
{id: "music", enabled: true},
{id: "clock", enabled: true},
{id: "weather", enabled: true}
]
property var defaultRightWidgets: [
{id: "systemTray", enabled: true},
{id: "clipboard", enabled: true},
{id: "systemResources", enabled: true},
{id: "notificationButton", enabled: true},
{id: "battery", enabled: true},
{id: "controlCenterButton", enabled: true}
]
Component.onCompleted: {
// Initialize sections with defaults if they're empty
if (!Prefs.topBarLeftWidgets || Prefs.topBarLeftWidgets.length === 0) {
Prefs.setTopBarLeftWidgets(defaultLeftWidgets)
}
if (!Prefs.topBarCenterWidgets || Prefs.topBarCenterWidgets.length === 0) {
Prefs.setTopBarCenterWidgets(defaultCenterWidgets)
}
if (!Prefs.topBarRightWidgets || Prefs.topBarRightWidgets.length === 0) {
Prefs.setTopBarRightWidgets(defaultRightWidgets)
}
}
property var defaultLeftWidgets: [{
"id": "launcherButton",
"enabled": true
}, {
"id": "workspaceSwitcher",
"enabled": true
}, {
"id": "focusedWindow",
"enabled": true
}]
property var defaultCenterWidgets: [{
"id": "music",
"enabled": true
}, {
"id": "clock",
"enabled": true
}, {
"id": "weather",
"enabled": true
}]
property var defaultRightWidgets: [{
"id": "systemTray",
"enabled": true
}, {
"id": "clipboard",
"enabled": true
}, {
"id": "systemResources",
"enabled": true
}, {
"id": "notificationButton",
"enabled": true
}, {
"id": "battery",
"enabled": true
}, {
"id": "controlCenterButton",
"enabled": true
}]
function addWidgetToSection(widgetId, targetSection) {
var leftWidgets = Prefs.topBarLeftWidgets.slice()
var centerWidgets = Prefs.topBarCenterWidgets.slice()
var rightWidgets = Prefs.topBarRightWidgets.slice()
// Create widget object with enabled state
var widgetObj = {id: widgetId, enabled: true}
var widgetObj = {
"id": widgetId,
"enabled": true
};
if (widgetId === "spacer")
widgetObj.size = 20;
var widgets = [];
if (targetSection === "left") {
leftWidgets.push(widgetObj)
Prefs.setTopBarLeftWidgets(leftWidgets)
widgets = Prefs.topBarLeftWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarLeftWidgets(widgets);
} else if (targetSection === "center") {
centerWidgets.push(widgetObj)
Prefs.setTopBarCenterWidgets(centerWidgets)
widgets = Prefs.topBarCenterWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarCenterWidgets(widgets);
} else if (targetSection === "right") {
rightWidgets.push(widgetObj)
Prefs.setTopBarRightWidgets(rightWidgets)
widgets = Prefs.topBarRightWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarRightWidgets(widgets);
}
}
function removeLastWidgetFromSection(sectionId) {
var leftWidgets = Prefs.topBarLeftWidgets.slice()
var centerWidgets = Prefs.topBarCenterWidgets.slice()
var rightWidgets = Prefs.topBarRightWidgets.slice()
if (sectionId === "left" && leftWidgets.length > 0) {
leftWidgets.pop()
Prefs.setTopBarLeftWidgets(leftWidgets)
} else if (sectionId === "center" && centerWidgets.length > 0) {
centerWidgets.pop()
Prefs.setTopBarCenterWidgets(centerWidgets)
} else if (sectionId === "right" && rightWidgets.length > 0) {
rightWidgets.pop()
Prefs.setTopBarRightWidgets(rightWidgets)
function removeWidgetFromSection(sectionId, itemId) {
var widgets = [];
if (sectionId === "left") {
widgets = Prefs.topBarLeftWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarLeftWidgets(widgets);
} else if (sectionId === "center") {
widgets = Prefs.topBarCenterWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarCenterWidgets(widgets);
} else if (sectionId === "right") {
widgets = Prefs.topBarRightWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarRightWidgets(widgets);
}
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
// Update the specific widget instance's enabled state in the section
var widgets = []
if (sectionId === "left") {
widgets = Prefs.topBarLeftWidgets.slice()
} else if (sectionId === "center") {
widgets = Prefs.topBarCenterWidgets.slice()
} else if (sectionId === "right") {
widgets = Prefs.topBarRightWidgets.slice()
}
// Find and update the specific widget instance
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
for (var i = 0; i < widgets.length; i++) {
// Handle both old string format and new object format for backward compatibility
var widget = widgets[i]
var widgetId = typeof widget === "string" ? widget : widget.id
// Update the enabled state for this specific instance
var widget = widgets[i];
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId === itemId) {
if (typeof widget === "string") {
// Convert old string format to object format
widgets[i] = {id: widget, enabled: enabled}
} else {
widgets[i] = {id: widget.id, enabled: enabled}
}
break
widgets[i] = typeof widget === "string" ? {
"id": widget,
"enabled": enabled
} : {
"id": widget.id,
"enabled": enabled,
"size": widget.size
};
break;
}
}
// Save the updated widgets array
if (sectionId === "left") {
Prefs.setTopBarLeftWidgets(widgets)
} else if (sectionId === "center") {
Prefs.setTopBarCenterWidgets(widgets)
} else if (sectionId === "right") {
Prefs.setTopBarRightWidgets(widgets)
}
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
function handleItemOrderChanged(sectionId, newOrder) {
if (sectionId === "left") {
Prefs.setTopBarLeftWidgets(newOrder)
} else if (sectionId === "center") {
Prefs.setTopBarCenterWidgets(newOrder)
} else if (sectionId === "right") {
Prefs.setTopBarRightWidgets(newOrder)
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(newOrder);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(newOrder);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(newOrder);
}
function handleSpacerSizeChanged(sectionId, itemId, newSize) {
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId === itemId && widgetId === "spacer") {
widgets[i] = typeof widget === "string" ? {
"id": widget,
"enabled": true,
"size": newSize
} : {
"id": widget.id,
"enabled": widget.enabled,
"size": newSize
};
break;
}
}
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
function getItemsForSection(sectionId) {
var widgets = []
var widgetData = []
if (sectionId === "left") {
widgetData = Prefs.topBarLeftWidgets || []
} else if (sectionId === "center") {
widgetData = Prefs.topBarCenterWidgets || []
} else if (sectionId === "right") {
widgetData = Prefs.topBarRightWidgets || []
}
widgetData.forEach(widget => {
// Handle both old string format and new object format for backward compatibility
var widgetId = typeof widget === "string" ? widget : widget.id
var widgetEnabled = typeof widget === "string" ? true : widget.enabled
var widgetDef = baseWidgetDefinitions.find(w => w.id === widgetId)
var widgets = [];
var widgetData = [];
if (sectionId === "left")
widgetData = Prefs.topBarLeftWidgets || [];
else if (sectionId === "center")
widgetData = Prefs.topBarCenterWidgets || [];
else if (sectionId === "right")
widgetData = Prefs.topBarRightWidgets || [];
widgetData.forEach((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
var widgetEnabled = typeof widget === "string" ? true : widget.enabled;
var widgetSize = typeof widget === "string" ? undefined : widget.size;
var widgetDef = baseWidgetDefinitions.find((w) => {
return w.id === widgetId;
});
if (widgetDef) {
var item = Object.assign({}, widgetDef)
// Use the per-instance enabled state
item.enabled = widgetEnabled
widgets.push(item)
var item = Object.assign({
}, widgetDef);
item.enabled = widgetEnabled;
if (widgetSize !== undefined)
item.size = widgetSize;
widgets.push(item);
}
})
return widgets
});
return widgets;
}
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Component.onCompleted: {
if (!Prefs.topBarLeftWidgets || Prefs.topBarLeftWidgets.length === 0)
Prefs.setTopBarLeftWidgets(defaultLeftWidgets);
if (!Prefs.topBarCenterWidgets || Prefs.topBarCenterWidgets.length === 0)
Prefs.setTopBarCenterWidgets(defaultCenterWidgets);
if (!Prefs.topBarRightWidgets || Prefs.topBarRightWidgets.length === 0)
Prefs.setTopBarRightWidgets(defaultRightWidgets);
// Ensure existing spacers have default sizes
["left", "center", "right"].forEach((sectionId) => {
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
var updated = false;
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
if (typeof widget === "object" && widget.id === "spacer" && !widget.size) {
widgets[i] = Object.assign({
}, widget, {
"size": 20
});
updated = true;
}
}
if (updated) {
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
});
}
Column {
@@ -300,18 +367,18 @@ ScrollView {
anchors.verticalCenter: parent.verticalCenter
border.width: 1
border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "refresh"
size: 14
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reset"
font.pixelSize: Theme.fontSizeSmall
@@ -319,35 +386,40 @@ ScrollView {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: resetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Reset all sections to defaults (with per-instance enabled states)
Prefs.setTopBarLeftWidgets(defaultLeftWidgets)
Prefs.setTopBarCenterWidgets(defaultCenterWidgets)
Prefs.setTopBarRightWidgets(defaultRightWidgets)
Prefs.setTopBarLeftWidgets(defaultLeftWidgets);
Prefs.setTopBarCenterWidgets(defaultCenterWidgets);
Prefs.setTopBarRightWidgets(defaultRightWidgets);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
@@ -358,18 +430,20 @@ ScrollView {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: true
opacity: 1.0
opacity: 1
z: 1
StyledText {
id: messageText
anchors.centerIn: parent
text: "Drag widgets to reorder within sections. Use + to add widgets and - to remove the last widget from each section."
text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width - Theme.spacingM * 2
wrapMode: Text.WordWrap
}
}
// Widget sections
@@ -385,23 +459,22 @@ ScrollView {
sectionId: "left"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("left")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled)
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("left", newOrder)
widgetsTab.handleItemOrderChanged("left", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveLastWidget: (sectionId) => {
widgetsTab.removeLastWidgetFromSection(sectionId)
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
@@ -413,23 +486,22 @@ ScrollView {
sectionId: "center"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("center")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled)
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("center", newOrder)
widgetsTab.handleItemOrderChanged("center", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveLastWidget: (sectionId) => {
widgetsTab.removeLastWidgetFromSection(sectionId)
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
@@ -441,25 +513,25 @@ ScrollView {
sectionId: "right"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("right")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled)
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("right", newOrder)
widgetsTab.handleItemOrderChanged("right", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveLastWidget: (sectionId) => {
widgetsTab.removeLastWidgetFromSection(sectionId)
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
}
// Workspace Section
@@ -496,6 +568,7 @@ ScrollView {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
@@ -517,8 +590,11 @@ ScrollView {
return Prefs.setShowWorkspacePadding(checked);
}
}
}
}
}
// Tooltip for reset button (positioned above the button)
@@ -534,30 +610,34 @@ ScrollView {
y: column.y + 48 // Position above the reset button in the header
x: parent.width - width - Theme.spacingM
z: 100
StyledText {
id: tooltipText
anchors.centerIn: parent
text: "Reset widget layout to defaults"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Widget selection popup
DankWidgetSelectionPopup {
id: widgetSelectionPopup
anchors.centerIn: parent
anchors.centerIn: parent
onWidgetSelected: (widgetId, targetSection) => {
widgetsTab.addWidgetToSection(widgetId, targetSection)
widgetsTab.addWidgetToSection(widgetId, targetSection);
}
}
}
}

View File

@@ -9,10 +9,10 @@ import qs.Widgets
PanelWindow {
id: root
property var modelData
screen: modelData
visible: ToastService.toastVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1

View File

@@ -25,14 +25,7 @@ Item {
repeat: true
onTriggered: {
// Generate fake audio levels when cava is unavailable
CavaService.values = [
Math.random() * 40 + 10,
Math.random() * 60 + 20,
Math.random() * 50 + 15,
Math.random() * 35 + 20,
Math.random() * 45 + 15,
Math.random() * 55 + 25
];
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
}
}

View File

@@ -5,11 +5,10 @@ import qs.Common
Rectangle {
id: root
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
signal menuRequested(var menu, var item, real x, real y)
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ?
SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
width: calculatedWidth
height: 30
radius: Theme.cornerRadius

View File

@@ -30,7 +30,6 @@ PanelWindow {
ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions");
Prefs.forceTopBarLayoutRefresh.connect(function() {
console.log("TopBar: Forcing layout refresh");
Qt.callLater(() => {
leftSection.visible = false;
centerSection.visible = false;
@@ -39,14 +38,12 @@ PanelWindow {
leftSection.visible = true;
centerSection.visible = true;
rightSection.visible = true;
console.log("TopBar: Layout refresh completed");
});
});
});
}
Connections {
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency;
}
@@ -180,6 +177,10 @@ PanelWindow {
return true;
case "controlCenterButton":
return true;
case "spacer":
return true;
case "separator":
return true;
default:
return false;
}
@@ -241,10 +242,13 @@ PanelWindow {
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetEnabled(model.enabled) && topBarContent.getWidgetVisible(model.widgetId)
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
}
}
@@ -263,26 +267,14 @@ PanelWindow {
centerWidgets = [];
totalWidgets = 0;
totalWidth = 0;
let allItemsReady = true;
for (let i = 0; i < centerRepeater.count; i++) {
let item = centerRepeater.itemAt(i);
if (item && item.active && item.item) {
if (item.item.width <= 0) {
allItemsReady = false;
break;
}
centerWidgets.push(item.item);
totalWidgets++;
totalWidth += item.item.width;
}
}
if (!allItemsReady) {
Qt.callLater(updateLayout);
return;
}
if (totalWidgets > 1)
totalWidth += spacing * (totalWidgets - 1);
@@ -341,7 +333,6 @@ PanelWindow {
width: parent.width
anchors.centerIn: parent
Component.onCompleted: {
console.log("Center widgets model count:", Prefs.topBarCenterWidgetsModel.count);
Qt.callLater(() => {
Qt.callLater(updateLayout);
});
@@ -354,13 +345,21 @@ PanelWindow {
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetEnabled(model.enabled) && topBarContent.getWidgetVisible(model.widgetId)
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
onLoaded: {
if (item) {
item.onWidthChanged.connect(centerSection.updateLayout);
if (model.widgetId === "spacer")
item.spacerSize = Qt.binding(() => {
return model.size || 20;
});
Qt.callLater(centerSection.updateLayout);
}
}
@@ -388,19 +387,19 @@ PanelWindow {
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
console.log("Right widgets model count:", Prefs.topBarRightWidgetsModel.count);
}
Repeater {
model: Prefs.topBarRightWidgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetEnabled(model.enabled) && topBarContent.getWidgetVisible(model.widgetId)
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
}
}
@@ -601,8 +600,26 @@ PanelWindow {
id: spacerComponent
Item {
width: 20
width: parent.spacerSize || 20
height: 30
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
border.width: 1
radius: 2
visible: false
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.visible = true
onExited: parent.visible = false
}
}
}
}