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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
QtObject {
|
||||
required property Singleton service
|
||||
|
||||
Component.onCompleted: service.refCount++
|
||||
Component.onDestruction: service.refCount--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import qs.Widgets
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
SysMonitorService.addRef();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SysMonitorService.removeRef();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ StyledRect {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: root.radius
|
||||
onClicked: {
|
||||
console.log("StateLayer clicked for button:", root.iconName);
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
41
shell.qml
41
shell.qml
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user