1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -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

@@ -1,25 +1,24 @@
pragma Singleton
import Quickshell
import qs.Common
pragma Singleton
Singleton {
id: root
// Clear all image cache
function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)])
Paths.mkdir(Paths.imagecache)
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
Paths.mkdir(Paths.imagecache);
}
// Clear cache older than specified minutes
function clearOldCache(ageInMinutes) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
}
// Clear cache for specific size
function clearCacheForSize(size) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
}
// Get cache size in MB
@@ -36,6 +35,7 @@ Singleton {
}
}
}
`, root)
`, root);
}
}
}

View File

@@ -500,7 +500,13 @@ Singleton {
// Handle both old string format and new object format
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id;
var enabled = typeof order[i] === "string" ? true : order[i].enabled;
listModel.append({"widgetId": widgetId, "enabled": enabled});
var size = typeof order[i] === "string" ? undefined : order[i].size;
var item = {"widgetId": widgetId, "enabled": enabled};
if (size !== undefined) {
item.size = size;
}
listModel.append(item);
}
}

View File

@@ -1,9 +1,9 @@
import Quickshell
import QtQuick
import Quickshell
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
}

View File

@@ -71,20 +71,16 @@ DankModal {
}
function generateThumbnails() {
if (!imagemagickAvailable) return;
if (!imagemagickAvailable)
return ;
for (let i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry;
const entryType = getEntryType(entry);
if (entryType === "image") {
const entryId = entry.split('\t')[0];
const thumbnailPath = `${thumbnailCacheDir}/${entryId}.png`;
thumbnailGenProcess.command = [
"sh", "-c",
`mkdir -p "${thumbnailCacheDir}" && cliphist decode ${entryId} | magick - -resize '128x128>' "${thumbnailPath}"`
];
thumbnailGenProcess.command = ["sh", "-c", `mkdir -p "${thumbnailCacheDir}" && cliphist decode ${entryId} | magick - -resize '128x128>' "${thumbnailPath}"`];
thumbnailGenProcess.running = true;
}
}
@@ -95,7 +91,6 @@ DankModal {
return `${thumbnailCacheDir}/${entryId}.png`;
}
function refreshClipboard() {
clipboardProcess.running = true;
}
@@ -157,7 +152,7 @@ DankModal {
cornerRadius: Theme.cornerRadiusLarge
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
enableShadow: true
onBackgroundClicked: {
hide();
}
@@ -292,8 +287,8 @@ DankModal {
for (const line of lines) {
if (line.trim().length > 0)
clipboardModel.append({
"entry": line
});
"entry": line
});
}
updateFilteredModel();
@@ -360,6 +355,7 @@ DankModal {
checkImageMagickProcess.running = true;
}
}
}
Process {
@@ -367,12 +363,11 @@ DankModal {
command: ["which", "magick"]
running: false
onExited: (exitCode) => {
imagemagickAvailable = (exitCode === 0);
if (!imagemagickAvailable) {
if (!imagemagickAvailable)
console.warn("ClipboardHistoryModal: ImageMagick not available, thumbnails disabled");
}
}
}
@@ -380,15 +375,13 @@ DankModal {
id: thumbnailGenProcess
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
if (exitCode !== 0)
console.warn("ClipboardHistoryModal: Thumbnail generation failed with exit code:", exitCode);
}
}
}
IpcHandler {
function open() {
console.log("ClipboardHistoryModal: IPC open() called");
@@ -440,6 +433,7 @@ DankModal {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
@@ -464,7 +458,9 @@ DankModal {
hoverColor: Theme.errorHover
onClicked: hide()
}
}
}
DankTextField {
@@ -490,6 +486,7 @@ DankModal {
target: clipboardHistoryModal
}
}
Rectangle {
@@ -504,30 +501,24 @@ DankModal {
ListView {
id: clipboardListView
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
model: filteredClipboardModel
spacing: Theme.spacingXS
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff }
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) * clipboardListView.wheelBaseStep;
if (ev.inverted) dy = -dy;
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * clipboardListView.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, clipboardListView.contentHeight - clipboardListView.height);
clipboardListView.contentY = Math.max(0, Math.min(maxY,
clipboardListView.contentY - dy * clipboardListView.wheelMultiplier));
clipboardListView.contentY = Math.max(0, Math.min(maxY, clipboardListView.contentY - dy * clipboardListView.wheelMultiplier));
ev.accepted = true;
}
}
@@ -540,212 +531,228 @@ DankModal {
visible: filteredClipboardModel.count === 0
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry)
property int entryIndex: index + 1
property string entryData: model.entry
property alias thumbnailImageSource: thumbnailImageSource
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry)
property int entryIndex: index + 1
property string entryData: model.entry
property alias thumbnailImageSource: thumbnailImageSource
width: clipboardListView.width
height: Math.max(entryType === "image" ? 72 : 60, contentText.contentHeight + Theme.spacingL)
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
border.color: Theme.outlineStrong
border.width: 1
width: clipboardListView.width
height: Math.max(entryType === "image" ? 72 : 60, contentText.contentHeight + Theme.spacingL)
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
border.color: Theme.outlineStrong
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingS // Reduced right margin
spacing: Theme.spacingL
// Index number
Rectangle {
width: 24
height: 24
radius: 12
color: Theme.primarySelected
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: entryIndex.toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
}
}
// Content thumbnail/icon and text
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingS // Reduced right margin
spacing: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68 // Account for index (24) + spacing (16) + delete button (32) - small margin
spacing: Theme.spacingM
// Index number
Rectangle {
width: 24
height: 24
radius: 12
color: Theme.primarySelected
// Thumbnail or icon container
Item {
width: entryType === "image" ? 48 : Theme.iconSize
height: entryType === "image" ? 48 : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
// Image thumbnail
CachingImage {
id: thumbnailImageSource
anchors.fill: parent
source: entryType === "image" && imagemagickAvailable ? "file://" + getThumbnailPath(model.entry) : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
visible: false
asynchronous: true
// Handle loading errors gracefully and retry once
onStatusChanged: {
if (status === Image.Error && source !== "") {
// Clear source to prevent repeated error attempts
const originalSource = source;
source = "";
// Retry once after 2 seconds to allow thumbnail generation
retryTimer.originalSource = originalSource;
retryTimer.start();
}
}
Timer {
id: retryTimer
property string originalSource: ""
interval: 2000
repeat: false
onTriggered: {
if (originalSource !== "" && thumbnailImageSource.source === "")
thumbnailImageSource.source = originalSource;
}
}
}
MultiEffect {
anchors.fill: parent
source: thumbnailImageSource
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image" && imagemagickAvailable && thumbnailImageSource.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: 48
height: 48
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
// Fallback icon
DankIcon {
visible: !(entryType === "image" && imagemagickAvailable && thumbnailImageSource.status === Image.Ready)
name: {
if (entryType === "image")
return "image";
if (entryType === "long_text")
return "subject";
return "content_copy";
}
size: Theme.iconSize
color: Theme.primary
anchors.centerIn: parent
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
anchors.centerIn: parent
text: entryIndex.toString()
text: {
switch (entryType) {
case "image":
return "Image • " + entryPreview;
case "long_text":
return "Long Text";
default:
return "Text";
}
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
}
// Content thumbnail/icon and text
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68 // Account for index (24) + spacing (16) + delete button (32) - small margin
spacing: Theme.spacingM
// Thumbnail or icon container
Item {
width: entryType === "image" ? 48 : Theme.iconSize
height: entryType === "image" ? 48 : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
// Image thumbnail
CachingImage {
id: thumbnailImageSource
anchors.fill: parent
source: entryType === "image" && imagemagickAvailable ? "file://" + getThumbnailPath(model.entry) : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
visible: false
asynchronous: true
// Handle loading errors gracefully and retry once
onStatusChanged: {
if (status === Image.Error && source !== "") {
// Clear source to prevent repeated error attempts
const originalSource = source;
source = "";
// Retry once after 2 seconds to allow thumbnail generation
retryTimer.originalSource = originalSource;
retryTimer.start();
}
}
Timer {
id: retryTimer
interval: 2000
repeat: false
property string originalSource: ""
onTriggered: {
if (originalSource !== "" && thumbnailImageSource.source === "") {
thumbnailImageSource.source = originalSource;
}
}
}
}
MultiEffect {
anchors.fill: parent
source: thumbnailImageSource
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image" && imagemagickAvailable && thumbnailImageSource.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: 48
height: 48
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
// Fallback icon
DankIcon {
visible: !(entryType === "image" && imagemagickAvailable && thumbnailImageSource.status === Image.Ready)
name: {
if (entryType === "image")
return "image";
if (entryType === "long_text")
return "subject";
return "content_copy";
}
size: Theme.iconSize
color: Theme.primary
anchors.centerIn: parent
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return "Image • " + entryPreview;
case "long_text":
return "Long Text";
default:
return "Text";
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
id: contentText
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
StyledText {
id: contentText
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
}
// Delete button
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
console.log("Delete clicked for entry:", model.entry);
deleteEntry(model.entry);
}
}
}
// Main click area - explicitly excludes delete button area
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
// Delete button
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
console.log("Delete clicked for entry:", model.entry);
deleteEntry(model.entry);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
// Main click area - explicitly excludes delete button area
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}

View File

@@ -48,79 +48,73 @@ DankModal {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Row {
width: parent.width
// Header
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog();
}
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog();
// Network Details
Flickable {
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * parent.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, parent.contentHeight - parent.height);
parent.contentY = Math.max(0, Math.min(maxY, parent.contentY - dy * parent.wheelMultiplier));
ev.accepted = true;
}
}
}
}
// Network Details
Flickable {
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff }
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) * parent.wheelBaseStep;
if (ev.inverted) dy = -dy;
const maxY = Math.max(0, parent.contentHeight - parent.height);
parent.contentY = Math.max(0, Math.min(maxY,
parent.contentY - dy * parent.wheelMultiplier));
ev.accepted = true;
}
}
Rectangle {
Rectangle {
id: detailsRect
width: parent.width
@@ -142,48 +136,58 @@ DankModal {
lineHeight: 1.5
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
}
// Close Button
Item {
width: parent.width
height: 40
// Close Button
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: closeText
StyledText {
id: closeText
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog();
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -193,7 +197,7 @@ DankModal {
}
}
}
}
}

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
DankModal {
id: processListModal
@@ -25,9 +25,9 @@ DankModal {
function hide() {
processListModal.visible = false;
// Close any open context menus
if (processContextMenu.visible) {
if (processContextMenu.visible)
processContextMenu.close();
}
}
function toggle() {
@@ -44,13 +44,40 @@ DankModal {
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusXLarge
enableShadow: true
onBackgroundClicked: hide()
Ref {
service: SysMonitorService
}
onBackgroundClicked: hide()
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {
}
}
Component {
id: systemTabComponent
SystemTab {
}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
@@ -102,6 +129,7 @@ DankModal {
onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
@@ -234,7 +262,9 @@ DankModal {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
@@ -252,7 +282,9 @@ DankModal {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
@@ -270,33 +302,17 @@ DankModal {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {}
}
Component {
id: systemTabComponent
SystemTab {}
}
ProcessContextMenu {
id: processContextMenu
}
}

View File

@@ -94,6 +94,7 @@ DankModal {
content: Component {
Item {
id: spotlightKeyHandler
anchors.fill: parent
focus: true
// Handle keyboard shortcuts
@@ -139,6 +140,7 @@ DankModal {
CategorySelector {
id: categorySelector
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
categories: appLauncher.categories
@@ -148,6 +150,7 @@ DankModal {
return appLauncher.setCategory(category);
}
}
}
// Search field with view toggle buttons
@@ -184,11 +187,10 @@ DankModal {
hide();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) {
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0));
}
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false;

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
}
}
}
}

View File

@@ -10,7 +10,7 @@ Singleton {
id: root
property int refCount: 0
property int updateInterval: 30000
property int updateInterval: refCount > 0 ? 2000 : 30000
property int maxProcesses: 100
property bool isUpdating: false

View File

@@ -31,7 +31,6 @@ StyledRect {
stateColor: Theme.primary
cornerRadius: root.radius
onClicked: {
console.log("StateLayer clicked for button:", root.iconName);
root.clicked();
}
}

View File

@@ -5,6 +5,8 @@ import qs.Common
import qs.Widgets
Rectangle {
// Force recreate popup when component becomes visible
id: root
property string text: ""
@@ -38,7 +40,6 @@ Rectangle {
if (!visible && popup && popup.visible)
popup.close();
else if (visible)
// Force recreate popup when component becomes visible
forceRecreateTimer.start();
}

View File

@@ -15,7 +15,8 @@ Column {
signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
signal itemOrderChanged(var newOrder)
signal addWidget(string sectionId)
signal removeLastWidget(string sectionId)
signal removeWidget(string sectionId, string itemId)
signal spacerSizeChanged(string sectionId, string itemId, int newSize)
width: parent.width
spacing: Theme.spacingM
@@ -44,30 +45,32 @@ Column {
width: parent.width - 60
height: 1
}
}
// Widget Items
Column {
id: itemsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.items
delegate: Item {
id: delegateItem
property bool held: dragArea.pressed
property real originalY: y
width: itemsList.width
height: 70
property int visualIndex: index
property bool held: dragArea.pressed
property string itemId: modelData.id
z: held ? 2 : 1
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
radius: Theme.cornerRadius
@@ -75,39 +78,29 @@ Column {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
// Drag handle
Rectangle {
width: 40
height: parent.height
color: "transparent"
DankIcon {
name: "drag_indicator"
size: Theme.iconSize - 4
color: Theme.outline
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.leftMargin: Theme.spacingM + 8
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "drag_indicator"
size: Theme.iconSize - 4
color: Theme.outline
anchors.centerIn: parent
opacity: 0.8
}
opacity: 0.8
}
// Widget icon
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: modelData.enabled ? Theme.primary : Theme.outline
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM + 40 + Theme.spacingM
anchors.leftMargin: Theme.spacingM * 2 + 40
anchors.verticalCenter: parent.verticalCenter
}
// Widget info
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM + 40 + Theme.spacingM + Theme.iconSize + Theme.spacingM
anchors.right: toggle.left
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
@@ -129,135 +122,174 @@ Column {
width: parent.width
wrapMode: Text.WordWrap
}
}
// Toggle - positioned at right edge
DankToggle {
id: toggle
Row {
id: actionButtons
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 48
height: 24
hideText: true
checked: modelData.enabled
onToggled: (checked) => {
root.itemEnabledChanged(root.sectionId, modelData.id, checked)
spacing: Theme.spacingXS
DankActionButton {
visible: modelData.id !== "spacer"
buttonSize: 32
iconName: modelData.enabled ? "visibility" : "visibility_off"
iconSize: 18
iconColor: modelData.enabled ? Theme.primary : Theme.outline
onClicked: {
root.itemEnabledChanged(root.sectionId, modelData.id, !modelData.enabled);
}
}
Row {
visible: modelData.id === "spacer"
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 24
iconName: "remove"
iconSize: 14
iconColor: Theme.outline
onClicked: {
var currentSize = modelData.size || 20;
var newSize = Math.max(5, currentSize - 5);
root.spacerSizeChanged(root.sectionId, modelData.id, newSize);
}
}
StyledText {
text: (modelData.size || 20).toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
buttonSize: 24
iconName: "add"
iconSize: 14
iconColor: Theme.outline
onClicked: {
var currentSize = modelData.size || 20;
var newSize = Math.min(5000, currentSize + 5);
root.spacerSizeChanged(root.sectionId, modelData.id, newSize);
}
}
}
DankActionButton {
buttonSize: 32
iconName: "close"
iconSize: 18
iconColor: Theme.error
onClicked: {
root.removeWidget(root.sectionId, modelData.id);
}
}
}
// Drag functionality
MouseArea {
id: dragArea
anchors.fill: parent
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 60
hoverEnabled: true
property bool validDragStart: false
drag.target: held && validDragStart ? delegateItem : undefined
cursorShape: Qt.SizeVerCursor
drag.target: held ? delegateItem : undefined
drag.axis: Drag.YAxis
drag.minimumY: -delegateItem.height
drag.maximumY: itemsList.height
onPressed: (mouse) => {
// Only allow dragging from the drag handle area (first 60px)
if (mouse.x <= 60) {
validDragStart = true
delegateItem.z = 2
} else {
validDragStart = false
mouse.accepted = false
}
onPressed: {
delegateItem.z = 2;
delegateItem.originalY = delegateItem.y;
}
onReleased: {
delegateItem.z = 1
if (drag.active && validDragStart) {
// Calculate new index based on position
var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing))
newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1))
delegateItem.z = 1;
if (drag.active) {
var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing));
newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1));
if (newIndex !== index) {
var newItems = root.items.slice()
var draggedItem = newItems.splice(index, 1)[0]
newItems.splice(newIndex, 0, draggedItem)
root.itemOrderChanged(newItems.map(item => ({id: item.id, enabled: item.enabled})))
var newItems = root.items.slice();
var draggedItem = newItems.splice(index, 1)[0];
newItems.splice(newIndex, 0, draggedItem);
root.itemOrderChanged(newItems.map((item) => {
return ({
"id": item.id,
"enabled": item.enabled,
"size": item.size
});
}));
}
}
// Reset position
delegateItem.x = 0
delegateItem.y = 0
validDragStart = false
delegateItem.x = 0;
delegateItem.y = delegateItem.originalY;
}
}
// Animations for drag
Behavior on y {
enabled: !dragArea.held && !dragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Add/Remove Controls
// Add Widget Control
Rectangle {
width: parent.width * 0.5
width: 200
height: 40
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
Row {
StyledText {
text: "Add Widget"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
anchors.centerIn: parent
spacing: Theme.spacingL
}
StyledText {
text: "Add or remove widgets"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
id: addButtonArea
Row {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
// Add button
DankActionButton {
iconName: "add"
iconSize: Theme.iconSize - 4
iconColor: Theme.primary
hoverColor: Theme.primaryContainer
onClicked: {
root.addWidget(root.sectionId);
}
}
// Remove button
DankActionButton {
iconName: "remove"
iconSize: Theme.iconSize - 4
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
enabled: root.items.length > 0
opacity: root.items.length > 0 ? 1.0 : 0.5
onClicked: {
if (root.items.length > 0) {
root.removeLastWidget(root.sectionId);
}
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(root.sectionId);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

@@ -44,30 +44,33 @@ Column {
width: parent.width - 60
height: 1
}
}
// Widget Items
Column {
id: itemsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.items
delegate: Item {
id: delegateItem
width: itemsList.width
height: 70
property int visualIndex: index
property bool held: dragArea.pressed
property string itemId: modelData.id
width: itemsList.width
height: 70
z: held ? 2 : 1
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
radius: Theme.cornerRadius
@@ -86,7 +89,7 @@ Column {
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "drag_indicator"
size: Theme.iconSize - 4
@@ -94,6 +97,7 @@ Column {
anchors.centerIn: parent
opacity: 0.8
}
}
// Widget icon
@@ -127,6 +131,7 @@ Column {
width: parent.width
wrapMode: Text.WordWrap
}
}
// Spacer to push toggle to right
@@ -143,70 +148,73 @@ Column {
hideText: true
checked: modelData.enabled
onToggled: (checked) => {
root.itemEnabledChanged(modelData.id, checked)
root.itemEnabledChanged(modelData.id, checked);
}
}
}
// Drag functionality
MouseArea {
id: dragArea
property bool validDragStart: false
anchors.fill: parent
hoverEnabled: true
property bool validDragStart: false
drag.target: held && validDragStart ? delegateItem : undefined
drag.axis: Drag.YAxis
drag.minimumY: -delegateItem.height
drag.maximumY: itemsList.height
onPressed: (mouse) => {
// Only allow dragging from the drag handle area (first 60px)
if (mouse.x <= 60) {
validDragStart = true
delegateItem.z = 2
validDragStart = true;
delegateItem.z = 2;
} else {
validDragStart = false
mouse.accepted = false
validDragStart = false;
mouse.accepted = false;
}
}
onReleased: {
delegateItem.z = 1
delegateItem.z = 1;
if (drag.active && validDragStart) {
// Calculate new index based on position
var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing))
newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1))
var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing));
newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1));
if (newIndex !== index) {
var newItems = root.items.slice()
var draggedItem = newItems.splice(index, 1)[0]
newItems.splice(newIndex, 0, draggedItem)
root.itemOrderChanged(newItems.map(item => item.id))
var newItems = root.items.slice();
var draggedItem = newItems.splice(index, 1)[0];
newItems.splice(newIndex, 0, draggedItem);
root.itemOrderChanged(newItems.map((item) => {
return item.id;
}));
}
}
// Reset position
delegateItem.x = 0
delegateItem.y = 0
validDragStart = false
delegateItem.x = 0;
delegateItem.y = 0;
validDragStart = false;
}
}
// Animations for drag
Behavior on y {
enabled: !dragArea.held && !dragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Add/Remove Controls
@@ -252,14 +260,18 @@ Column {
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
enabled: root.items.length > 0
opacity: root.items.length > 0 ? 1.0 : 0.5
opacity: root.items.length > 0 ? 1 : 0.5
onClicked: {
if (root.items.length > 0) {
if (root.items.length > 0)
root.removeLastWidget(root.sectionId);
}
}
}
}
}
}
}

View File

@@ -12,30 +12,28 @@ Popup {
signal widgetSelected(string widgetId, string targetSection)
// Prevent multiple openings
function safeOpen() {
if (!isOpening && !visible) {
isOpening = true;
open();
}
}
width: 400
height: 450
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
// Prevent multiple openings
function safeOpen() {
if (!isOpening && !visible) {
isOpening = true
open()
}
}
onOpened: {
isOpening = false
isOpening = false;
}
onClosed: {
isOpening = false
isOpening = false;
// Clear references to prevent memory leaks
allWidgets = []
targetSection = ""
allWidgets = [];
targetSection = "";
}
background: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
border.color: Theme.primarySelected
@@ -61,6 +59,7 @@ Popup {
Column {
id: contentColumn
spacing: Theme.spacingM
anchors.fill: parent
anchors.margins: Theme.spacingL
@@ -85,15 +84,16 @@ Popup {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Select a widget to add to the " + root.targetSection.toLowerCase() + " section of the top bar. You can add multiple instances of the same widget if needed."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
}
StyledText {
text: "Select a widget to add to the " + root.targetSection.toLowerCase() + " section of the top bar. You can add multiple instances of the same widget if needed."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
}
// Widget List
ScrollView {
@@ -103,6 +103,7 @@ Popup {
ListView {
id: widgetList
spacing: Theme.spacingS
model: root.allWidgets
@@ -150,6 +151,7 @@ Popup {
width: parent.width
wrapMode: Text.WordWrap
}
}
// Add icon
@@ -159,17 +161,18 @@ Popup {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: widgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.widgetSelected(modelData.id, root.targetSection)
root.close()
root.widgetSelected(modelData.id, root.targetSection);
root.close();
}
}
@@ -178,10 +181,17 @@ Popup {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}

View File

@@ -10,13 +10,14 @@ Text {
color: Theme.surfaceText
font.pixelSize: Appearance.fontSize.normal
font.family: {
// Use system default
var requestedFont = isMonospace ? Prefs.monoFontFamily : Prefs.fontFamily;
var defaultFont = isMonospace ? Prefs.defaultMonoFontFamily : Prefs.defaultFontFamily;
// If user hasn't overridden the font and we're using the default
if (requestedFont === defaultFont) {
var availableFonts = Qt.fontFamilies();
if (!availableFonts.includes(requestedFont))
// Use system default
return isMonospace ? "Monospace" : "DejaVu Sans";
}

View File

@@ -4,30 +4,32 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Modals
import qs.Modules
import qs.Modules.TopBar
import qs.Modules.AppDrawer
import qs.Modules.CentcomCenter
import qs.Modules.ControlCenter
import qs.Modules.Settings
import qs.Modules.ProcessList
import qs.Modules.ControlCenter.Network
import qs.Modules.Lock
import qs.Modules.Notifications.Center
import qs.Modules.Notifications.Popup
import qs.Modals
import qs.Modules.ProcessList
import qs.Modules.Settings
import qs.Modules.TopBar
import qs.Services
ShellRoot {
id: root
WallpaperBackground {}
WallpaperBackground {
}
Lock {
id: lock
anchors.fill: parent
}
// Multi-monitor support using Variants
Variants {
model: Quickshell.screens
@@ -35,18 +37,17 @@ ShellRoot {
delegate: TopBar {
modelData: item
}
}
CentcomPopout {
id: centcomPopout
}
SystemTrayContextMenu {
id: systemTrayContextMenu
}
NotificationCenterPopout {
id: notificationCenter
}
@@ -57,10 +58,12 @@ ShellRoot {
delegate: NotificationPopupManager {
modelData: item
}
}
ControlCenterPopout {
id: controlCenterPopout
onPowerActionRequested: (action, title, message) => {
powerConfirmModal.powerConfirmAction = action;
powerConfirmModal.powerConfirmTitle = title;
@@ -115,33 +118,36 @@ ShellRoot {
LazyLoader {
id: processListModalLoader
active: false
ProcessListModal {
id: processListModal
}
}
IpcHandler {
function open() {
processListModalLoader.active = true;
if (processListModalLoader.item) {
if (processListModalLoader.item)
processListModalLoader.item.show();
}
return "PROCESSLIST_OPEN_SUCCESS";
}
function close() {
if (processListModalLoader.item) {
if (processListModalLoader.item)
processListModalLoader.item.hide();
}
return "PROCESSLIST_CLOSE_SUCCESS";
}
function toggle() {
processListModalLoader.active = true;
if (processListModalLoader.item) {
if (processListModalLoader.item)
processListModalLoader.item.toggle();
}
return "PROCESSLIST_TOGGLE_SUCCESS";
}
@@ -154,6 +160,7 @@ ShellRoot {
delegate: Toast {
modelData: item
}
}
Variants {
@@ -162,5 +169,7 @@ ShellRoot {
delegate: VolumePopup {
modelData: item
}
}
}