1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

...

3 Commits

30 changed files with 1761 additions and 1372 deletions

View File

@@ -7,60 +7,8 @@ import Quickshell
Singleton {
id: root
readonly property Rounding rounding: Rounding {}
readonly property Spacing spacing: Spacing {}
readonly property FontSize fontSize: FontSize {}
readonly property Anim anim: Anim {}
component Rounding: QtObject {
readonly property int small: 8
readonly property int normal: 12
readonly property int large: 16
readonly property int extraLarge: 24
readonly property int full: 1000
}
component Spacing: QtObject {
readonly property int small: 4
readonly property int normal: 8
readonly property int large: 12
readonly property int extraLarge: 16
readonly property int huge: 24
}
component FontSize: QtObject {
readonly property int small: 12
readonly property int normal: 14
readonly property int large: 16
readonly property int extraLarge: 20
readonly property int huge: 24
}
component AnimCurves: QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: QtObject {
readonly property int quick: 150
readonly property int normal: 300
readonly property int slow: 500
readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200
}
component Anim: QtObject {
readonly property AnimCurves curves: AnimCurves {}
readonly property AnimDurations durations: AnimDurations {}
}
readonly property AppearanceRounding rounding: AppearanceRounding {}
readonly property AppearanceSpacing spacing: AppearanceSpacing {}
readonly property AppearanceFontSize fontSize: AppearanceFontSize {}
readonly property AppearanceAnim anim: AppearanceAnim {}
}

View File

@@ -0,0 +1,6 @@
import QtQuick
QtObject {
readonly property AppearanceAnimCurves curves: AppearanceAnimCurves {}
readonly property AppearanceAnimDurations durations: AppearanceAnimDurations {}
}

View File

@@ -0,0 +1,13 @@
import QtQuick
QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}

View File

@@ -0,0 +1,11 @@
import QtQuick
QtObject {
readonly property int quick: 150
readonly property int normal: 300
readonly property int slow: 500
readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200
}

View File

@@ -0,0 +1,9 @@
import QtQuick
QtObject {
readonly property int small: 12
readonly property int normal: 14
readonly property int large: 16
readonly property int extraLarge: 20
readonly property int huge: 24
}

View File

@@ -0,0 +1,9 @@
import QtQuick
QtObject {
readonly property int small: 8
readonly property int normal: 12
readonly property int large: 16
readonly property int extraLarge: 24
readonly property int full: 1000
}

View File

@@ -0,0 +1,9 @@
import QtQuick
QtObject {
readonly property int small: 4
readonly property int normal: 8
readonly property int large: 12
readonly property int extraLarge: 16
readonly property int huge: 24
}

View File

@@ -301,6 +301,7 @@ Singleton {
property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true
property bool mediaAdaptiveWidthEnabled: true
property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5

View File

@@ -140,6 +140,7 @@ var SPEC = {
workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true },
scrollTitleEnabled: { def: true },
mediaAdaptiveWidthEnabled: { def: true },
audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 },

View File

@@ -0,0 +1,54 @@
import QtQuick
import qs.Common
import qs.Widgets
Row {
id: checkboxRow
property alias checked: checkbox.checked
property alias label: labelText.text
property bool indeterminate: false
spacing: Theme.spacingS
height: 24
Rectangle {
id: checkbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checkboxRow.indeterminate ? Theme.surfaceVariant : (checked ? Theme.primary : "transparent")
border.color: checkboxRow.indeterminate ? Theme.outlineButton : (checked ? Theme.primary : Theme.outlineButton)
border.width: 2
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: checkboxRow.indeterminate ? "remove" : "check"
size: 12
color: checkboxRow.indeterminate ? Theme.surfaceVariantText : Theme.background
visible: parent.checked || checkboxRow.indeterminate
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (checkboxRow.indeterminate) {
checkboxRow.indeterminate = false;
checkbox.checked = true;
} else {
checkbox.checked = !checkbox.checked;
}
}
}
}
StyledText {
id: labelText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}

View File

@@ -0,0 +1,17 @@
import QtQuick
import qs.Common
Rectangle {
id: inputFieldRect
default property alias contentData: inputFieldRect.data
property bool hasFocus: false
property int fieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
width: parent.width
height: fieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: hasFocus ? Theme.primary : Theme.outlineStrong
border.width: hasFocus ? 2 : 1
}

View File

@@ -297,78 +297,6 @@ FloatingWindow {
}
}
component SectionHeader: StyledText {
property string title
text: title
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
topPadding: Theme.spacingM
bottomPadding: Theme.spacingXS
width: parent.width
horizontalAlignment: Text.AlignLeft
}
component CheckboxRow: Row {
property alias checked: checkbox.checked
property alias label: labelText.text
property bool indeterminate: false
spacing: Theme.spacingS
height: 24
Rectangle {
id: checkbox
property bool checked: false
width: 20
height: 20
radius: 4
color: parent.indeterminate ? Theme.surfaceVariant : (checked ? Theme.primary : "transparent")
border.color: parent.indeterminate ? Theme.outlineButton : (checked ? Theme.primary : Theme.outlineButton)
border.width: 2
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: parent.parent.indeterminate ? "remove" : "check"
size: 12
color: parent.parent.indeterminate ? Theme.surfaceVariantText : Theme.background
visible: parent.checked || parent.parent.indeterminate
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (parent.parent.indeterminate) {
parent.parent.indeterminate = false;
parent.checked = true;
} else {
parent.checked = !parent.checked;
}
}
}
}
StyledText {
id: labelText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
component InputField: Rectangle {
id: inputFieldRect
default property alias contentData: inputFieldRect.data
property bool hasFocus: false
width: parent.width
height: root.inputFieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: hasFocus ? Theme.primary : Theme.outlineStrong
border.width: hasFocus ? 2 : 1
}
FocusScope {
anchors.fill: parent
focus: true
@@ -447,7 +375,7 @@ FloatingWindow {
width: flickable.width - Theme.spacingM
spacing: Theme.spacingXS
InputField {
WindowRuleInputField {
hasFocus: nameInput.activeFocus
DankTextField {
id: nameInput
@@ -460,11 +388,11 @@ FloatingWindow {
}
}
SectionHeader {
WindowRuleSectionHeader {
title: I18n.tr("Match Criteria")
}
InputField {
WindowRuleInputField {
hasFocus: appIdInput.activeFocus
DankTextField {
id: appIdInput
@@ -481,7 +409,7 @@ FloatingWindow {
width: parent.width
spacing: Theme.spacingS
InputField {
WindowRuleInputField {
width: addTitleBtn.visible ? parent.width - addTitleBtn.width - Theme.spacingS : parent.width
hasFocus: titleInput.activeFocus
DankTextField {
@@ -514,7 +442,7 @@ FloatingWindow {
}
}
SectionHeader {
WindowRuleSectionHeader {
title: I18n.tr("Window Opening")
}
@@ -522,24 +450,24 @@ FloatingWindow {
width: parent.width
spacing: Theme.spacingL
CheckboxRow {
WindowRuleCheckboxRow {
id: floatingToggle
label: I18n.tr("Float")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: maximizedToggle
label: I18n.tr("Maximize")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: fullscreenToggle
label: I18n.tr("Fullscreen")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: maximizedToEdgesToggle
label: I18n.tr("Max to Edges")
visible: isNiri
}
CheckboxRow {
WindowRuleCheckboxRow {
id: openFocusedToggle
label: I18n.tr("Focus")
visible: isNiri
@@ -563,7 +491,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: outputInput.activeFocus
DankTextField {
@@ -590,7 +518,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: workspaceInput.activeFocus
DankTextField {
@@ -623,7 +551,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: columnWidthInput.activeFocus
DankTextField {
@@ -650,7 +578,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: windowHeightInput.activeFocus
DankTextField {
@@ -666,7 +594,7 @@ FloatingWindow {
}
}
SectionHeader {
WindowRuleSectionHeader {
title: I18n.tr("Dynamic Properties")
}
@@ -674,7 +602,7 @@ FloatingWindow {
width: parent.width
spacing: Theme.spacingM
CheckboxRow {
WindowRuleCheckboxRow {
id: opacityEnabled
label: I18n.tr("Opacity")
anchors.verticalCenter: parent.verticalCenter
@@ -696,19 +624,19 @@ FloatingWindow {
spacing: Theme.spacingL
visible: isNiri
CheckboxRow {
WindowRuleCheckboxRow {
id: vrrToggle
label: I18n.tr("VRR On-Demand")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: clipToGeometryToggle
label: I18n.tr("Clip to Geometry")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: tiledStateToggle
label: I18n.tr("Tiled State")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: drawBorderBgToggle
label: I18n.tr("Border with BG")
}
@@ -769,7 +697,7 @@ FloatingWindow {
spacing: Theme.spacingM
visible: isNiri
CheckboxRow {
WindowRuleCheckboxRow {
id: scrollFactorEnabled
label: I18n.tr("Scroll Factor")
anchors.verticalCenter: parent.verticalCenter
@@ -790,7 +718,7 @@ FloatingWindow {
width: parent.width
spacing: Theme.spacingM
CheckboxRow {
WindowRuleCheckboxRow {
id: cornerRadiusEnabled
label: I18n.tr("Corner Radius")
anchors.verticalCenter: parent.verticalCenter
@@ -807,7 +735,7 @@ FloatingWindow {
}
}
SectionHeader {
WindowRuleSectionHeader {
title: I18n.tr("Size Constraints")
}
@@ -827,7 +755,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: minWidthInput.activeFocus
DankTextField {
@@ -854,7 +782,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: maxWidthInput.activeFocus
DankTextField {
@@ -881,7 +809,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: minHeightInput.activeFocus
DankTextField {
@@ -908,7 +836,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: maxHeightInput.activeFocus
DankTextField {
@@ -924,7 +852,7 @@ FloatingWindow {
}
}
SectionHeader {
WindowRuleSectionHeader {
title: I18n.tr("Hyprland Options")
visible: isHyprland
}
@@ -934,43 +862,43 @@ FloatingWindow {
spacing: Theme.spacingL
visible: isHyprland
CheckboxRow {
WindowRuleCheckboxRow {
id: tileToggle
label: I18n.tr("Tile")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noFocusToggle
label: I18n.tr("No Focus")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noBorderToggle
label: I18n.tr("No Border")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noShadowToggle
label: I18n.tr("No Shadow")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noDimToggle
label: I18n.tr("No Dim")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noBlurToggle
label: I18n.tr("No Blur")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noAnimToggle
label: I18n.tr("No Anim")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: noRoundingToggle
label: I18n.tr("No Rounding")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: pinToggle
label: I18n.tr("Pin")
}
CheckboxRow {
WindowRuleCheckboxRow {
id: opaqueToggle
label: I18n.tr("Opaque")
}
@@ -993,7 +921,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: sizeInput.activeFocus
DankTextField {
@@ -1020,7 +948,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: moveInput.activeFocus
DankTextField {
@@ -1053,7 +981,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: monitorInput.activeFocus
DankTextField {
@@ -1080,7 +1008,7 @@ FloatingWindow {
horizontalAlignment: Text.AlignLeft
}
InputField {
WindowRuleInputField {
width: parent.width
hasFocus: hyprWorkspaceInput.activeFocus
DankTextField {

View File

@@ -0,0 +1,15 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledText {
property string title
text: title
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
topPadding: Theme.spacingM
bottomPadding: Theme.spacingXS
width: parent.width
horizontalAlignment: Text.AlignLeft
}

View File

@@ -969,6 +969,7 @@ Item {
axis: barWindow.axis
barSpacing: barConfig?.spacing ?? 4
barConfig: topBarContent.barConfig
widgetData: parent.widgetData
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
isAtBottom: barWindow.axis?.edge === "bottom"
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0

View File

@@ -19,7 +19,8 @@ BasePill {
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property bool compactMode: false
property var widgetData: null
readonly property int textWidth: {
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
readonly property int maxTextWidth: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
switch (size) {
case 0:
@@ -36,10 +37,7 @@ BasePill {
if (isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2;
}
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
return 0;
}
readonly property int currentContentHeight: {
if (!isVerticalOrientation) {
@@ -119,7 +117,28 @@ BasePill {
content: Component {
Item {
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
id: contentRoot
readonly property real measuredTextWidth: {
if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
return 0;
// Preserve the fixed-width text slot even if metadata is briefly empty.
if (!root.adaptiveWidthEnabled)
return root.maxTextWidth;
if (textContainer.displayText.length === 0)
return 0;
const rawWidth = mediaText.contentWidth;
if (!isFinite(rawWidth) || rawWidth <= 0)
return 0;
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
}
readonly property int horizontalContentWidth: {
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
}
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
opacity: root.playerAvailable ? 1 : 0
@@ -132,8 +151,9 @@ BasePill {
Behavior on implicitWidth {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
duration: Theme.mediumDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
@@ -269,7 +289,7 @@ BasePill {
}
anchors.verticalCenter: parent.verticalCenter
width: textWidth
width: contentRoot.measuredTextWidth
height: root.widgetThickness
visible: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
@@ -278,50 +298,95 @@ BasePill {
clip: true
color: "transparent"
StyledText {
id: mediaText
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
property real scrollOffset: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: needsScrolling ? -scrollOffset : 0
onTextChanged: {
scrollOffset = 0;
scrollAnimation.restart();
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
SequentialAnimation {
id: scrollAnimation
running: mediaText.needsScrolling && textContainer.visible
loops: Animation.Infinite
Item {
id: textClip
anchors.fill: parent
clip: true
PauseAnimation {
duration: 2000
StyledText {
id: mediaText
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
property real scrollOffset: 0
property real textShift: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: (needsScrolling ? -scrollOffset : 0) + textShift
opacity: 1
onTextChanged: {
scrollOffset = 0;
textShift = 0;
scrollAnimation.restart();
textChangeAnimation.restart();
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
SequentialAnimation {
id: scrollAnimation
running: mediaText.needsScrolling && textContainer.visible
loops: Animation.Infinite
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
}
PauseAnimation {
duration: 2000
}
SequentialAnimation {
id: textChangeAnimation
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
ParallelAnimation {
NumberAnimation {
target: mediaText
property: "opacity"
from: 0.7
to: 1
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
NumberAnimation {
target: mediaText
property: "textShift"
from: 4
to: 0
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
}
}

View File

@@ -16,8 +16,11 @@ BasePill {
enableCursor: false
property var parentWindow: null
property var widgetData: null
property string section: "right"
property bool isAtBottom: false
property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -40,6 +43,76 @@ BasePill {
return `${id}::${tooltipTitle}`;
}
function trayIconSourceFor(trayItem) {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
function activateInlineTrayItem(trayItem, anchorItem) {
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
if (!trayItem) {
return;
}
if (!trayItem.hasMenu) {
const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function toggleIconName() {
const edge = root.axis?.edge;
if (root.useOverflowPopup) {
switch (edge) {
case "left":
return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
case "right":
return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
case "bottom":
return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
case "top":
return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
}
if (edge === "left" || edge === "right") {
return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
}
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
function callContextMenuFallback(trayItemId, globalX, globalY) {
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
@@ -78,6 +151,13 @@ BasePill {
item: item
}))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
key: getTrayItemKey(item),
item: item
}))
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
@@ -103,6 +183,7 @@ BasePill {
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0
@@ -198,10 +279,11 @@ BasePill {
id: rowComp
Row {
spacing: 0
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
Repeater {
model: ScriptModel {
values: root.mainBarItems
values: root.displayedMainBarItems
objectProp: "key"
}
@@ -209,29 +291,7 @@ BasePill {
id: delegateRoot
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize
height: root.barThickness
@@ -371,7 +431,8 @@ BasePill {
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
@@ -380,8 +441,8 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
root.dropTargetIndex = root.draggedIndex;
}
}
if (!dragHandler.dragging)
@@ -391,7 +452,8 @@ BasePill {
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
@@ -407,7 +469,8 @@ BasePill {
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
@@ -429,7 +492,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: root.menuOpen ? "expand_less" : "expand_more"
name: root.toggleIconName()
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
}
@@ -451,6 +514,301 @@ BasePill {
}
}
}
Repeater {
model: ScriptModel {
values: root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
}
}
Component {
id: inlineExpandedTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
visible: width > 0 || height > 0
Behavior on width {
enabled: !root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: inlineVisualContent
width: root.trayItemSize
height: root.trayItemSize
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
radius: Theme.cornerRadius
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
opacity: root.inlineExpanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
IconImage {
id: inlineIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !inlineIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: inlineItemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: inlineTrayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
enabled: root.inlineExpanded
onPressed: mouse => {
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
inlineItemRipple.trigger(pos.x, pos.y);
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
root.activateInlineTrayItem(trayItem, inlineVisualContent);
return;
}
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
}
}
}
}
Component {
id: verticalMainTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = root.draggedIndex;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
}
}
}
}
@@ -459,219 +817,23 @@ BasePill {
Column {
spacing: 0
// Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
// When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
// Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
Repeater {
model: ScriptModel {
values: root.mainBarItems
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
objectProp: "key"
}
delegate: verticalMainTrayItemDelegate
}
delegate: Item {
id: delegateRoot
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: delegateRoot.shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!delegateRoot.trayItem)
return;
if (!delegateRoot.trayItem.onlyMenu) {
delegateRoot.trayItem.activate();
return;
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
if (!delegateRoot.trayItem?.hasMenu) {
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
Item {
@@ -689,14 +851,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: {
const edge = root.axis?.edge;
if (edge === "left") {
return root.menuOpen ? "chevron_left" : "chevron_right";
} else {
return root.menuOpen ? "chevron_right" : "chevron_left";
}
}
name: root.toggleIconName()
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
}
@@ -718,6 +873,22 @@ BasePill {
}
}
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? root.displayedMainBarItems : []
objectProp: "key"
}
delegate: verticalMainTrayItemDelegate
}
}
}
@@ -733,7 +904,7 @@ BasePill {
blurRadius: Theme.cornerRadius
}
visible: root.menuOpen
visible: root.useOverflowPopup && root.menuOpen
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
@@ -749,13 +920,14 @@ BasePill {
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
}
Connections {
target: PopoutManager
function onPopoutOpening() {
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
}
}
@@ -1021,30 +1193,7 @@ BasePill {
delegate: Rectangle {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem?.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize + 4
height: root.trayItemSize + 4
@@ -1313,7 +1462,8 @@ BasePill {
onVisibleChanged: {
if (visible) {
updatePosition();
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null);
}

View File

@@ -0,0 +1,161 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: gaugeRoot
property real value: 0
property string label: ""
property string sublabel: ""
property string detail: ""
property color accentColor: Theme.primary
property color detailColor: Theme.surfaceVariantText
readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
readonly property real glowExtra: thickness * 1.4
readonly property real arcPadding: (thickness + glowExtra) / 2
readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
readonly property real maxTextWidth: innerDiameter * 0.9
readonly property real baseLabelSize: Math.round(width * 0.18)
readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
property real animValue: 0
onValueChanged: animValue = Math.min(1, Math.max(0, value))
Behavior on animValue {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
Canvas {
id: glowCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
glowCanvas.requestPaint();
}
function onAccentColorChanged() {
glowCanvas.requestPaint();
}
function onWidthChanged() {
glowCanvas.requestPaint();
}
function onHeightChanged() {
glowCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Canvas {
id: arcCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = gaugeRoot.accentColor;
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
arcCanvas.requestPaint();
}
function onAccentColorChanged() {
arcCanvas.requestPaint();
}
function onWidthChanged() {
arcCanvas.requestPaint();
}
function onHeightChanged() {
arcCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Column {
anchors.centerIn: parent
spacing: 1
StyledText {
text: gaugeRoot.label
font.pixelSize: gaugeRoot.labelSize
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.sublabel
font.pixelSize: gaugeRoot.sublabelSize
font.weight: Font.Medium
color: gaugeRoot.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.detail
font.pixelSize: gaugeRoot.detailSize
font.family: SettingsData.monoFontFamily
color: gaugeRoot.detailColor
anchors.horizontalCenter: parent.horizontalCenter
visible: gaugeRoot.detail.length > 0
}
}
}

View File

@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
RowLayout {
property string label: ""
property string value: ""
Layout.fillWidth: true
spacing: Theme.spacingS
StyledText {
text: label + ":"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 100
}
StyledText {
text: value
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
elide: Text.ElideRight
}
}

View File

@@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
Rectangle {
id: card
property string title: ""
property string icon: ""
property string value: ""
property string subtitle: ""
property color accentColor: Theme.primary
property var history: []
property var history2: null
property real maxValue: 100
property bool showSecondary: false
property string extraInfo: ""
property color extraInfoColor: Theme.surfaceVariantText
property int historySize: 60
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Canvas {
id: graphCanvas
anchors.fill: parent
anchors.margins: 4
renderStrategy: Canvas.Cooperative
property var hist: card.history
property var hist2: card.history2
onHistChanged: requestPaint()
onHist2Changed: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.clearRect(0, 0, width, height);
if (!hist || hist.length < 2)
return;
let max = card.maxValue;
if (max <= 0) {
max = 1;
for (let k = 0; k < hist.length; k++)
max = Math.max(max, hist[k]);
if (hist2) {
for (let l = 0; l < hist2.length; l++)
max = Math.max(max, hist2[l]);
}
max *= 1.1;
}
const c = card.accentColor;
const grad = ctx.createLinearGradient(0, 0, 0, height);
grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, height);
for (let i = 0; i < hist.length; i++) {
const x = (width / (card.historySize - 1)) * i;
const y = height - (hist[i] / max) * height * 0.8;
ctx.lineTo(x, y);
}
ctx.lineTo((width / (card.historySize - 1)) * (hist.length - 1), height);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
ctx.lineWidth = 2;
ctx.beginPath();
for (let j = 0; j < hist.length; j++) {
const px = (width / (card.historySize - 1)) * j;
const py = height - (hist[j] / max) * height * 0.8;
j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
if (hist2 && hist2.length >= 2 && card.showSecondary) {
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
for (let m = 0; m < hist2.length; m++) {
const sx = (width / (card.historySize - 1)) * m;
const sy = height - (hist2[m] / max) * height * 0.8;
m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
}
ctx.stroke();
ctx.setLineDash([]);
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
DankIcon {
name: card.icon
size: Theme.iconSize
color: card.accentColor
}
StyledText {
text: card.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
Layout.fillWidth: true
}
StyledText {
text: card.extraInfo
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: card.extraInfoColor
visible: card.extraInfo.length > 0
}
}
Item {
Layout.fillHeight: true
}
StyledText {
text: card.value
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: card.subtitle
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}

View File

@@ -3,7 +3,6 @@ import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -73,6 +72,7 @@ Item {
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
historySize: root.historySize
title: "CPU"
icon: "memory"
value: DgopService.cpuUsage.toFixed(1) + "%"
@@ -88,6 +88,7 @@ Item {
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
historySize: root.historySize
title: I18n.tr("Memory")
icon: "sd_card"
value: DgopService.memoryUsage.toFixed(1) + "%"
@@ -109,6 +110,7 @@ Item {
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
historySize: root.historySize
title: I18n.tr("Network")
icon: "swap_horiz"
value: "↓ " + root.formatBytes(DgopService.networkRxRate)
@@ -125,6 +127,7 @@ Item {
PerformanceCard {
Layout.fillWidth: true
Layout.fillHeight: true
historySize: root.historySize
title: I18n.tr("Disk")
icon: "storage"
value: "R: " + root.formatBytes(DgopService.diskReadRate)
@@ -146,159 +149,4 @@ Item {
}
}
}
component PerformanceCard: Rectangle {
id: card
property string title: ""
property string icon: ""
property string value: ""
property string subtitle: ""
property color accentColor: Theme.primary
property var history: []
property var history2: null
property real maxValue: 100
property bool showSecondary: false
property string extraInfo: ""
property color extraInfoColor: Theme.surfaceVariantText
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Canvas {
id: graphCanvas
anchors.fill: parent
anchors.margins: 4
renderStrategy: Canvas.Cooperative
property var hist: card.history
property var hist2: card.history2
onHistChanged: requestPaint()
onHist2Changed: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.clearRect(0, 0, width, height);
if (!hist || hist.length < 2)
return;
let max = card.maxValue;
if (max <= 0) {
max = 1;
for (let k = 0; k < hist.length; k++)
max = Math.max(max, hist[k]);
if (hist2) {
for (let l = 0; l < hist2.length; l++)
max = Math.max(max, hist2[l]);
}
max *= 1.1;
}
const c = card.accentColor;
const grad = ctx.createLinearGradient(0, 0, 0, height);
grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, height);
for (let i = 0; i < hist.length; i++) {
const x = (width / (root.historySize - 1)) * i;
const y = height - (hist[i] / max) * height * 0.8;
ctx.lineTo(x, y);
}
ctx.lineTo((width / (root.historySize - 1)) * (hist.length - 1), height);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
ctx.lineWidth = 2;
ctx.beginPath();
for (let j = 0; j < hist.length; j++) {
const px = (width / (root.historySize - 1)) * j;
const py = height - (hist[j] / max) * height * 0.8;
j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
if (hist2 && hist2.length >= 2 && card.showSecondary) {
ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
for (let m = 0; m < hist2.length; m++) {
const sx = (width / (root.historySize - 1)) * m;
const sy = height - (hist2[m] / max) * height * 0.8;
m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
}
ctx.stroke();
ctx.setLineDash([]);
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
DankIcon {
name: card.icon
size: Theme.iconSize
color: card.accentColor
}
StyledText {
text: card.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
Layout.fillWidth: true
}
StyledText {
text: card.extraInfo
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: card.extraInfoColor
visible: card.extraInfo.length > 0
}
}
Item {
Layout.fillHeight: true
}
StyledText {
text: card.value
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: card.subtitle
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
}

View File

@@ -0,0 +1,334 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: processItemRoot
property var process: null
property bool isExpanded: false
property bool isSelected: false
property var contextMenu: null
signal toggleExpand
signal clicked
signal contextMenuRequested(real mouseX, real mouseY)
readonly property int processPid: process?.pid ?? 0
readonly property real processCpu: process?.cpu ?? 0
readonly property int processMemKB: process?.memoryKB ?? 0
readonly property string processCmd: process?.command ?? ""
readonly property string processFullCmd: process?.fullCommand ?? processCmd
height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44
radius: Theme.cornerRadius
color: {
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent";
}
border.color: {
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
}
border.width: 1
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
processItemRoot.contextMenuRequested(mouse.x, mouse.y);
return;
}
processItemRoot.clicked();
processItemRoot.toggleExpand();
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 44
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 0
Item {
Layout.fillWidth: true
Layout.minimumWidth: 200
height: parent.height
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: DgopService.getProcessIcon(processItemRoot.processCmd)
size: Theme.iconSize - 4
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: processItemRoot.processCmd
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: Math.min(implicitWidth, 280)
anchors.verticalCenter: parent.verticalCenter
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processCpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processCpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatCpuUsage(processItemRoot.processCpu)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processMemKB > 1024 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatMemoryUsage(processItemRoot.processMemKB)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Theme.error;
if (processItemRoot.processMemKB > 1024 * 1024)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 80
height: parent.height
StyledText {
anchors.centerIn: parent
text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
Item {
Layout.preferredWidth: 40
height: parent.height
DankIcon {
anchors.centerIn: parent
name: processItemRoot.isExpanded ? "expand_less" : "expand_more"
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
}
}
}
}
Rectangle {
id: expandedRect
width: parent.width - Theme.spacingM * 2
height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius - 2
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6)
clip: true
visible: processItemRoot.isExpanded
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Column {
id: expandedContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingS
spacing: Theme.spacingXS
RowLayout {
width: parent.width
spacing: Theme.spacingS
StyledText {
id: cmdLabel
text: I18n.tr("Full Command:", "process detail label")
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
StyledText {
id: cmdText
text: processItemRoot.processFullCmd
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideMiddle
}
Rectangle {
id: copyBtn
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
radius: Theme.cornerRadius - 2
color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "content_copy"
size: 14
color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: copyMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]);
}
}
}
}
Row {
spacing: Theme.spacingL
Row {
spacing: Theme.spacingXS
StyledText {
text: "PPID:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
StyledText {
text: "Mem:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
}
}
}
}
}

View File

@@ -374,162 +374,4 @@ DankPopout {
}
}
}
component CircleGauge: Item {
id: gaugeRoot
property real value: 0
property string label: ""
property string sublabel: ""
property string detail: ""
property color accentColor: Theme.primary
property color detailColor: Theme.surfaceVariantText
readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
readonly property real glowExtra: thickness * 1.4
readonly property real arcPadding: (thickness + glowExtra) / 2
readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
readonly property real maxTextWidth: innerDiameter * 0.9
readonly property real baseLabelSize: Math.round(width * 0.18)
readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
property real animValue: 0
onValueChanged: animValue = Math.min(1, Math.max(0, value))
Behavior on animValue {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
Canvas {
id: glowCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
glowCanvas.requestPaint();
}
function onAccentColorChanged() {
glowCanvas.requestPaint();
}
function onWidthChanged() {
glowCanvas.requestPaint();
}
function onHeightChanged() {
glowCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Canvas {
id: arcCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
const startAngle = -Math.PI * 0.5;
const endAngle = Math.PI * 1.5;
ctx.lineCap = "round";
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
if (gaugeRoot.animValue > 0) {
const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
ctx.beginPath();
ctx.arc(cx, cy, radius, startAngle, prog);
ctx.strokeStyle = gaugeRoot.accentColor;
ctx.lineWidth = gaugeRoot.thickness;
ctx.stroke();
}
}
Connections {
target: gaugeRoot
function onAnimValueChanged() {
arcCanvas.requestPaint();
}
function onAccentColorChanged() {
arcCanvas.requestPaint();
}
function onWidthChanged() {
arcCanvas.requestPaint();
}
function onHeightChanged() {
arcCanvas.requestPaint();
}
}
Component.onCompleted: requestPaint()
}
Column {
anchors.centerIn: parent
spacing: 1
StyledText {
text: gaugeRoot.label
font.pixelSize: gaugeRoot.labelSize
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.sublabel
font.pixelSize: gaugeRoot.sublabelSize
font.weight: Font.Medium
color: gaugeRoot.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: gaugeRoot.detail
font.pixelSize: gaugeRoot.detailSize
font.family: SettingsData.monoFontFamily
color: gaugeRoot.detailColor
anchors.horizontalCenter: parent.horizontalCenter
visible: gaugeRoot.detail.length > 0
}
}
}
}

View File

@@ -368,402 +368,4 @@ Item {
}
}
}
component SortableHeader: Item {
id: headerItem
property string text: ""
property string sortKey: ""
property string currentSort: ""
property bool sortAscending: false
property int alignment: Text.AlignHCenter
signal clicked
readonly property bool isActive: sortKey === currentSort
height: 36
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: Theme.cornerRadius
color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent")
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 4
Item {
Layout.fillWidth: headerItem.alignment === Text.AlignLeft
visible: headerItem.alignment !== Text.AlignLeft
}
StyledText {
text: headerItem.text
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: headerItem.isActive ? Font.Bold : Font.Medium
color: headerItem.isActive ? Theme.primary : Theme.surfaceText
opacity: headerItem.isActive ? 1 : 0.8
}
DankIcon {
name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward"
size: Theme.fontSizeSmall
color: Theme.primary
visible: headerItem.isActive
}
Item {
Layout.fillWidth: headerItem.alignment !== Text.AlignLeft
visible: headerItem.alignment === Text.AlignLeft
}
}
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: headerItem.clicked()
}
}
component ProcessItem: Rectangle {
id: processItemRoot
property var process: null
property bool isExpanded: false
property bool isSelected: false
property var contextMenu: null
signal toggleExpand
signal clicked
signal contextMenuRequested(real mouseX, real mouseY)
readonly property int processPid: process?.pid ?? 0
readonly property real processCpu: process?.cpu ?? 0
readonly property int processMemKB: process?.memoryKB ?? 0
readonly property string processCmd: process?.command ?? ""
readonly property string processFullCmd: process?.fullCommand ?? processCmd
height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44
radius: Theme.cornerRadius
color: {
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent";
}
border.color: {
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
}
border.width: 1
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
processItemRoot.contextMenuRequested(mouse.x, mouse.y);
return;
}
processItemRoot.clicked();
processItemRoot.toggleExpand();
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 44
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 0
Item {
Layout.fillWidth: true
Layout.minimumWidth: 200
height: parent.height
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: DgopService.getProcessIcon(processItemRoot.processCmd)
size: Theme.iconSize - 4
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: processItemRoot.processCmd
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: Math.min(implicitWidth, 280)
anchors.verticalCenter: parent.verticalCenter
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processCpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processCpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatCpuUsage(processItemRoot.processCpu)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processCpu > 80)
return Theme.error;
if (processItemRoot.processCpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 100
height: parent.height
Rectangle {
anchors.centerIn: parent
width: 70
height: 24
radius: Theme.cornerRadius
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
if (processItemRoot.processMemKB > 1024 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
}
StyledText {
anchors.centerIn: parent
text: DgopService.formatMemoryUsage(processItemRoot.processMemKB)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (processItemRoot.processMemKB > 2 * 1024 * 1024)
return Theme.error;
if (processItemRoot.processMemKB > 1024 * 1024)
return Theme.warning;
return Theme.surfaceText;
}
}
}
}
Item {
Layout.preferredWidth: 80
height: parent.height
StyledText {
anchors.centerIn: parent
text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceVariantText
}
}
Item {
Layout.preferredWidth: 40
height: parent.height
DankIcon {
anchors.centerIn: parent
name: processItemRoot.isExpanded ? "expand_less" : "expand_more"
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
}
}
}
}
Rectangle {
id: expandedRect
width: parent.width - Theme.spacingM * 2
height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius - 2
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6)
clip: true
visible: processItemRoot.isExpanded
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Column {
id: expandedContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingS
spacing: Theme.spacingXS
RowLayout {
width: parent.width
spacing: Theme.spacingS
StyledText {
id: cmdLabel
text: I18n.tr("Full Command:", "process detail label")
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
StyledText {
id: cmdText
text: processItemRoot.processFullCmd
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideMiddle
}
Rectangle {
id: copyBtn
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
radius: Theme.cornerRadius - 2
color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "content_copy"
size: 14
color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: copyMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]);
}
}
}
}
Row {
spacing: Theme.spacingL
Row {
spacing: Theme.spacingXS
StyledText {
text: "PPID:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
Row {
spacing: Theme.spacingXS
StyledText {
text: "Mem:"
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
StyledText {
text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall - 2
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
Item {
id: headerItem
property string text: ""
property string sortKey: ""
property string currentSort: ""
property bool sortAscending: false
property int alignment: Text.AlignHCenter
signal clicked
readonly property bool isActive: sortKey === currentSort
height: 36
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: Theme.cornerRadius
color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent")
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: 4
Item {
Layout.fillWidth: headerItem.alignment === Text.AlignLeft
visible: headerItem.alignment !== Text.AlignLeft
}
StyledText {
text: headerItem.text
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: headerItem.isActive ? Font.Bold : Font.Medium
color: headerItem.isActive ? Theme.primary : Theme.surfaceText
opacity: headerItem.isActive ? 1 : 0.8
}
DankIcon {
name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward"
size: Theme.fontSizeSmall
color: Theme.primary
visible: headerItem.isActive
}
Item {
Layout.fillWidth: headerItem.alignment !== Text.AlignLeft
visible: headerItem.alignment === Text.AlignLeft
}
}
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: headerItem.clicked()
}
}

View File

@@ -358,29 +358,4 @@ Item {
}
}
}
component InfoRow: RowLayout {
property string label: ""
property string value: ""
Layout.fillWidth: true
spacing: Theme.spacingS
StyledText {
text: label + ":"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 100
}
StyledText {
text: value
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}

View File

@@ -46,6 +46,13 @@ Item {
onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked)
}
SettingsToggleRow {
text: I18n.tr("Adaptive Media Width")
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
checked: SettingsData.mediaAdaptiveWidthEnabled
onToggled: checked => SettingsData.set("mediaAdaptiveWidthEnabled", checked)
}
SettingsDropdownRow {
property var scrollOptsInternal: ["volume", "song", "nothing"]
property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")]

View File

@@ -430,7 +430,7 @@ Item {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -712,6 +712,8 @@ Item {
item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps;
if (widget.barShowOverflowBadge !== undefined)
item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
}
widgets.push(item);
});

View File

@@ -39,7 +39,7 @@ Column {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -437,7 +437,7 @@ Column {
Row {
spacing: Theme.spacingXS
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock"
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
DankActionButton {
id: compactModeButton
@@ -543,6 +543,39 @@ Column {
}
}
DankActionButton {
id: trayMenuButton
buttonSize: 32
visible: modelData.id === "systemTray"
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
trayContextMenu.widgetData = modelData;
trayContextMenu.sectionId = root.sectionId;
trayContextMenu.widgetIndex = index;
var buttonPos = trayMenuButton.mapToItem(root, 0, 0);
var popupWidth = trayContextMenu.width;
var popupHeight = trayContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0)
xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS;
var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
trayContextMenu.x = xPos;
trayContextMenu.y = yPos;
trayContextMenu.open();
}
}
Rectangle {
id: compactModeTooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
@@ -931,6 +964,88 @@ Column {
}
}
Popup {
id: trayContextMenu
property var widgetData: null
property string sectionId: ""
property int widgetIndex: -1
readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData
width: 220
height: contentColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
}
contentItem: Item {
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayOverflowArea.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: "arrow_selector_tool"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Use Inline Expansion")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayOverflowToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false
}
MouseArea {
id: trayOverflowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue);
}
}
}
}
}
}
Popup {
id: diskUsageContextMenu

View File

@@ -0,0 +1,147 @@
import QtQuick
import Quickshell.Services.Notifications
import qs.Common
QtObject {
id: wrapper
property bool popup: false
property bool removedByLimit: false
property bool isPersistent: true
property int seq: 0
property string persistedImagePath: ""
onPopupChanged: {
if (!popup) {
NotificationService.removeFromVisibleNotifications(wrapper);
}
}
readonly property Timer timer: Timer {
interval: {
if (!wrapper.notification)
return 5000;
switch (wrapper.urgency) {
case NotificationUrgency.Low:
return SettingsData.notificationTimeoutLow;
case NotificationUrgency.Critical:
return SettingsData.notificationTimeoutCritical;
default:
return SettingsData.notificationTimeoutNormal;
}
}
repeat: false
running: false
onTriggered: {
if (interval > 0) {
wrapper.popup = false;
}
}
}
readonly property date time: new Date()
readonly property string timeStr: {
NotificationService.timeUpdateTick;
NotificationService.clockFormatChanged;
const now = new Date();
const diff = now.getTime() - time.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
if (hours < 1) {
if (minutes < 1) {
return "now";
}
return `${minutes}m ago`;
}
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate());
const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24));
if (daysDiff === 0) {
return formatTime(time);
}
try {
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
const weekday = time.toLocaleDateString(localeName, {
weekday: "long"
});
return `${weekday}, ${formatTime(time)}`;
} catch (e) {
return formatTime(time);
}
}
function formatTime(date) {
let use24Hour = true;
try {
if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
use24Hour = SettingsData.use24HourClock;
}
} catch (e) {
use24Hour = true;
}
if (use24Hour) {
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
} else {
return date.toLocaleTimeString(Qt.locale(), "h:mm AP");
}
}
required property Notification notification
readonly property string summary: (notification?.summary ?? "").replace(/<img\b[^>]*>/gi, "")
readonly property string body: (notification?.body ?? "").replace(/<img\b[^>]*>/gi, "")
readonly property string htmlBody: NotificationService._resolveHtmlBody(body)
readonly property string appIcon: notification?.appIcon ?? ""
readonly property string appName: {
if (!notification)
return "app";
if (notification.appName == "") {
const entry = DesktopEntries.heuristicLookup(notification.desktopEntry);
if (entry && entry.name)
return entry.name.toLowerCase();
}
return notification.appName || "app";
}
readonly property string desktopEntry: notification?.desktopEntry ?? ""
readonly property string image: notification?.image ?? ""
readonly property string cleanImage: {
if (!image)
return "";
return Paths.strip(image);
}
property int urgencyOverride: notification?.urgency ?? NotificationUrgency.Normal
readonly property int urgency: urgencyOverride
readonly property list<NotificationAction> actions: notification?.actions ?? []
readonly property Connections conn: Connections {
target: wrapper.notification?.Retainable ?? null
function onDropped(): void {
NotificationService.allWrappers = NotificationService.allWrappers.filter(w => w !== wrapper);
NotificationService.notifications = NotificationService.notifications.filter(w => w !== wrapper);
if (NotificationService.bulkDismissing) {
return;
}
const groupKey = NotificationService.getGroupKey(wrapper);
const remainingInGroup = NotificationService.notifications.filter(n => NotificationService.getGroupKey(n) === groupKey);
if (remainingInGroup.length <= 1) {
NotificationService.clearGroupExpansionState(groupKey);
}
NotificationService.cleanupExpansionStates();
NotificationService._recomputeGroupsLater();
}
function onAboutToDestroy(): void {
wrapper.destroy();
}
}
}

View File

@@ -655,150 +655,6 @@ Singleton {
}
}
component NotifWrapper: QtObject {
id: wrapper
property bool popup: false
property bool removedByLimit: false
property bool isPersistent: true
property int seq: 0
property string persistedImagePath: ""
onPopupChanged: {
if (!popup) {
removeFromVisibleNotifications(wrapper);
}
}
readonly property Timer timer: Timer {
interval: {
if (!wrapper.notification)
return 5000;
switch (wrapper.urgency) {
case NotificationUrgency.Low:
return SettingsData.notificationTimeoutLow;
case NotificationUrgency.Critical:
return SettingsData.notificationTimeoutCritical;
default:
return SettingsData.notificationTimeoutNormal;
}
}
repeat: false
running: false
onTriggered: {
if (interval > 0) {
wrapper.popup = false;
}
}
}
readonly property date time: new Date()
readonly property string timeStr: {
root.timeUpdateTick;
root.clockFormatChanged;
const now = new Date();
const diff = now.getTime() - time.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
if (hours < 1) {
if (minutes < 1) {
return "now";
}
return `${minutes}m ago`;
}
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate());
const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24));
if (daysDiff === 0) {
return formatTime(time);
}
try {
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
const weekday = time.toLocaleDateString(localeName, {
weekday: "long"
});
return `${weekday}, ${formatTime(time)}`;
} catch (e) {
return formatTime(time);
}
}
function formatTime(date) {
let use24Hour = true;
try {
if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
use24Hour = SettingsData.use24HourClock;
}
} catch (e) {
use24Hour = true;
}
if (use24Hour) {
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
} else {
return date.toLocaleTimeString(Qt.locale(), "h:mm AP");
}
}
required property Notification notification
readonly property string summary: (notification?.summary ?? "").replace(/<img\b[^>]*>/gi, "")
readonly property string body: (notification?.body ?? "").replace(/<img\b[^>]*>/gi, "")
readonly property string htmlBody: root._resolveHtmlBody(body)
readonly property string appIcon: notification?.appIcon ?? ""
readonly property string appName: {
if (!notification)
return "app";
if (notification.appName == "") {
const entry = DesktopEntries.heuristicLookup(notification.desktopEntry);
if (entry && entry.name)
return entry.name.toLowerCase();
}
return notification.appName || "app";
}
readonly property string desktopEntry: notification?.desktopEntry ?? ""
readonly property string image: notification?.image ?? ""
readonly property string cleanImage: {
if (!image)
return "";
return Paths.strip(image);
}
property int urgencyOverride: notification?.urgency ?? NotificationUrgency.Normal
readonly property int urgency: urgencyOverride
readonly property list<NotificationAction> actions: notification?.actions ?? []
readonly property Connections conn: Connections {
target: wrapper.notification?.Retainable ?? null
function onDropped(): void {
root.allWrappers = root.allWrappers.filter(w => w !== wrapper);
root.notifications = root.notifications.filter(w => w !== wrapper);
if (root.bulkDismissing) {
return;
}
const groupKey = getGroupKey(wrapper);
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey);
if (remainingInGroup.length <= 1) {
clearGroupExpansionState(groupKey);
}
cleanupExpansionStates();
root._recomputeGroupsLater();
}
function onAboutToDestroy(): void {
wrapper.destroy();
}
}
}
Component {
id: notifComponent
NotifWrapper {}