diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar')
44 files changed, 17078 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/AxisContext.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/AxisContext.qml new file mode 100644 index 0000000..8a8fe97 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/AxisContext.qml @@ -0,0 +1,57 @@ +import QtQuick + +QtObject { + id: root + + property string edge: "top" + + readonly property string orientation: isVertical ? "vertical" : "horizontal" + readonly property bool isVertical: edge === "left" || edge === "right" + readonly property bool isHorizontal: !isVertical + + function primarySize(item) { + return isVertical ? item.height : item.width + } + + function crossSize(item) { + return isVertical ? item.width : item.height + } + + function setPrimaryPos(item, value) { + if (isVertical) { + item.y = value + } else { + item.x = value + } + } + + function getPrimaryPos(item) { + return isVertical ? item.y : item.x + } + + function primaryAnchor(anchors) { + return isVertical ? anchors.verticalCenter : anchors.horizontalCenter + } + + function crossAnchor(anchors) { + return isVertical ? anchors.horizontalCenter : anchors.verticalCenter + } + + function outerVisualEdge() { + if (edge === "bottom") return "bottom" + if (edge === "left") return "right" + if (edge === "right") return "left" + if (edge === "top") return "top" + return "bottom" + } + + signal axisEdgeChanged() + signal axisOrientationChanged() + signal changed() // Single coalesced signal + + onEdgeChanged: { + axisEdgeChanged() + axisOrientationChanged() + changed() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/BarCanvas.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/BarCanvas.qml new file mode 100644 index 0000000..df3f17f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/BarCanvas.qml @@ -0,0 +1,671 @@ +import QtQuick +import QtQuick.Shapes +import qs.Common +import qs.Services + +Item { + id: root + + required property var barWindow + required property var axis + required property var barConfig + + anchors.fill: parent + + anchors.left: parent.left + anchors.top: parent.top + readonly property bool gothEnabled: (barConfig?.gothCornersEnabled ?? false) && !barWindow.hasMaximizedToplevel + anchors.leftMargin: -(gothEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0) + anchors.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0) + anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0) + anchors.bottomMargin: -(gothEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0) + + readonly property int barPos: barConfig?.position ?? 0 + readonly property bool isTop: barPos === SettingsData.Position.Top + readonly property bool isBottom: barPos === SettingsData.Position.Bottom + readonly property bool isLeft: barPos === SettingsData.Position.Left + readonly property bool isRight: barPos === SettingsData.Position.Right + + property real wing: gothEnabled ? barWindow._wingR : 0 + + Behavior on wing { + enabled: root.width > 0 && root.height > 0 + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + property real rt: { + if (barConfig?.squareCorners ?? false) + return 0; + if (barWindow.hasMaximizedToplevel) + return 0; + return Theme.cornerRadius; + } + + Behavior on rt { + enabled: root.width > 0 && root.height > 0 + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + // M3 elevation shadow — Level 2 baseline (navigation bar), with per-bar override support + readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0 + readonly property var elevLevel: Theme.elevationLevel2 + readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride + readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top"))) + readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection + readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit" + readonly property string perBarManualShadowDirection: { + switch (barConfig?.shadowDirection) { + case "top": + case "topLeft": + case "topRight": + case "bottom": + return barConfig.shadowDirection; + default: + return "top"; + } + } + readonly property string effectiveShadowDirection: { + if (!hasPerBarOverride) + return globalShadowDirection; + switch (perBarShadowDirectionMode) { + case "autoBar": + return autoBarShadowDirection; + case "manual": + return perBarManualShadowDirection === "autoBar" ? autoBarShadowDirection : perBarManualShadowDirection; + default: + return globalShadowDirection; + } + } + + // Per-bar override values (when barConfig.shadowIntensity > 0) + readonly property real overrideBlurPx: (barConfig?.shadowIntensity ?? 0) * 0.2 + readonly property real overrideOpacity: (barConfig?.shadowOpacity ?? 60) / 100 + readonly property string overrideColorMode: barConfig?.shadowColorMode ?? "default" + readonly property color overrideBaseColor: { + switch (overrideColorMode) { + case "surface": + return Theme.surface; + case "primary": + return Theme.primary; + case "secondary": + return Theme.secondary; + case "custom": + return barConfig?.shadowCustomColor ?? "#000000"; + default: + return "#000000"; + } + } + + // Resolved values — per-bar override wins if set, otherwise use global M3 elevation + readonly property real shadowBlurPx: hasPerBarOverride ? overrideBlurPx : (elevLevel.blurPx ?? 8) + readonly property color shadowColor: hasPerBarOverride ? Theme.withAlpha(overrideBaseColor, overrideOpacity) : Theme.elevationShadowColor(elevLevel) + readonly property real shadowOffsetMagnitude: hasPerBarOverride ? (overrideBlurPx * 0.5) : Theme.elevationOffsetMagnitude(elevLevel, 4, effectiveShadowDirection) + readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) + readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) + + readonly property string mainPath: generatePathForPosition(width, height) + readonly property string borderFullPath: generateBorderFullPath(width, height) + readonly property string borderEdgePath: generateBorderEdgePath(width, height) + property bool mainPathCorrectShape: false + property bool borderFullPathCorrectShape: false + property bool borderEdgePathCorrectShape: false + + onMainPathChanged: { + if (width > 0 && height > 0) { + root: mainPathCorrectShape = true; + } + } + onBorderFullPathChanged: { + if (width > 0 && height > 0) { + root: borderFullPathCorrectShape = true; + } + } + onBorderEdgePathChanged: { + if (width > 0 && height > 0) { + root: borderEdgePathCorrectShape = true; + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + z: -999 + onClicked: { + const activePopout = PopoutManager.getActivePopout(barWindow.screen); + if (activePopout) { + if (activePopout.dashVisible !== undefined) { + activePopout.dashVisible = false; + } else if (activePopout.notificationHistoryVisible !== undefined) { + activePopout.notificationHistoryVisible = false; + } else { + activePopout.close(); + } + } + TrayMenuManager.closeAllMenus(); + } + } + + ElevationShadow { + id: barShadow + visible: root.shadowEnabled && root.width > 0 && root.height > 0 + + // Size to the bar's rectangular body, excluding gothic wing extensions + x: root.isRight ? root.wing : 0 + y: root.isBottom ? root.wing : 0 + width: axis.isVertical ? (parent.width - root.wing) : parent.width + height: axis.isVertical ? parent.height : (parent.height - root.wing) + + shadowEnabled: root.shadowEnabled + level: root.hasPerBarOverride ? null : root.elevLevel + direction: root.effectiveShadowDirection + fallbackOffset: 4 + targetRadius: root.rt + targetColor: barWindow._bgColor + + shadowBlurPx: root.shadowBlurPx + shadowOffsetX: root.shadowOffsetX + shadowOffsetY: root.shadowOffsetY + shadowColor: root.shadowColor + blurMax: Theme.elevationBlurMax + } + + Loader { + id: barShape + anchors.fill: parent + active: mainPathCorrectShape + sourceComponent: Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: barWindow._bgColor + strokeColor: "transparent" + strokeWidth: 0 + + PathSvg { + path: root.mainPath + } + } + } + } + + Loader { + id: barBorder + anchors.fill: parent + active: borderFullPathCorrectShape && borderEdgePathCorrectShape + + readonly property real _scale: CompositorService.getScreenScale(barWindow.screen) + readonly property real borderThickness: Math.ceil(Math.max(1, barConfig?.borderThickness ?? 1) * _scale) / _scale + readonly property real inset: borderThickness / 2 + readonly property string borderColorKey: barConfig?.borderColor || "surfaceText" + readonly property color baseColor: (borderColorKey === "surfaceText") ? Theme.surfaceText : (borderColorKey === "primary") ? Theme.primary : Theme.secondary + readonly property color borderColor: Theme.withAlpha(baseColor, barConfig?.borderOpacity ?? 1.0) + readonly property bool showFullBorder: (barConfig?.spacing ?? 4) > 0 + sourceComponent: Shape { + id: barBorderShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + visible: barConfig?.borderEnabled ?? false + + ShapePath { + fillColor: "transparent" + strokeColor: barBorder.borderColor + strokeWidth: barBorder.borderThickness + joinStyle: ShapePath.RoundJoin + capStyle: ShapePath.FlatCap + + PathSvg { + path: barBorder.showFullBorder ? root.borderFullPath : root.borderEdgePath + } + } + } + } + + function generatePathForPosition(w, h) { + if (isTop) + return generateTopPath(w, h); + if (isBottom) + return generateBottomPath(w, h); + if (isLeft) + return generateLeftPath(w, h); + if (isRight) + return generateRightPath(w, h); + return generateTopPath(w, h); + } + + function generateBorderPathForPosition() { + if (isTop) + return generateTopBorderPath(); + if (isBottom) + return generateBottomBorderPath(); + if (isLeft) + return generateLeftBorderPath(); + if (isRight) + return generateRightBorderPath(); + return generateTopBorderPath(); + } + + function generateTopPath(w, h) { + h = h - wing; + const r = wing; + const cr = rt; + + let d = `M ${cr} 0`; + d += ` L ${w - cr} 0`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`; + if (r > 0) { + d += ` L ${w} ${h + r}`; + d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`; + d += ` L ${r} ${h}`; + d += ` A ${r} ${r} 0 0 0 0 ${h + r}`; + } else { + d += ` L ${w} ${h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w - cr} ${h}`; + d += ` L ${cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; + } + d += ` L 0 ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; + d += " Z"; + return d; + } + + function generateBottomPath(w, h) { + const fullH = h; + h = h - wing; + const r = wing; + const cr = rt; + + let d = `M ${cr} ${fullH}`; + d += ` L ${w - cr} ${fullH}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`; + if (r > 0) { + d += ` L ${w} 0`; + d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`; + d += ` L ${r} ${r}`; + d += ` A ${r} ${r} 0 0 1 0 0`; + } else { + d += ` L ${w} ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`; + d += ` L ${cr} 0`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; + } + d += ` L 0 ${fullH - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`; + d += " Z"; + return d; + } + + function generateLeftPath(w, h) { + w = w - wing; + const r = wing; + const cr = rt; + + let d = `M 0 ${cr}`; + d += ` L 0 ${h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`; + if (r > 0) { + d += ` L ${w + r} ${h}`; + d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`; + d += ` L ${w} ${r}`; + d += ` A ${r} ${r} 0 0 1 ${w + r} 0`; + } else { + d += ` L ${w - cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${w} ${h - cr}`; + d += ` L ${w} ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`; + } + d += ` L ${cr} 0`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`; + d += " Z"; + return d; + } + + function generateRightPath(w, h) { + const fullW = w; + w = w - wing; + const r = wing; + const cr = rt; + + let d = `M ${fullW} ${cr}`; + d += ` L ${fullW} ${h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`; + if (r > 0) { + d += ` L 0 ${h}`; + d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`; + d += ` L ${r} ${r}`; + d += ` A ${r} ${r} 0 0 0 0 0`; + } else { + d += ` L ${cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; + d += ` L 0 ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; + } + d += ` L ${fullW - cr} 0`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`; + d += " Z"; + return d; + } + + function generateTopBorderPath() { + const w = barBorder.width; + const h = barBorder.height - wing; + const r = wing; + const cr = rt; + + let d = ""; + if (r > 0) { + d = `M ${w} ${h + r}`; + d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`; + d += ` L ${r} ${h}`; + d += ` A ${r} ${r} 0 0 0 0 ${h + r}`; + } else { + d = `M ${w} ${h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w - cr} ${h}`; + d += ` L ${cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; + } + return d; + } + + function generateBottomBorderPath() { + const w = barBorder.width; + const r = wing; + const cr = rt; + + let d = ""; + if (r > 0) { + d = `M ${w} 0`; + d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`; + d += ` L ${r} ${r}`; + d += ` A ${r} ${r} 0 0 1 0 0`; + } else { + d = `M ${w} ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w - cr} 0`; + d += ` L ${cr} 0`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 0 ${cr}`; + } + return d; + } + + function generateLeftBorderPath() { + const w = barBorder.width - wing; + const h = barBorder.height; + const r = wing; + const cr = rt; + + let d = ""; + if (r > 0) { + d = `M ${w + r} ${h}`; + d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`; + d += ` L ${w} ${r}`; + d += ` A ${r} ${r} 0 0 1 ${w + r} 0`; + } else { + d = `M ${w - cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w} ${h - cr}`; + d += ` L ${w} ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${w - cr} 0`; + } + return d; + } + + function generateRightBorderPath() { + const h = barBorder.height; + const r = wing; + const cr = rt; + + let d = ""; + if (r > 0) { + d = `M 0 ${h}`; + d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`; + d += ` L ${r} ${r}`; + d += ` A ${r} ${r} 0 0 0 0 0`; + } else { + d = `M ${cr} ${h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`; + d += ` L 0 ${cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`; + } + return d; + } + + function generateBorderFullPath(fullW, fullH) { + const i = barBorder.inset; + const r = wing; + const cr = rt; + + if (isTop) { + const w = fullW - i * 2; + const h = fullH - wing - i * 2; + + let d = `M ${i + cr} ${i}`; + d += ` L ${i + w - cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w} ${i + cr}`; + if (r > 0) { + d += ` L ${i + w} ${fullH - i}`; + d += ` A ${r} ${r} 0 0 0 ${i + w - r} ${i + h}`; + d += ` L ${i + r} ${i + h}`; + d += ` A ${r} ${r} 0 0 0 ${i} ${fullH - i}`; + } else { + d += ` L ${i + w} ${i + h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w - cr} ${i + h}`; + d += ` L ${i + cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h - cr}`; + } + d += ` L ${i} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`; + d += " Z"; + return d; + } + + if (isBottom) { + const w = fullW - i * 2; + const h = fullH - wing - i * 2; + + let d = `M ${i + cr} ${fullH - i}`; + d += ` L ${i + w - cr} ${fullH - i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w} ${fullH - i - cr}`; + if (r > 0) { + d += ` L ${i + w} ${i}`; + d += ` A ${r} ${r} 0 0 1 ${i + w - r} ${i + r}`; + d += ` L ${i + r} ${i + r}`; + d += ` A ${r} ${r} 0 0 1 ${i} ${i}`; + } else { + d += ` L ${i + w} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w - cr} ${i}`; + d += ` L ${i + cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i} ${i + cr}`; + } + d += ` L ${i} ${fullH - i - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + cr} ${fullH - i}`; + d += " Z"; + return d; + } + + if (isLeft) { + const w = fullW - wing - i * 2; + const h = fullH - i * 2; + + let d = `M ${i} ${i + cr}`; + d += ` L ${i} ${i + h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + cr} ${i + h}`; + if (r > 0) { + d += ` L ${fullW - i} ${i + h}`; + d += ` A ${r} ${r} 0 0 1 ${i + w} ${i + h - r}`; + d += ` L ${i + w} ${i + r}`; + d += ` A ${r} ${r} 0 0 1 ${fullW - i} ${i}`; + } else { + d += ` L ${i + w - cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w} ${i + h - cr}`; + d += ` L ${i + w} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w - cr} ${i}`; + } + d += ` L ${i + cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i} ${i + cr}`; + d += " Z"; + return d; + } + + if (isRight) { + const w = fullW - wing - i * 2; + const h = fullH - i * 2; + + let d = `M ${fullW - i} ${i + cr}`; + d += ` L ${fullW - i} ${i + h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${fullW - i - cr} ${i + h}`; + if (r > 0) { + d += ` L ${i} ${i + h}`; + d += ` A ${r} ${r} 0 0 0 ${i + r} ${i + h - r}`; + d += ` L ${i + r} ${i + r}`; + d += ` A ${r} ${r} 0 0 0 ${i} ${i}`; + } else { + d += ` L ${wing + i + cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${wing + i} ${i + h - cr}`; + d += ` L ${wing + i} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${wing + i + cr} ${i}`; + } + d += ` L ${fullW - i - cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${fullW - i} ${i + cr}`; + d += " Z"; + return d; + } + + return ""; + } + + function generateBorderEdgePath(fullW, fullH) { + const i = barBorder.inset; + const r = wing; + const cr = rt; + + if (isTop) { + const w = fullW - i * 2; + const h = fullH - wing - i * 2; + + let d = ""; + if (r > 0) { + d = `M ${i + w} ${i + h + r}`; + d += ` A ${r} ${r} 0 0 0 ${i + w - r} ${i + h}`; + d += ` L ${i + r} ${i + h}`; + d += ` A ${r} ${r} 0 0 0 ${i} ${i + h + r}`; + } else { + d = `M ${i + w} ${i + h - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w - cr} ${i + h}`; + d += ` L ${i + cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h - cr}`; + } + return d; + } + + if (isBottom) { + const w = fullW - i * 2; + + let d = ""; + if (r > 0) { + d = `M ${i + w} ${i}`; + d += ` A ${r} ${r} 0 0 1 ${i + w - r} ${i + r}`; + d += ` L ${i + r} ${i + r}`; + d += ` A ${r} ${r} 0 0 1 ${i} ${i}`; + } else { + d = `M ${i + w} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w - cr} ${i}`; + d += ` L ${i + cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i} ${i + cr}`; + } + return d; + } + + if (isLeft) { + const w = fullW - wing - i * 2; + const h = fullH - i * 2; + + let d = ""; + if (r > 0) { + d = `M ${i + w + r} ${i + h}`; + d += ` A ${r} ${r} 0 0 1 ${i + w} ${i + h - r}`; + d += ` L ${i + w} ${i + r}`; + d += ` A ${r} ${r} 0 0 1 ${i + w + r} ${i}`; + } else { + d = `M ${i + w - cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w} ${i + h - cr}`; + d += ` L ${i + w} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 0 ${i + w - cr} ${i}`; + } + return d; + } + + if (isRight) { + const h = fullH - i * 2; + + let d = ""; + if (r > 0) { + d = `M ${i} ${i + h}`; + d += ` A ${r} ${r} 0 0 0 ${i + r} ${i + h - r}`; + d += ` L ${i + r} ${i + r}`; + d += ` A ${r} ${r} 0 0 0 ${i} ${i}`; + } else { + d = `M ${i + cr} ${i + h}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h - cr}`; + d += ` L ${i} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`; + } + return d; + } + + return ""; + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/CenterSection.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/CenterSection.qml new file mode 100644 index 0000000..df86d4d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/CenterSection.qml @@ -0,0 +1,387 @@ +import QtQuick +import qs.Common + +Item { + id: root + + property var widgetsModel: null + property var components: null + property bool noBackground: false + required property var axis + property string section: "center" + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property real barSpacing: 4 + property var barConfig: null + property var blurBarWindow: null + property bool overrideAxisLayout: false + property bool forceVerticalLayout: false + + readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) + readonly property real widgetSpacing: { + const baseSpacing = noBackground ? 2 : Theme.spacingXS; + const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return baseSpacing + (outlineThickness * 2); + } + + property var centerWidgets: [] + property int totalWidgets: 0 + property real totalSize: 0 + + function updateLayout() { + if (SettingsData.centeringMode === "geometric") { + applyGeometricLayout(); + } else { + applyIndexLayout(); + } + } + + function applyGeometricLayout() { + if ((isVertical ? height : width) <= 0 || !visible) + return; + + centerWidgets = []; + totalWidgets = 0; + totalSize = 0; + + for (var i = 0; i < centerRepeater.count; i++) { + const loader = centerRepeater.itemAt(i); + if (loader && loader.active && loader.item) { + centerWidgets.push(loader.item); + totalWidgets++; + totalSize += isVertical ? loader.item.height : loader.item.width; + } + } + + if (totalWidgets === 0) + return; + + if (totalWidgets > 1) + totalSize += widgetSpacing * (totalWidgets - 1); + + positionWidgetsGeometric(); + } + + function positionWidgetsGeometric() { + const parentLength = isVertical ? height : width; + const parentCenter = parentLength / 2; + let currentPos = parentCenter - (totalSize / 2); + + centerWidgets.forEach(widget => { + if (isVertical) { + widget.anchors.verticalCenter = undefined; + widget.y = currentPos; + } else { + widget.anchors.horizontalCenter = undefined; + widget.x = currentPos; + } + const widgetSize = isVertical ? widget.height : widget.width; + currentPos += widgetSize + widgetSpacing; + }); + } + + function applyIndexLayout() { + if ((isVertical ? height : width) <= 0 || !visible) + return; + + centerWidgets = []; + totalWidgets = 0; + totalSize = 0; + + let configuredMiddleWidget = null; + let configuredLeftWidget = null; + let configuredRightWidget = null; + + const configuredWidgets = centerRepeater.count; + const isOddConfigured = configuredWidgets % 2 === 1; + const configuredMiddlePos = Math.floor(configuredWidgets / 2); + const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1); + const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2); + + for (var i = 0; i < centerRepeater.count; i++) { + const wrapper = centerRepeater.itemAt(i); + if (!wrapper) + continue; + + if (isOddConfigured && i === configuredMiddlePos && wrapper.active && wrapper.item) + configuredMiddleWidget = wrapper.item; + if (!isOddConfigured && i === configuredLeftPos && wrapper.active && wrapper.item) + configuredLeftWidget = wrapper.item; + if (!isOddConfigured && i === configuredRightPos && wrapper.active && wrapper.item) + configuredRightWidget = wrapper.item; + + if (wrapper.active && wrapper.item) { + centerWidgets.push(wrapper.item); + totalWidgets++; + totalSize += isVertical ? wrapper.item.height : wrapper.item.width; + } + } + + if (totalWidgets === 0) + return; + + if (totalWidgets > 1) + totalSize += widgetSpacing * (totalWidgets - 1); + + positionWidgetsByIndex(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget); + } + + function positionWidgetsByIndex(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget) { + const parentCenter = (isVertical ? height : width) / 2; + const isOddConfigured = configuredWidgets % 2 === 1; + + centerWidgets.forEach(widget => { + if (isVertical) + widget.anchors.verticalCenter = undefined; + else + widget.anchors.horizontalCenter = undefined; + }); + + if (isOddConfigured && configuredMiddleWidget) { + const middleWidget = configuredMiddleWidget; + const middleIndex = centerWidgets.indexOf(middleWidget); + const middleSize = isVertical ? middleWidget.height : middleWidget.width; + + if (isVertical) + middleWidget.y = parentCenter - (middleSize / 2); + else + middleWidget.x = parentCenter - (middleSize / 2); + + let currentPos = isVertical ? middleWidget.y : middleWidget.x; + for (var i = middleIndex - 1; i >= 0; i--) { + const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width; + currentPos -= (widgetSpacing + size); + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + } + + currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize; + for (var i = middleIndex + 1; i < totalWidgets; i++) { + currentPos += widgetSpacing; + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width; + } + return; + } + + if (totalWidgets === 1) { + const widget = centerWidgets[0]; + const size = isVertical ? widget.height : widget.width; + if (isVertical) + widget.y = parentCenter - (size / 2); + else + widget.x = parentCenter - (size / 2); + return; + } + + if (!configuredLeftWidget || !configuredRightWidget) { + if (totalWidgets % 2 === 1) { + const middleIndex = Math.floor(totalWidgets / 2); + const middleWidget = centerWidgets[middleIndex]; + + if (!middleWidget) + return; + + const middleSize = isVertical ? middleWidget.height : middleWidget.width; + + if (isVertical) + middleWidget.y = parentCenter - (middleSize / 2); + else + middleWidget.x = parentCenter - (middleSize / 2); + + let currentPos = isVertical ? middleWidget.y : middleWidget.x; + for (var i = middleIndex - 1; i >= 0; i--) { + const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width; + currentPos -= (widgetSpacing + size); + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + } + + currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize; + for (var i = middleIndex + 1; i < totalWidgets; i++) { + currentPos += widgetSpacing; + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width; + } + } else { + const leftIndex = (totalWidgets / 2) - 1; + const rightIndex = totalWidgets / 2; + const fallbackLeft = centerWidgets[leftIndex]; + const fallbackRight = centerWidgets[rightIndex]; + + if (!fallbackLeft || !fallbackRight) + return; + + const halfSpacing = widgetSpacing / 2; + const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width; + + if (isVertical) { + fallbackLeft.y = parentCenter - halfSpacing - leftSize; + fallbackRight.y = parentCenter + halfSpacing; + } else { + fallbackLeft.x = parentCenter - halfSpacing - leftSize; + fallbackRight.x = parentCenter + halfSpacing; + } + + let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x; + for (var i = leftIndex - 1; i >= 0; i--) { + const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width; + currentPos -= (widgetSpacing + size); + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + } + + currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width); + for (var i = rightIndex + 1; i < totalWidgets; i++) { + currentPos += widgetSpacing; + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width; + } + } + return; + } + + const leftWidget = configuredLeftWidget; + const rightWidget = configuredRightWidget; + const leftIndex = centerWidgets.indexOf(leftWidget); + const rightIndex = centerWidgets.indexOf(rightWidget); + const halfSpacing = widgetSpacing / 2; + const leftSize = isVertical ? leftWidget.height : leftWidget.width; + + if (isVertical) { + leftWidget.y = parentCenter - halfSpacing - leftSize; + rightWidget.y = parentCenter + halfSpacing; + } else { + leftWidget.x = parentCenter - halfSpacing - leftSize; + rightWidget.x = parentCenter + halfSpacing; + } + + let currentPos = isVertical ? leftWidget.y : leftWidget.x; + for (var i = leftIndex - 1; i >= 0; i--) { + const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width; + currentPos -= (widgetSpacing + size); + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + } + + currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width); + for (var i = rightIndex + 1; i < totalWidgets; i++) { + currentPos += widgetSpacing; + if (isVertical) + centerWidgets[i].y = currentPos; + else + centerWidgets[i].x = currentPos; + currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width; + } + } + + height: parent.height + width: parent.width + anchors.centerIn: parent + + implicitWidth: isVertical ? widgetThickness : totalSize + implicitHeight: isVertical ? totalSize : widgetThickness + + Timer { + id: layoutTimer + interval: 0 + repeat: false + onTriggered: root.updateLayout() + } + + Component.onCompleted: layoutTimer.restart() + + onWidthChanged: { + if (width > 0) + layoutTimer.restart(); + } + + onHeightChanged: { + if (height > 0) + layoutTimer.restart(); + } + + onVisibleChanged: { + if (visible && (isVertical ? height : width) > 0) + layoutTimer.restart(); + } + + Repeater { + id: centerRepeater + model: root.widgetsModel + + onCountChanged: layoutTimer.restart() + + Item { + property var itemData: modelData + readonly property real itemSpacing: root.widgetSpacing + + width: root.isVertical ? root.width : (widgetLoader.item ? widgetLoader.item.width : 0) + height: widgetLoader.item ? widgetLoader.item.height : 0 + + readonly property bool active: widgetLoader.active + readonly property var item: widgetLoader.item + + WidgetHost { + id: widgetLoader + + anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined + anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined + + widgetId: itemData.widgetId + widgetData: itemData + spacerSize: itemData.size || 20 + components: root.components + isInColumn: root.isVertical + axis: root.axis + section: "center" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + barSpacing: root.barSpacing + barConfig: root.barConfig + blurBarWindow: root.blurBarWindow + isFirst: index === 0 + isLast: index === centerRepeater.count - 1 + sectionSpacing: parent.itemSpacing + isLeftBarEdge: false + isRightBarEdge: false + isTopBarEdge: false + isBottomBarEdge: false + + onContentItemReady: contentItem => { + contentItem.widthChanged.connect(() => layoutTimer.restart()); + contentItem.heightChanged.connect(() => layoutTimer.restart()); + layoutTimer.restart(); + } + + onActiveChanged: layoutTimer.restart() + } + } + } + + Connections { + target: SettingsData + function onCenteringModeChanged() { + layoutTimer.restart(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBar.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBar.qml new file mode 100644 index 0000000..392bf9b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBar.qml @@ -0,0 +1,182 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.I3 +import qs.Common +import qs.Services + +Item { + id: root + + required property var barConfig + + signal colorPickerRequested + signal barReady(var barConfig) + + property alias barVariants: barVariants + property var hyprlandOverviewLoader: null + property bool systemTrayMenuOpen: false + + property alias leftWidgetsModel: leftWidgetsModel + property alias centerWidgetsModel: centerWidgetsModel + property alias rightWidgetsModel: rightWidgetsModel + + property string _leftWidgetsJson: { + root.barConfig; + const leftWidgets = root.barConfig?.leftWidgets || []; + const mapped = leftWidgets.map((w, index) => { + if (typeof w === "string") { + return { + widgetId: w, + id: w + "_" + index, + enabled: true + }; + } else { + const obj = Object.assign({}, w); + obj.widgetId = w.id || w.widgetId; + obj.id = (w.id || w.widgetId) + "_" + index; + obj.enabled = w.enabled !== false; + return obj; + } + }); + return JSON.stringify(mapped); + } + + property string _centerWidgetsJson: { + root.barConfig; + const centerWidgets = root.barConfig?.centerWidgets || []; + const mapped = centerWidgets.map((w, index) => { + if (typeof w === "string") { + return { + widgetId: w, + id: w + "_" + index, + enabled: true + }; + } else { + const obj = Object.assign({}, w); + obj.widgetId = w.id || w.widgetId; + obj.id = (w.id || w.widgetId) + "_" + index; + obj.enabled = w.enabled !== false; + return obj; + } + }); + return JSON.stringify(mapped); + } + + property string _rightWidgetsJson: { + root.barConfig; + const rightWidgets = root.barConfig?.rightWidgets || []; + const mapped = rightWidgets.map((w, index) => { + if (typeof w === "string") { + return { + widgetId: w, + id: w + "_" + index, + enabled: true + }; + } else { + const obj = Object.assign({}, w); + obj.widgetId = w.id || w.widgetId; + obj.id = (w.id || w.widgetId) + "_" + index; + obj.enabled = w.enabled !== false; + return obj; + } + }); + return JSON.stringify(mapped); + } + + ScriptModel { + id: leftWidgetsModel + values: JSON.parse(root._leftWidgetsJson) + } + + ScriptModel { + id: centerWidgetsModel + values: JSON.parse(root._centerWidgetsJson) + } + + ScriptModel { + id: rightWidgetsModel + values: JSON.parse(root._rightWidgetsJson) + } + + function triggerControlCenterOnFocusedScreen() { + let focusedScreenName = ""; + if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) { + focusedScreenName = Hyprland.focusedWorkspace.monitor.name; + } else if (CompositorService.isNiri && NiriService.currentOutput) { + focusedScreenName = NiriService.currentOutput; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + focusedScreenName = focusedWs?.monitor?.name || ""; + } else if (CompositorService.isDwl && DwlService.activeOutput) { + focusedScreenName = DwlService.activeOutput; + } + + if (!focusedScreenName && barVariants.instances.length > 0) { + const firstBar = barVariants.instances[0]; + firstBar.triggerControlCenter(); + return true; + } + + for (var i = 0; i < barVariants.instances.length; i++) { + const barInstance = barVariants.instances[i]; + if (barInstance.modelData && barInstance.modelData.name === focusedScreenName) { + barInstance.triggerControlCenter(); + return true; + } + } + return false; + } + + function triggerWallpaperBrowserOnFocusedScreen() { + let focusedScreenName = ""; + if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) { + focusedScreenName = Hyprland.focusedWorkspace.monitor.name; + } else if (CompositorService.isNiri && NiriService.currentOutput) { + focusedScreenName = NiriService.currentOutput; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + focusedScreenName = focusedWs?.monitor?.name || ""; + } else if (CompositorService.isDwl && DwlService.activeOutput) { + focusedScreenName = DwlService.activeOutput; + } + + if (!focusedScreenName && barVariants.instances.length > 0) { + const firstBar = barVariants.instances[0]; + firstBar.triggerWallpaperBrowser(); + return true; + } + + for (var i = 0; i < barVariants.instances.length; i++) { + const barInstance = barVariants.instances[i]; + if (barInstance.modelData && barInstance.modelData.name === focusedScreenName) { + barInstance.triggerWallpaperBrowser(); + return true; + } + } + return false; + } + + Variants { + id: barVariants + model: { + const prefs = root.barConfig?.screenPreferences || ["all"]; + if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) { + return Quickshell.screens; + } + const filtered = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs)); + if (filtered.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) { + return Quickshell.screens; + } + return filtered; + } + + delegate: DankBarWindow { + rootWindow: root + barConfig: root.barConfig + leftWidgetsModel: root.leftWidgetsModel + centerWidgetsModel: root.centerWidgetsModel + rightWidgetsModel: root.rightWidgetsModel + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarContent.qml new file mode 100644 index 0000000..0c977bf --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarContent.qml @@ -0,0 +1,1459 @@ +import QtQuick +import Quickshell.Hyprland +import Quickshell.I3 +import Quickshell.Services.SystemTray +import Quickshell.Wayland +import qs.Common +import qs.Modules.DankBar.Widgets +import qs.Services + +Item { + id: topBarContent + + required property var barWindow + required property var rootWindow + required property var barConfig + + readonly property var blurBarWindow: barWindow + + property var leftWidgetsModel + property var centerWidgetsModel + property var rightWidgetsModel + + readonly property real innerPadding: barConfig?.innerPadding ?? 4 + readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0 + + property alias hLeftSection: hLeftSection + property alias hCenterSection: hCenterSection + property alias hRightSection: hRightSection + property alias vLeftSection: vLeftSection + property alias vCenterSection: vCenterSection + property alias vRightSection: vRightSection + + anchors.fill: parent + anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0 + anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0 + clip: false + + property int componentMapRevision: 0 + + function updateComponentMap() { + componentMapRevision++; + } + + readonly property var sortedToplevels: { + return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, barWindow.screenName); + } + + function getRealWorkspaces() { + if (CompositorService.isNiri) { + const fallbackWorkspaces = [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { + const currentWorkspaces = NiriService.getCurrentOutputWorkspaces(); + return currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces; + } + const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName); + return workspaces.length > 0 ? workspaces : fallbackWorkspaces; + } else if (CompositorService.isHyprland) { + const workspaces = Hyprland.workspaces?.values || []; + + if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { + const sorted = workspaces.slice().sort((a, b) => a.id - b.id); + const filtered = sorted.filter(ws => ws.id > -1); + return filtered.length > 0 ? filtered : [ + { + "id": 1, + "name": "1" + } + ]; + } + + const monitorWorkspaces = workspaces.filter(ws => { + return ws.lastIpcObject && ws.lastIpcObject.monitor === barWindow.screenName && ws.id > -1; + }); + + if (monitorWorkspaces.length === 0) { + return [ + { + "id": 1, + "name": "1" + } + ]; + } + + return monitorWorkspaces.sort((a, b) => a.id - b.id); + } else if (CompositorService.isDwl) { + if (!DwlService.dwlAvailable) { + return [0]; + } + if (SettingsData.dwlShowAllTags) { + return Array.from({ + length: DwlService.tagCount + }, (_, i) => i); + } + return DwlService.getVisibleTags(barWindow.screenName); + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const workspaces = I3.workspaces?.values || []; + if (workspaces.length === 0) + return [ + { + "num": 1 + } + ]; + + if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { + return workspaces.slice().sort((a, b) => a.num - b.num); + } + + const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === barWindow.screenName); + return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [ + { + "num": 1 + } + ]; + } + return [1]; + } + + function getCurrentWorkspace() { + if (CompositorService.isNiri) { + if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { + return NiriService.getCurrentWorkspaceNumber(); + } + const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active); + return activeWs ? activeWs.idx : 1; + } else if (CompositorService.isHyprland) { + const monitors = Hyprland.monitors?.values || []; + const currentMonitor = monitors.find(monitor => monitor.name === barWindow.screenName); + return currentMonitor?.activeWorkspace?.id ?? 1; + } else if (CompositorService.isDwl) { + if (!DwlService.dwlAvailable) + return 0; + const outputState = DwlService.getOutputState(barWindow.screenName); + if (!outputState || !outputState.tags) + return 0; + const activeTags = DwlService.getActiveTags(barWindow.screenName); + return activeTags.length > 0 ? activeTags[0] : 0; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + if (!barWindow.screenName || SettingsData.workspaceFollowFocus) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + + const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === barWindow.screenName && ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + return 1; + } + + function switchWorkspace(direction) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + if (CompositorService.isNiri) { + const currentWs = getCurrentWorkspace(); + const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === currentWs); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex !== validIndex) { + const nextWorkspace = realWorkspaces[nextIndex]; + if (!nextWorkspace || nextWorkspace.idx === undefined) { + return; + } + NiriService.switchToWorkspace(nextWorkspace.idx); + } + } else if (CompositorService.isHyprland) { + const currentWs = getCurrentWorkspace(); + const currentIndex = realWorkspaces.findIndex(ws => ws.id === currentWs); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex !== validIndex) { + Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`); + } + } else if (CompositorService.isDwl) { + const currentTag = getCurrentWorkspace(); + const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex !== validIndex) { + DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]); + } + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const currentWs = getCurrentWorkspace(); + const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex !== validIndex) { + try { + I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`); + } catch (_) {} + } + } + } + + function switchApp(deltaY) { + const windows = sortedToplevels; + if (windows.length < 2) { + return; + } + let currentIndex = -1; + for (let i = 0; i < windows.length; i++) { + if (windows[i].activated) { + currentIndex = i; + break; + } + } + let nextIndex; + if (deltaY < 0) { + if (currentIndex === -1) { + nextIndex = 0; + } else { + nextIndex = currentIndex + 1; + } + } else { + if (currentIndex === -1) { + nextIndex = windows.length - 1; + } else { + nextIndex = currentIndex - 1; + } + } + const nextWindow = windows[nextIndex]; + if (nextWindow) { + nextWindow.activate(); + } + } + + readonly property int availableWidth: width + readonly property int launcherButtonWidth: 40 + readonly property int workspaceSwitcherWidth: 120 + readonly property int focusedAppMaxWidth: 456 + readonly property int estimatedLeftSectionWidth: launcherButtonWidth + workspaceSwitcherWidth + focusedAppMaxWidth + (Theme.spacingXS * 2) + readonly property int rightSectionWidth: 200 + readonly property int clockWidth: 120 + readonly property int mediaMaxWidth: 280 + readonly property int weatherWidth: 80 + readonly property bool validLayout: availableWidth > 100 && estimatedLeftSectionWidth > 0 && rightSectionWidth > 0 + readonly property int clockLeftEdge: (availableWidth - clockWidth) / 2 + readonly property int clockRightEdge: clockLeftEdge + clockWidth + readonly property int leftSectionRightEdge: estimatedLeftSectionWidth + readonly property int mediaLeftEdge: clockLeftEdge - mediaMaxWidth - Theme.spacingS + readonly property int rightSectionLeftEdge: availableWidth - rightSectionWidth + readonly property int leftToClockGap: Math.max(0, clockLeftEdge - leftSectionRightEdge) + readonly property int leftToMediaGap: mediaMaxWidth > 0 ? Math.max(0, mediaLeftEdge - leftSectionRightEdge) : leftToClockGap + readonly property int mediaToClockGap: mediaMaxWidth > 0 ? Theme.spacingS : 0 + readonly property int clockToRightGap: validLayout ? Math.max(0, rightSectionLeftEdge - clockRightEdge) : 1000 + readonly property bool spacingTight: !barWindow.isVertical && validLayout && (leftToMediaGap < 150 || clockToRightGap < 100) + readonly property bool overlapping: !barWindow.isVertical && validLayout && (leftToMediaGap < 100 || clockToRightGap < 50) + + function getWidgetEnabled(enabled) { + return enabled !== false; + } + + function getWidgetSection(parentItem) { + let current = parentItem; + while (current) { + if (current.objectName === "leftSection") { + return "left"; + } + if (current.objectName === "centerSection") { + return "center"; + } + if (current.objectName === "rightSection") { + return "right"; + } + current = current.parent; + } + return "left"; + } + + readonly property var widgetVisibility: ({ + "cpuUsage": DgopService.dgopAvailable, + "memUsage": DgopService.dgopAvailable, + "cpuTemp": DgopService.dgopAvailable, + "gpuTemp": DgopService.dgopAvailable, + "network_speed_monitor": DgopService.dgopAvailable + }) + + function getWidgetVisible(widgetId) { + return widgetVisibility[widgetId] ?? true; + } + + readonly property var componentMap: { + componentMapRevision; + + let baseMap = { + "launcherButton": launcherButtonComponent, + "workspaceSwitcher": workspaceSwitcherComponent, + "focusedWindow": focusedWindowComponent, + "runningApps": runningAppsComponent, + "appsDock": appsDockComponent, + "clock": clockComponent, + "music": mediaComponent, + "weather": weatherComponent, + "systemTray": systemTrayComponent, + "privacyIndicator": privacyIndicatorComponent, + "clipboard": clipboardComponent, + "cpuUsage": cpuUsageComponent, + "memUsage": memUsageComponent, + "diskUsage": diskUsageComponent, + "cpuTemp": cpuTempComponent, + "gpuTemp": gpuTempComponent, + "notificationButton": notificationButtonComponent, + "battery": batteryComponent, + "layout": layoutComponent, + "controlCenterButton": controlCenterButtonComponent, + "capsLockIndicator": capsLockIndicatorComponent, + "idleInhibitor": idleInhibitorComponent, + "spacer": spacerComponent, + "separator": separatorComponent, + "network_speed_monitor": networkComponent, + "keyboard_layout_name": keyboardLayoutNameComponent, + "vpn": vpnComponent, + "notepadButton": notepadButtonComponent, + "colorPicker": colorPickerComponent, + "systemUpdate": systemUpdateComponent, + "powerMenuButton": powerMenuButtonComponent + }; + + let pluginMap = PluginService.getWidgetComponents(); + return Object.assign(baseMap, pluginMap); + } + + function getWidgetComponent(widgetId) { + return componentMap[widgetId] || null; + } + + readonly property var allComponents: ({ + "launcherButtonComponent": launcherButtonComponent, + "workspaceSwitcherComponent": workspaceSwitcherComponent, + "focusedWindowComponent": focusedWindowComponent, + "runningAppsComponent": runningAppsComponent, + "appsDockComponent": appsDockComponent, + "clockComponent": clockComponent, + "mediaComponent": mediaComponent, + "weatherComponent": weatherComponent, + "systemTrayComponent": systemTrayComponent, + "privacyIndicatorComponent": privacyIndicatorComponent, + "clipboardComponent": clipboardComponent, + "cpuUsageComponent": cpuUsageComponent, + "memUsageComponent": memUsageComponent, + "diskUsageComponent": diskUsageComponent, + "cpuTempComponent": cpuTempComponent, + "gpuTempComponent": gpuTempComponent, + "notificationButtonComponent": notificationButtonComponent, + "batteryComponent": batteryComponent, + "layoutComponent": layoutComponent, + "controlCenterButtonComponent": controlCenterButtonComponent, + "capsLockIndicatorComponent": capsLockIndicatorComponent, + "idleInhibitorComponent": idleInhibitorComponent, + "spacerComponent": spacerComponent, + "separatorComponent": separatorComponent, + "networkComponent": networkComponent, + "keyboardLayoutNameComponent": keyboardLayoutNameComponent, + "vpnComponent": vpnComponent, + "notepadButtonComponent": notepadButtonComponent, + "colorPickerComponent": colorPickerComponent, + "systemUpdateComponent": systemUpdateComponent, + "powerMenuButtonComponent": powerMenuButtonComponent + }) + + Item { + id: stackContainer + anchors.fill: parent + + Item { + id: horizontalStack + anchors.fill: parent + visible: !barWindow.axis.isVertical + + LeftSection { + id: hLeftSection + objectName: "leftSection" + overrideAxisLayout: true + forceVerticalLayout: false + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.leftWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: hLeftSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: hLeftSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + + RightSection { + id: hRightSection + objectName: "rightSection" + overrideAxisLayout: true + forceVerticalLayout: false + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.rightWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: hRightSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: hRightSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + + CenterSection { + id: hCenterSection + objectName: "centerSection" + overrideAxisLayout: true + forceVerticalLayout: false + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.centerWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: hCenterSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: hCenterSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + } + + Item { + id: verticalStack + anchors.fill: parent + visible: barWindow.axis.isVertical + + LeftSection { + id: vLeftSection + objectName: "leftSection" + overrideAxisLayout: true + forceVerticalLayout: true + width: parent.width + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.leftWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: vLeftSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: vLeftSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + + CenterSection { + id: vCenterSection + objectName: "centerSection" + overrideAxisLayout: true + forceVerticalLayout: true + width: parent.width + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.centerWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: vCenterSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: vCenterSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + + RightSection { + id: vRightSection + objectName: "rightSection" + overrideAxisLayout: true + forceVerticalLayout: true + width: parent.width + height: implicitHeight + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + axis: barWindow.axis + widgetsModel: topBarContent.rightWidgetsModel + components: topBarContent.allComponents + noBackground: barConfig?.noBackground ?? false + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + } + + Binding { + target: vRightSection + property: "barConfig" + value: topBarContent.barConfig + restoreMode: Binding.RestoreNone + } + Binding { + target: vRightSection + property: "blurBarWindow" + value: topBarContent.blurBarWindow + restoreMode: Binding.RestoreNone + } + } + } + + Component { + id: clipboardComponent + + ClipboardButton { + id: clipboardWidget + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) + parentScreen: barWindow.screen + popoutTarget: clipboardHistoryPopoutLoader.item ?? null + + function openClipboardPopout(initialTab) { + clipboardHistoryPopoutLoader.active = true; + if (!clipboardHistoryPopoutLoader.item) { + return; + } + const popout = clipboardHistoryPopoutLoader.item; + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (popout.setBarContext) { + popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (popout.setTriggerPosition) { + const globalPos = clipboardWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clipboardWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + popout.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + if (initialTab) { + popout.activeTab = initialTab; + } + PopoutManager.requestPopout(popout, undefined, "clipboard"); + } + + onClipboardClicked: openClipboardPopout("recents") + + onShowSavedItemsRequested: openClipboardPopout("saved") + + onClearAllRequested: { + clipboardHistoryPopoutLoader.active = true; + const popout = clipboardHistoryPopoutLoader.item; + if (!popout?.confirmDialog) { + return; + } + const hasPinned = popout.pinnedCount > 0; + const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(popout.pinnedCount) : I18n.tr("This will permanently delete all clipboard history."); + popout.confirmDialog.show(I18n.tr("Clear History?"), message, function () { + if (popout && typeof popout.clearAll === "function") { + popout.clearAll(); + } + }, function () {}); + } + } + } + + Component { + id: powerMenuButtonComponent + + PowerMenuButton { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) + parentScreen: barWindow.screen + onClicked: { + if (powerMenuModalLoader) { + powerMenuModalLoader.active = true; + if (powerMenuModalLoader.item) { + powerMenuModalLoader.item.openCentered(); + } + } + } + } + } + + Component { + id: launcherButtonComponent + + LauncherButton { + id: launcherButton + isActive: false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + section: topBarContent.getWidgetSection(parent) + popoutTarget: appDrawerLoader.item + parentScreen: barWindow.screen + hyprlandOverviewLoader: barWindow ? barWindow.hyprlandOverviewLoader : null + + function _preparePopout() { + appDrawerLoader.active = true; + if (!appDrawerLoader.item) + return false; + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (appDrawerLoader.item.setBarContext) + appDrawerLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + if (appDrawerLoader.item.setTriggerPosition) { + const globalPos = launcherButton.visualContent.mapToItem(null, 0, 0); + const currentScreen = barWindow.screen; + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + appDrawerLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + return true; + } + + function openWithMode(mode) { + if (!_preparePopout()) + return; + appDrawerLoader.item.openWithMode(mode); + } + + function toggleWithMode(mode) { + if (!_preparePopout()) + return; + appDrawerLoader.item.toggleWithMode(mode); + } + + function openWithQuery(query) { + if (!_preparePopout()) + return; + appDrawerLoader.item.openWithQuery(query); + } + + function toggleWithQuery(query) { + if (!_preparePopout()) + return; + appDrawerLoader.item.toggleWithQuery(query); + } + + onClicked: { + if (!_preparePopout()) + return; + PopoutManager.requestPopout(appDrawerLoader.item, undefined, "appDrawer"); + } + } + } + + Component { + id: workspaceSwitcherComponent + + WorkspaceSwitcher { + axis: barWindow.axis + screenName: barWindow.screenName + widgetHeight: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + parentScreen: barWindow.screen + hyprlandOverviewLoader: barWindow ? barWindow.hyprlandOverviewLoader : null + } + } + + Component { + id: focusedWindowComponent + + FocusedApp { + axis: barWindow.axis + availableWidth: topBarContent.leftToMediaGap + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + barConfig: topBarContent.barConfig + isAutoHideBar: topBarContent.barConfig?.autoHide ?? false + parentScreen: barWindow.screen + } + } + + Component { + id: runningAppsComponent + + RunningApps { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + section: topBarContent.getWidgetSection(parent) + parentScreen: barWindow.screen + topBar: topBarContent + barConfig: topBarContent.barConfig + isAutoHideBar: topBarContent.barConfig?.autoHide ?? false + } + } + + Component { + id: appsDockComponent + + AppsDock { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + barSpacing: barConfig?.spacing ?? 4 + section: topBarContent.getWidgetSection(parent) + parentScreen: barWindow.screen + topBar: topBarContent + barConfig: topBarContent.barConfig + isAutoHideBar: topBarContent.barConfig?.autoHide ?? false + } + } + + Component { + id: clockComponent + + Clock { + axis: barWindow.axis + compactMode: topBarContent.overlapping + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "center" + popoutTarget: dankDashPopoutLoader.item ?? null + parentScreen: barWindow.screen + + Component.onCompleted: { + barWindow.clockButtonRef = this; + } + + Component.onDestruction: { + if (barWindow.clockButtonRef === this) { + barWindow.clockButtonRef = null; + } + } + + onClockClicked: { + dankDashPopoutLoader.active = true; + if (dankDashPopoutLoader.item) { + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (dankDashPopoutLoader.item.setBarContext) { + dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (dankDashPopoutLoader.item.setTriggerPosition) { + let triggerPos, triggerWidth; + if (section === "center") { + const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection; + if (centerSection) { + if (barWindow.isVertical) { + const centerY = centerSection.height / 2; + const centerGlobalPos = centerSection.mapToItem(null, 0, centerY); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.height; + } else { + const centerGlobalPos = centerSection.mapToItem(null, 0, 0); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.width; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } else { + dankDashPopoutLoader.item.triggerScreen = barWindow.screen; + } + PopoutManager.requestPopout(dankDashPopoutLoader.item, 0, (effectiveBarConfig?.id ?? "default") + "-" + section + "-0"); + } + } + } + } + + Component { + id: mediaComponent + + Media { + axis: barWindow.axis + compactMode: topBarContent.spacingTight || topBarContent.overlapping + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "center" + popoutTarget: dankDashPopoutLoader.item ?? null + parentScreen: barWindow.screen + onClicked: { + dankDashPopoutLoader.active = true; + if (dankDashPopoutLoader.item) { + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (dankDashPopoutLoader.item.setBarContext) { + dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (dankDashPopoutLoader.item.setTriggerPosition) { + let triggerPos, triggerWidth; + if (section === "center") { + const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection; + if (centerSection) { + if (barWindow.isVertical) { + const centerY = centerSection.height / 2; + const centerGlobalPos = centerSection.mapToItem(null, 0, centerY); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.height; + } else { + const centerGlobalPos = centerSection.mapToItem(null, 0, 0); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.width; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } else { + dankDashPopoutLoader.item.triggerScreen = barWindow.screen; + } + PopoutManager.requestPopout(dankDashPopoutLoader.item, 1, (effectiveBarConfig?.id ?? "default") + "-" + section + "-1"); + } + } + } + } + + Component { + id: weatherComponent + + Weather { + axis: barWindow.axis + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "center" + popoutTarget: dankDashPopoutLoader.item ?? null + parentScreen: barWindow.screen + onClicked: { + dankDashPopoutLoader.active = true; + if (dankDashPopoutLoader.item) { + const effectiveBarConfig = topBarContent.barConfig; + // Calculate barPosition from axis.edge + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (dankDashPopoutLoader.item.setBarContext) { + dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (dankDashPopoutLoader.item.setTriggerPosition) { + // For center section widgets, use center section bounds for DankDash centering + let triggerPos, triggerWidth; + if (section === "center") { + const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection; + if (centerSection) { + // For vertical bars, use center Y of section; for horizontal, use left edge + if (barWindow.isVertical) { + const centerY = centerSection.height / 2; + const centerGlobalPos = centerSection.mapToItem(null, 0, centerY); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.height; + } else { + // For horizontal bars, use left edge (DankPopout will center it) + const centerGlobalPos = centerSection.mapToItem(null, 0, 0); + triggerPos = centerGlobalPos; + triggerWidth = centerSection.width; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + } else { + triggerPos = visualContent.mapToItem(null, 0, 0); + triggerWidth = visualWidth; + } + const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } else { + dankDashPopoutLoader.item.triggerScreen = barWindow.screen; + } + PopoutManager.requestPopout(dankDashPopoutLoader.item, 3, (effectiveBarConfig?.id ?? "default") + "-" + section + "-3"); + } + } + } + } + + Component { + id: systemTrayComponent + + SystemTrayBar { + parentWindow: barWindow + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + 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 + } + } + + Component { + id: privacyIndicatorComponent + + PrivacyIndicator { + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: barWindow.screen + } + } + + Component { + id: cpuUsageComponent + + CpuMonitor { + id: cpuWidget + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: processListPopoutLoader.item ?? null + parentScreen: barWindow.screen + widgetData: parent.widgetData + onCpuClicked: { + processListPopoutLoader.active = true; + if (!processListPopoutLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (processListPopoutLoader.item.setBarContext) { + processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (processListPopoutLoader.item.setTriggerPosition) { + const globalPos = cpuWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, cpuWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "cpu"); + } + } + } + + Component { + id: memUsageComponent + + RamMonitor { + id: ramWidget + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: processListPopoutLoader.item ?? null + parentScreen: barWindow.screen + widgetData: parent.widgetData + onRamClicked: { + processListPopoutLoader.active = true; + if (!processListPopoutLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (processListPopoutLoader.item.setBarContext) { + processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (processListPopoutLoader.item.setTriggerPosition) { + const globalPos = ramWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, ramWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "memory"); + } + } + } + + Component { + id: diskUsageComponent + + DiskUsage { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + widgetData: parent.widgetData + parentScreen: barWindow.screen + barConfig: topBarContent.barConfig + isAutoHideBar: topBarContent.barConfig?.autoHide ?? false + } + } + + Component { + id: cpuTempComponent + + CpuTemperature { + id: cpuTempWidget + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: processListPopoutLoader.item ?? null + parentScreen: barWindow.screen + widgetData: parent.widgetData + onCpuTempClicked: { + processListPopoutLoader.active = true; + if (!processListPopoutLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (processListPopoutLoader.item.setBarContext) { + processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (processListPopoutLoader.item.setTriggerPosition) { + const globalPos = cpuTempWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, cpuTempWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "cpu_temp"); + } + } + } + + Component { + id: gpuTempComponent + + GpuTemperature { + id: gpuTempWidget + barThickness: barWindow.effectiveBarThickness + widgetThickness: barWindow.widgetThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: processListPopoutLoader.item ?? null + parentScreen: barWindow.screen + widgetData: parent.widgetData + onGpuTempClicked: { + processListPopoutLoader.active = true; + if (!processListPopoutLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (processListPopoutLoader.item.setBarContext) { + processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (processListPopoutLoader.item.setTriggerPosition) { + const globalPos = gpuTempWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, gpuTempWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "gpu_temp"); + } + } + } + + Component { + id: networkComponent + + NetworkMonitor {} + } + + Component { + id: notificationButtonComponent + + NotificationCenterButton { + id: notificationButton + hasUnread: barWindow.notificationCount > 0 + isActive: notificationCenterLoader.item ? notificationCenterLoader.item.shouldBeVisible : false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: notificationCenterLoader.item ?? null + parentScreen: barWindow.screen + onClicked: { + notificationCenterLoader.active = true; + if (!notificationCenterLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (notificationCenterLoader.item.setBarContext) { + notificationCenterLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (notificationCenterLoader.item.setTriggerPosition) { + const globalPos = notificationButton.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, notificationButton.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + notificationCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(notificationCenterLoader.item, undefined, "notifications"); + } + } + } + + Component { + id: batteryComponent + + Battery { + id: batteryWidget + batteryPopupVisible: batteryPopoutLoader.item ? batteryPopoutLoader.item.shouldBeVisible : false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + barSpacing: barConfig?.spacing ?? 4 + barConfig: topBarContent.barConfig + popoutTarget: batteryPopoutLoader.item ?? null + parentScreen: barWindow.screen + onToggleBatteryPopup: { + batteryPopoutLoader.active = true; + if (!batteryPopoutLoader.item) { + return; + } + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (batteryPopoutLoader.item.setBarContext) { + batteryPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (batteryPopoutLoader.item.setTriggerPosition) { + const globalPos = batteryWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, batteryWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + batteryPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(batteryPopoutLoader.item, undefined, "battery"); + } + } + } + + Component { + id: layoutComponent + + DWLLayout { + id: layoutWidget + layoutPopupVisible: layoutPopoutLoader.item ? layoutPopoutLoader.item.shouldBeVisible : false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "center" + popoutTarget: layoutPopoutLoader.item ?? null + parentScreen: barWindow.screen + onToggleLayoutPopup: { + layoutPopoutLoader.active = true; + if (!layoutPopoutLoader.item) + return; + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + + if (layoutPopoutLoader.item.setTriggerPosition) { + const globalPos = layoutWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, layoutWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "center"; + layoutPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + + PopoutManager.requestPopout(layoutPopoutLoader.item, undefined, "layout"); + } + } + } + + Component { + id: vpnComponent + + Vpn { + id: vpnWidget + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + barSpacing: barConfig?.spacing ?? 4 + barConfig: topBarContent.barConfig + isAutoHideBar: topBarContent.barConfig?.autoHide ?? false + popoutTarget: vpnPopoutLoader.item ?? null + parentScreen: barWindow.screen + onToggleVpnPopup: { + vpnPopoutLoader.active = true; + if (!vpnPopoutLoader.item) + return; + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + + if (vpnPopoutLoader.item.setBarContext) { + vpnPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + + if (vpnPopoutLoader.item.setTriggerPosition) { + const globalPos = vpnWidget.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, vpnWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const widgetSection = topBarContent.getWidgetSection(parent) || "right"; + vpnPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + + PopoutManager.requestPopout(vpnPopoutLoader.item, undefined, "vpn"); + } + } + } + + Component { + id: controlCenterButtonComponent + + ControlCenterButton { + isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: controlCenterLoader.item ?? null + parentScreen: barWindow.screen + screenName: barWindow.screen?.name || "" + screenModel: barWindow.screen?.model || "" + widgetData: parent.widgetData + + Component.onCompleted: { + barWindow.controlCenterButtonRef = this; + } + + Component.onDestruction: { + if (barWindow.controlCenterButtonRef === this) { + barWindow.controlCenterButtonRef = null; + } + } + + onClicked: { + controlCenterLoader.active = true; + if (!controlCenterLoader.item) { + return; + } + controlCenterLoader.item.triggerScreen = barWindow.screen; + if (controlCenterLoader.item.setTriggerPosition) { + const globalPos = mapToItem(null, 0, 0); + // Use topBarContent.barConfig directly + const effectiveBarConfig = topBarContent.barConfig; + // Calculate barPosition from axis.edge like Battery widget does + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + const section = topBarContent.getWidgetSection(parent) || "right"; + controlCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(controlCenterLoader.item, undefined, "controlCenter"); + if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { + NetworkService.scanWifi(); + } + } + } + } + + Component { + id: capsLockIndicatorComponent + + CapsLockIndicator { + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: barWindow.screen + } + } + + Component { + id: idleInhibitorComponent + + IdleInhibitor { + widgetThickness: barWindow.widgetThickness + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: barWindow.screen + } + } + + Component { + id: spacerComponent + + Item { + width: barWindow.isVertical ? barWindow.widgetThickness : (parent.spacerSize || 20) + height: barWindow.isVertical ? (parent.spacerSize || 20) : barWindow.widgetThickness + implicitWidth: width + implicitHeight: height + + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + border.width: 1 + radius: 2 + visible: false + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + cursorShape: Qt.ArrowCursor + onEntered: parent.visible = true + onExited: parent.visible = false + } + } + } + } + + Component { + id: separatorComponent + + Item { + width: barWindow.isVertical ? parent.barThickness : 1 + height: barWindow.isVertical ? 1 : parent.barThickness + implicitWidth: width + implicitHeight: height + + Rectangle { + width: barWindow.isVertical ? parent.width * 0.6 : 1 + height: barWindow.isVertical ? 1 : parent.height * 0.6 + anchors.centerIn: parent + color: Theme.outline + opacity: 0.3 + } + } + } + + Component { + id: keyboardLayoutNameComponent + + KeyboardLayoutName {} + } + + Component { + id: notepadButtonComponent + + NotepadButton { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: barWindow.screen + } + } + + Component { + id: colorPickerComponent + + ColorPicker { + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + section: topBarContent.getWidgetSection(parent) || "right" + parentScreen: barWindow.screen + onColorPickerRequested: { + barWindow.colorPickerRequested(); + } + } + } + + Component { + id: systemUpdateComponent + + SystemUpdate { + isActive: systemUpdateLoader.item ? systemUpdateLoader.item.shouldBeVisible : false + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness + axis: barWindow.axis + section: topBarContent.getWidgetSection(parent) || "right" + popoutTarget: systemUpdateLoader.item ?? null + parentScreen: barWindow.screen + onClicked: { + systemUpdateLoader.active = true; + if (!systemUpdateLoader.item) + return; + const popout = systemUpdateLoader.item; + const effectiveBarConfig = topBarContent.barConfig; + const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); + if (popout.setBarContext) { + popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + } + if (popout.setTriggerPosition) { + const globalPos = visualContent.mapToItem(null, 0, 0); + const currentScreen = parentScreen || Screen; + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); + popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + } + PopoutManager.requestPopout(popout, undefined, "systemUpdate"); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarWindow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarWindow.qml new file mode 100644 index 0000000..d8b0216 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/DankBarWindow.qml @@ -0,0 +1,1058 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services + +PanelWindow { + id: barWindow + + required property var rootWindow + required property var barConfig + property var modelData: item + property var hyprlandOverviewLoader: rootWindow ? rootWindow.hyprlandOverviewLoader : null + + property var leftWidgetsModel + property var centerWidgetsModel + property var rightWidgetsModel + + property var controlCenterButtonRef: null + property var clockButtonRef: null + + function triggerControlCenter() { + controlCenterLoader.active = true; + if (!controlCenterLoader.item) { + return; + } + + if (controlCenterButtonRef && controlCenterLoader.item.setTriggerPosition) { + const screenPos = controlCenterButtonRef.mapToItem(null, 0, 0); + const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, controlCenterButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); + const section = controlCenterButtonRef.section || "right"; + controlCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + } else { + controlCenterLoader.item.triggerScreen = barWindow.screen; + } + + controlCenterLoader.item.toggle(); + if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { + NetworkService.scanWifi(); + } + } + + function triggerWallpaperBrowser() { + dankDashPopoutLoader.active = true; + if (!dankDashPopoutLoader.item) { + return; + } + + let section = "center"; + if (clockButtonRef && clockButtonRef.visualContent && dankDashPopoutLoader.item.setTriggerPosition) { + // Calculate barPosition from axis.edge + const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); + section = clockButtonRef.section || "center"; + + let triggerPos, triggerWidth; + if (section === "center") { + const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? topBarContent.vCenterSection : topBarContent.vCenterSection) : topBarContent.hCenterSection; + if (centerSection) { + if (barWindow.isVertical) { + const centerY = centerSection.height / 2; + triggerPos = centerSection.mapToItem(null, 0, centerY); + triggerWidth = centerSection.height; + } else { + triggerPos = centerSection.mapToItem(null, 0, 0); + triggerWidth = centerSection.width; + } + } else { + triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); + triggerWidth = clockButtonRef.visualWidth; + } + } else { + triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); + triggerWidth = clockButtonRef.visualWidth; + } + + const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, barConfig?.spacing ?? 4, barPosition, barConfig); + dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + } else { + dankDashPopoutLoader.item.triggerScreen = barWindow.screen; + } + + PopoutManager.requestPopout(dankDashPopoutLoader.item, 2, (barConfig?.id ?? "default") + "-" + section + "-2"); + } + + readonly property var dBarLayer: { + switch (Quickshell.env("DMS_DANKBAR_LAYER")) { + case "bottom": + return WlrLayer.Bottom; + case "overlay": + return WlrLayer.Overlay; + case "background": + return WlrLayer.background; + default: + return WlrLayer.Top; + } + } + + property var blurRegion: null + property var _blurWidgetItems: [] + + function registerBlurWidget(item) { + if (_blurWidgetItems.indexOf(item) >= 0) + return; + _blurWidgetItems = _blurWidgetItems.concat([item]); + _blurRebuildTimer.restart(); + } + + function unregisterBlurWidget(item) { + const idx = _blurWidgetItems.indexOf(item); + if (idx < 0) + return; + const arr = _blurWidgetItems.slice(); + arr.splice(idx, 1); + _blurWidgetItems = arr; + _blurRebuildTimer.restart(); + } + + Timer { + id: _blurRebuildTimer + interval: 1 + onTriggered: barBlur.rebuild() + } + + Item { + id: barBlur + visible: false + + readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1 + + function rebuild() { + teardown(); + if (!BlurService.enabled || !BlurService.available) + return; + + const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0); + const hasBar = barHasTransparency; + if (!hasBar && widgets.length === 0) + return; + + const cr = Theme.cornerRadius; + let qml = 'import QtQuick; import Quickshell; Region {'; + for (let i = 0; i < widgets.length; i++) { + qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`; + } + qml += '}'; + + try { + const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion"); + + if (hasBar) { + region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x); + region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y); + region.width = Qt.binding(() => barUnitInset.width); + region.height = Qt.binding(() => barUnitInset.height); + region.radius = Qt.binding(() => barBackground.rt); + } + + for (let i = 0; i < widgets.length; i++) { + region[`w${i}`] = widgets[i]; + } + + barWindow.BackgroundEffect.blurRegion = region; + barWindow.blurRegion = region; + } catch (e) { + console.warn("BarBlur: Failed to create blur region:", e); + } + } + + function teardown() { + if (!barWindow.blurRegion) + return; + try { + barWindow.BackgroundEffect.blurRegion = null; + } catch (e) {} + barWindow.blurRegion.destroy(); + barWindow.blurRegion = null; + } + + onBarHasTransparencyChanged: _blurRebuildTimer.restart() + + Connections { + target: BlurService + function onEnabledChanged() { + barBlur.rebuild(); + } + } + + Connections { + target: topBarSlide + function onXChanged() { + if (barWindow.blurRegion) + barWindow.blurRegion.changed(); + } + function onYChanged() { + if (barWindow.blurRegion) + barWindow.blurRegion.changed(); + } + } + + Component.onCompleted: rebuild() + Component.onDestruction: teardown() + } + + WlrLayershell.layer: dBarLayer + WlrLayershell.namespace: "dms:bar" + + signal colorPickerRequested + + onColorPickerRequested: rootWindow.colorPickerRequested() + + property alias axis: axis + + AxisContext { + id: axis + edge: { + switch (barConfig?.position ?? 0) { + case SettingsData.Position.Top: + return "top"; + case SettingsData.Position.Bottom: + return "bottom"; + case SettingsData.Position.Left: + return "left"; + case SettingsData.Position.Right: + return "right"; + default: + return "top"; + } + } + } + + readonly property bool isVertical: axis.isVertical + + property bool gothCornersEnabled: barConfig?.gothCornersEnabled ?? false + property real wingtipsRadius: barConfig?.gothCornerRadiusOverride ? (barConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius + readonly property real _wingR: Math.max(0, wingtipsRadius) + readonly property color _surfaceContainer: Theme.surfaceContainer + readonly property string _barId: barConfig?.id ?? "default" + property real _backgroundAlpha: barConfig?.transparency ?? 1.0 + readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha) + + function _updateBackgroundAlpha() { + const live = SettingsData.barConfigs.find(c => c.id === _barId); + _backgroundAlpha = (live ?? barConfig)?.transparency ?? 1.0; + } + readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) + + // Shadow buffer: extra window space for shadow to render beyond bar bounds + readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0 + readonly property real _shadowBuffer: { + if (!_shadowActive) + return 0; + const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0; + if (hasOverride) { + const blur = (barConfig.shadowIntensity ?? 0) * 0.2; + const offset = blur * 0.5; + return Theme.snap(Math.max(16, blur + offset + 8), _dpr); + } + return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); + } + + property string screenName: modelData.name + + property bool hasMaximizedToplevel: false + property bool hasFullscreenToplevel: false + property bool shouldHideForWindows: false + + function _updateHasMaximizedToplevel() { + if (!(barConfig?.maximizeDetection ?? true)) { + hasMaximizedToplevel = false; + return; + } + if (!CompositorService.isHyprland && !CompositorService.isNiri) { + hasMaximizedToplevel = false; + return; + } + + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + for (let i = 0; i < filtered.length; i++) { + if (filtered[i]?.maximized) { + hasMaximizedToplevel = true; + return; + } + } + hasMaximizedToplevel = false; + } + + function _updateHasFullscreenToplevel() { + if (!CompositorService.isHyprland) { + hasFullscreenToplevel = false; + return; + } + + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + for (let i = 0; i < filtered.length; i++) { + if (filtered[i]?.fullscreen) { + // On niri, fullscreen windows in inactive columns should not hide the bar + if (CompositorService.isNiri && !filtered[i]?.activated) + continue; + hasFullscreenToplevel = true; + return; + } + } + hasFullscreenToplevel = false; + } + + function _updateShouldHideForWindows() { + if (!(barConfig?.showOnWindowsOpen ?? false)) { + shouldHideForWindows = false; + return; + } + if (!(barConfig?.autoHide ?? false)) { + shouldHideForWindows = false; + return; + } + if (!CompositorService.isNiri && !CompositorService.isHyprland) { + shouldHideForWindows = false; + return; + } + + if (CompositorService.isNiri) { + let currentWorkspaceId = null; + for (let i = 0; i < NiriService.allWorkspaces.length; i++) { + const ws = NiriService.allWorkspaces[i]; + if (ws.output === screenName && ws.is_active) { + currentWorkspaceId = ws.id; + break; + } + } + + if (currentWorkspaceId === null) { + shouldHideForWindows = false; + return; + } + + let hasTiled = false; + let hasFloatingTouchingBar = false; + const pos = barConfig?.position ?? 0; + const barThickness = barWindow.effectiveBarThickness + (barConfig?.spacing ?? 4); + + for (let i = 0; i < NiriService.windows.length; i++) { + const win = NiriService.windows[i]; + if (win.workspace_id !== currentWorkspaceId) + continue; + + if (!win.is_floating) { + hasTiled = true; + continue; + } + + const tilePos = win.layout?.tile_pos_in_workspace_view; + const winSize = win.layout?.window_size || win.layout?.tile_size; + if (!tilePos || !winSize) + continue; + + switch (pos) { + case SettingsData.Position.Top: + if (tilePos[1] < barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Bottom: + const screenHeight = barWindow.screen?.height ?? 0; + if (tilePos[1] + winSize[1] > screenHeight - barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Left: + if (tilePos[0] < barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Right: + const screenWidth = barWindow.screen?.width ?? 0; + if (tilePos[0] + winSize[0] > screenWidth - barThickness) + hasFloatingTouchingBar = true; + break; + } + } + + shouldHideForWindows = hasTiled || hasFloatingTouchingBar; + return; + } + + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + shouldHideForWindows = filtered.length > 0; + } + + property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4) + + Behavior on effectiveSpacing { + enabled: barWindow.visible + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + readonly property int notificationCount: NotificationService.notifications.length + readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) + readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr) + + readonly property bool hasAdjacentTopBar: { + if (barConfig?.autoHide ?? false) + return false; + if (!isVertical) + return false; + return SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Top && bc.position !== 0) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + } + + readonly property bool hasAdjacentBottomBar: { + if (barConfig?.autoHide ?? false) + return false; + if (!isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Bottom && bc.position !== 1) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + readonly property bool hasAdjacentLeftBar: { + if (barConfig?.autoHide ?? false) + return false; + if (isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Left && bc.position !== 2) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + readonly property bool hasAdjacentRightBar: { + if (barConfig?.autoHide ?? false) + return false; + if (isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Right && bc.position !== 3) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + screen: modelData + implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 + implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 + color: "transparent" + + property var nativeInhibitor: null + + Component.onCompleted: { + if (SettingsData.forceStatusBarLayoutRefresh) { + SettingsData.forceStatusBarLayoutRefresh.connect(() => { + Qt.callLater(() => { + stackContainer.visible = false; + Qt.callLater(() => { + stackContainer.visible = true; + }); + }); + }); + } + + updateGpuTempConfig(); + _updateBackgroundAlpha(); + _updateHasMaximizedToplevel(); + _updateShouldHideForWindows(); + + inhibitorInitTimer.start(); + } + + Timer { + id: inhibitorInitTimer + interval: 300 + repeat: false + onTriggered: { + if (SessionService.nativeInhibitorAvailable) { + createNativeInhibitor(); + } + } + } + + Connections { + target: PluginService + function onPluginLoaded(pluginId) { + console.info("DankBar: Plugin loaded:", pluginId); + SettingsData.widgetDataChanged(); + } + function onPluginUnloaded(pluginId) { + console.info("DankBar: Plugin unloaded:", pluginId); + SettingsData.widgetDataChanged(); + } + } + + function updateGpuTempConfig() { + const leftWidgets = barConfig?.leftWidgets || []; + const centerWidgets = barConfig?.centerWidgets || []; + const rightWidgets = barConfig?.rightWidgets || []; + const allWidgets = [...leftWidgets, ...centerWidgets, ...rightWidgets]; + + const hasGpuTempWidget = allWidgets.some(widget => { + const widgetId = typeof widget === "string" ? widget : widget.id; + const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false); + return widgetId === "gpuTemp" && widgetEnabled; + }); + + DgopService.gpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled || SessionData.nonNvidiaGpuTempEnabled; + DgopService.nvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled; + DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled; + } + + function createNativeInhibitor() { + if (!SessionService.nativeInhibitorAvailable) { + return; + } + + try { + const qmlString = ` + import QtQuick + import Quickshell.Wayland + + IdleInhibitor { + enabled: false + } + `; + + nativeInhibitor = Qt.createQmlObject(qmlString, barWindow, "DankBar.NativeInhibitor"); + nativeInhibitor.window = barWindow; + nativeInhibitor.enabled = Qt.binding(() => SessionService.idleInhibited); + nativeInhibitor.enabledChanged.connect(function () { + if (SessionService.idleInhibited !== nativeInhibitor.enabled) { + SessionService.idleInhibited = nativeInhibitor.enabled; + SessionService.inhibitorChanged(); + } + }); + } catch (e) { + nativeInhibitor = null; + } + } + + Connections { + function onBarConfigChanged() { + barWindow.updateGpuTempConfig(); + barWindow._updateBackgroundAlpha(); + barWindow._updateHasMaximizedToplevel(); + barWindow._updateShouldHideForWindows(); + } + + target: rootWindow + } + + Connections { + target: SettingsData + function onBarConfigsChanged() { + barWindow._updateBackgroundAlpha(); + } + } + + Connections { + target: CompositorService + function onToplevelsChanged() { + barWindow._updateHasMaximizedToplevel(); + barWindow._updateHasFullscreenToplevel(); + barWindow._updateShouldHideForWindows(); + } + } + + Connections { + target: NiriService + function onAllWorkspacesChanged() { + barWindow._updateHasMaximizedToplevel(); + barWindow._updateHasFullscreenToplevel(); + barWindow._updateShouldHideForWindows(); + } + } + + Connections { + function onNvidiaGpuTempEnabledChanged() { + barWindow.updateGpuTempConfig(); + } + + function onNonNvidiaGpuTempEnabledChanged() { + barWindow.updateGpuTempConfig(); + } + + target: SessionData + } + + readonly property int barPos: barConfig?.position ?? 0 + + anchors.top: !isVertical ? (barPos === SettingsData.Position.Top) : true + anchors.bottom: !isVertical ? (barPos === SettingsData.Position.Bottom) : true + anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left) + anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right) + + exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0)) + + Item { + id: inputMask + + readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) + + readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) + readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow + readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) + + readonly property int maskThickness: showing ? barThickness : 1 + + x: { + if (!axis.isVertical) { + return 0; + } else { + switch (barPos) { + case SettingsData.Position.Left: + return 0; + case SettingsData.Position.Right: + return parent.width - maskThickness; + default: + return 0; + } + } + } + y: { + if (axis.isVertical) { + return 0; + } else { + switch (barPos) { + case SettingsData.Position.Top: + return 0; + case SettingsData.Position.Bottom: + return parent.height - maskThickness; + default: + return 0; + } + } + } + width: axis.isVertical ? maskThickness : parent.width + height: axis.isVertical ? parent.height : maskThickness + } + + readonly property bool clickThroughEnabled: barConfig?.clickThrough ?? false + + readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null + readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null + readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null + readonly property real _revealProgress: topBarSlide.x + topBarSlide.y + + function sectionRect(section, isCenter, _dep) { + if (!section) + return { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + }; + + const pos = section.mapToItem(barWindow.contentItem, 0, 0); + const implW = section.implicitWidth || 0; + const implH = section.implicitHeight || 0; + + const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0; + const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0); + + const edgePad = 2; + return { + "x": pos.x + offsetX - edgePad, + "y": pos.y + offsetY - edgePad, + "w": implW + edgePad * 2, + "h": implH + edgePad * 2 + }; + } + + mask: Region { + item: clickThroughEnabled ? null : inputMask + + Region { + readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + } + x: r.x + y: r.y + width: r.w + height: r.h + } + + Region { + readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + } + x: r.x + y: r.y + width: r.w + height: r.h + } + + Region { + readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + } + x: r.x + y: r.y + width: r.w + height: r.h + } + + Region { + readonly property bool active: barWindow.clickThroughEnabled && !inputMask.showing + x: active ? inputMask.x : 0 + y: active ? inputMask.y : 0 + width: active ? inputMask.width : 0 + height: active ? inputMask.height : 0 + } + } + + Item { + id: topBarCore + anchors.fill: parent + layer.enabled: false + + property bool autoHide: barConfig?.autoHide ?? false + property bool revealSticky: false + + Timer { + id: revealHold + interval: barWindow.clickThroughEnabled ? Math.max((barConfig?.autoHideDelay ?? 250) * 6, 1500) : (barConfig?.autoHideDelay ?? 250) + repeat: false + onTriggered: { + if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) { + topBarCore.revealSticky = false; + } + } + } + + property bool reveal: { + const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false); + if (inOverviewWithShow) + return true; + + if (barWindow.hasFullscreenToplevel) + return false; + + const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false; + if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) { + if (barWindow.shouldHideForWindows) + return topBarMouseArea.containsMouse || hasActivePopout || revealSticky; + return true; + } + + if (CompositorService.isNiri && NiriService.inOverview) + return topBarMouseArea.containsMouse || hasActivePopout || revealSticky; + + return (barConfig?.visible ?? true) && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky); + } + + property bool hasActivePopout: false + + onHasActivePopoutChanged: evaluateReveal() + + function updateActivePopoutState() { + if (!barWindow.screen) + return; + const screenName = barWindow.screen.name; + const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; + const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; + const trayOpen = rootWindow.systemTrayMenuOpen; + + const hasVisiblePopout = activePopout && activePopout.shouldBeVisible; + topBarCore.hasActivePopout = !!(hasVisiblePopout || activeTrayMenu || trayOpen); + } + + Connections { + target: PopoutManager + function onPopoutChanged() { + topBarCore.updateActivePopoutState(); + } + } + + Connections { + target: TrayMenuManager + function onActiveTrayMenusChanged() { + topBarCore.updateActivePopoutState(); + } + } + + Connections { + function onBarConfigChanged() { + topBarCore.autoHide = barConfig?.autoHide ?? false; + } + + target: rootWindow + } + + function evaluateReveal() { + if (!autoHide) + return; + + if (topBarMouseArea.containsMouse || hasActivePopout) { + revealSticky = true; + revealHold.stop(); + return; + } + + revealHold.restart(); + } + + Connections { + target: topBarMouseArea + function onContainsMouseChanged() { + topBarCore.evaluateReveal(); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + topBarCore.evaluateReveal(); + } + } + + MouseArea { + id: topBarMouseArea + y: !barWindow.isVertical ? (barPos === SettingsData.Position.Bottom ? parent.height - height : 0) : 0 + x: barWindow.isVertical ? (barPos === SettingsData.Position.Right ? parent.width - width : 0) : 0 + height: !barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined + width: barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined + anchors { + left: !barWindow.isVertical ? parent.left : (barPos === SettingsData.Position.Left ? parent.left : undefined) + right: !barWindow.isVertical ? parent.right : (barPos === SettingsData.Position.Right ? parent.right : undefined) + top: barWindow.isVertical ? parent.top : undefined + bottom: barWindow.isVertical ? parent.bottom : undefined + } + readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false) + hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout + acceptedButtons: Qt.NoButton + enabled: (barConfig?.autoHide ?? false) && !inOverview + + Item { + id: topBarContainer + anchors.fill: parent + + transform: Translate { + id: topBarSlide + x: barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Right ? barWindow.implicitWidth : -barWindow.implicitWidth), barWindow._dpr) : 0 + y: !barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Bottom ? barWindow.implicitHeight : -barWindow.implicitHeight), barWindow._dpr) : 0 + + Behavior on x { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + + Item { + id: barUnitInset + property int spacingPx: Theme.px(barWindow.effectiveSpacing, barWindow._dpr) + anchors.fill: parent + anchors.leftMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "left" ? spacingPx : 0) + anchors.rightMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "right" ? spacingPx : 0) + anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? 0 : spacingPx) + anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? spacingPx : 0) + + BarCanvas { + id: barBackground + barWindow: barWindow + axis: axis + barConfig: barWindow.barConfig + } + + MouseArea { + id: scrollArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + + property real touchpadAccumulatorY: 0 + property real touchpadAccumulatorX: 0 + property real mouseAccumulatorY: 0 + property real mouseAccumulatorX: 0 + property bool actionInProgress: false + + Timer { + id: cooldownTimer + interval: 100 + onTriggered: parent.actionInProgress = false + } + + function handleScrollAction(behavior, direction) { + switch (behavior) { + case "workspace": + topBarContent.switchWorkspace(direction); + return true; + case "column": + if (!CompositorService.isNiri) + return false; + if (direction > 0) + NiriService.moveColumnRight(); + else + NiriService.moveColumnLeft(); + return true; + default: + return false; + } + } + + onWheel: wheel => { + if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) { + wheel.accepted = false; + return; + } + + const deltaY = wheel.angleDelta.y; + const deltaX = wheel.angleDelta.x; + const isTouchpadY = wheel.pixelDelta && wheel.pixelDelta.y !== 0; + const isTouchpadX = wheel.pixelDelta && wheel.pixelDelta.x !== 0; + const xBehavior = barConfig?.scrollXBehavior ?? "column"; + const yBehavior = barConfig?.scrollYBehavior ?? "workspace"; + const reverse = SettingsData.reverseScrolling ? -1 : 1; + + if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) { + if (isTouchpadX) { + touchpadAccumulatorX += deltaX; + if (Math.abs(touchpadAccumulatorX) >= 500) { + const direction = touchpadAccumulatorX * reverse < 0 ? 1 : -1; + if (handleScrollAction(xBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + touchpadAccumulatorX = 0; + } + } else { + mouseAccumulatorX += deltaX; + if (Math.abs(mouseAccumulatorX) >= 120) { + const direction = mouseAccumulatorX * reverse < 0 ? 1 : -1; + if (handleScrollAction(xBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + mouseAccumulatorX = 0; + } + } + wheel.accepted = false; + return; + } + + if (yBehavior === "none") { + wheel.accepted = false; + return; + } + + if (isTouchpadY) { + touchpadAccumulatorY += deltaY; + if (Math.abs(touchpadAccumulatorY) >= 500) { + const direction = touchpadAccumulatorY * reverse < 0 ? 1 : -1; + if (handleScrollAction(yBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + touchpadAccumulatorY = 0; + } + } else { + mouseAccumulatorY += deltaY; + if (Math.abs(mouseAccumulatorY) >= 120) { + const direction = mouseAccumulatorY * reverse < 0 ? 1 : -1; + if (handleScrollAction(yBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + mouseAccumulatorY = 0; + } + } + + wheel.accepted = false; + } + } + + DankBarContent { + id: topBarContent + barWindow: barWindow + rootWindow: barWindow.rootWindow + barConfig: barWindow.barConfig + leftWidgetsModel: barWindow.leftWidgetsModel + centerWidgetsModel: barWindow.centerWidgetsModel + rightWidgetsModel: barWindow.rightWidgetsModel + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/LeftSection.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/LeftSection.qml new file mode 100644 index 0000000..d7219db --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/LeftSection.qml @@ -0,0 +1,119 @@ +import QtQuick +import qs.Common + +Item { + id: root + + property var widgetsModel: null + property var components: null + property bool noBackground: false + required property var axis + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property real barSpacing: 4 + property var barConfig: null + property var blurBarWindow: null + property bool overrideAxisLayout: false + property bool forceVerticalLayout: false + + readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) + + implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 + implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 + + Loader { + id: layoutLoader + anchors.fill: parent + sourceComponent: root.isVertical ? columnComp : rowComp + } + + Component { + id: rowComp + Row { + readonly property real widgetSpacing: { + const baseSpacing = noBackground ? 2 : Theme.spacingXS; + const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return baseSpacing + (outlineThickness * 2); + } + spacing: widgetSpacing + Repeater { + id: rowRepeater + model: root.widgetsModel + Item { + readonly property real rowSpacing: parent.widgetSpacing + property var itemData: modelData + width: widgetLoader.item ? widgetLoader.item.width : 0 + height: widgetLoader.item ? widgetLoader.item.height : 0 + WidgetHost { + id: widgetLoader + anchors.verticalCenter: parent.verticalCenter + widgetId: itemData.widgetId + widgetData: itemData + spacerSize: itemData.size || 20 + components: root.components + isInColumn: false + axis: root.axis + section: "left" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + barSpacing: root.barSpacing + barConfig: root.barConfig + blurBarWindow: root.blurBarWindow + isFirst: index === 0 + isLast: index === rowRepeater.count - 1 + sectionSpacing: parent.rowSpacing + isLeftBarEdge: true + isRightBarEdge: false + } + } + } + } + } + + Component { + id: columnComp + Column { + width: parent.width + readonly property real widgetSpacing: { + const baseSpacing = noBackground ? 2 : Theme.spacingXS; + const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return baseSpacing + (outlineThickness * 2); + } + spacing: widgetSpacing + Repeater { + id: columnRepeater + model: root.widgetsModel + Item { + width: parent.width + readonly property real columnSpacing: parent.widgetSpacing + property var itemData: modelData + height: widgetLoader.item ? widgetLoader.item.height : 0 + WidgetHost { + id: widgetLoader + anchors.horizontalCenter: parent.horizontalCenter + widgetId: itemData.widgetId + widgetData: itemData + spacerSize: itemData.size || 20 + components: root.components + isInColumn: true + axis: root.axis + section: "left" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + barSpacing: root.barSpacing + barConfig: root.barConfig + blurBarWindow: root.blurBarWindow + isFirst: index === 0 + isLast: index === columnRepeater.count - 1 + sectionSpacing: parent.columnSpacing + isTopBarEdge: true + isBottomBarEdge: false + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml new file mode 100644 index 0000000..f0cb9d8 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml @@ -0,0 +1,633 @@ +import QtQuick +import Quickshell +import Quickshell.Services.UPower +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + layerNamespace: "dms:battery" + + property var triggerScreen: null + + function isActiveProfile(profile) { + if (typeof PowerProfiles === "undefined") { + return false; + } + + return PowerProfiles.profile === profile; + } + + function setProfile(profile) { + if (typeof PowerProfiles === "undefined") { + ToastService.showError(I18n.tr("power-profiles-daemon not available")); + return; + } + PowerProfiles.profile = profile; + if (PowerProfiles.profile !== profile) { + ToastService.showError(I18n.tr("Failed to set power profile")); + } + } + + popupWidth: 400 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 0 + triggerWidth: 70 + positioning: "" + screen: triggerScreen + + onBackgroundClicked: close() + + content: Component { + Rectangle { + id: batteryContent + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 + color: "transparent" + focus: true + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus(); + } + } + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Escape) { + root.close(); + event.accepted = true; + } + } + + Connections { + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(function () { + batteryContent.forceActiveFocus(); + }); + } + } + + target: root + } + + Column { + id: contentColumn + + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + height: 48 + spacing: Theme.spacingM + + DankIcon { + name: { + if (!BatteryService.batteryAvailable) + return "power"; + + if (!BatteryService.isCharging && BatteryService.isPluggedIn) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + return "battery_charging_20"; + } + if (BatteryService.isCharging) { + if (BatteryService.batteryLevel >= 90) { + return "battery_charging_full"; + } + if (BatteryService.batteryLevel >= 80) { + return "battery_charging_90"; + } + if (BatteryService.batteryLevel >= 60) { + return "battery_charging_80"; + } + if (BatteryService.batteryLevel >= 50) { + return "battery_charging_60"; + } + if (BatteryService.batteryLevel >= 30) { + return "battery_charging_50"; + } + if (BatteryService.batteryLevel >= 20) { + return "battery_charging_30"; + } + return "battery_charging_20"; + } else { + if (BatteryService.batteryLevel >= 95) { + return "battery_full"; + } + if (BatteryService.batteryLevel >= 85) { + return "battery_6_bar"; + } + if (BatteryService.batteryLevel >= 70) { + return "battery_5_bar"; + } + if (BatteryService.batteryLevel >= 55) { + return "battery_4_bar"; + } + if (BatteryService.batteryLevel >= 40) { + return "battery_3_bar"; + } + if (BatteryService.batteryLevel >= 25) { + return "battery_2_bar"; + } + return "battery_1_bar"; + } + } + size: Theme.iconSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) + return Theme.error; + if (BatteryService.isCharging || BatteryService.isPluggedIn) + return Theme.primary; + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + id: headerInfoColumn + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2 + readonly property string timeInfoText: { + if (!BatteryService.batteryAvailable) + return I18n.tr("Power profile management available"); + const time = BatteryService.formatTimeRemaining(); + if (time !== "Unknown") { + return BatteryService.isCharging ? I18n.tr("Time until full: %1").arg(time) : I18n.tr("Time remaining: %1").arg(time); + } + return ""; + } + readonly property bool showPowerRate: BatteryService.batteryAvailable && Math.abs(BatteryService.changeRate) > 0.05 + readonly property bool isOnAC: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn) + readonly property bool isDischarging: BatteryService.batteryAvailable && !BatteryService.isCharging && !BatteryService.isPluggedIn + + Row { + spacing: Theme.spacingS + + StyledText { + text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power") + font.pixelSize: Theme.fontSizeXLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + if (BatteryService.isCharging) { + return Theme.primary; + } + return Theme.surfaceText; + } + font.weight: Font.Bold + } + + StyledText { + text: BatteryService.batteryStatus + font.pixelSize: Theme.fontSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + if (BatteryService.isCharging) { + return Theme.primary; + } + return Theme.surfaceText; + } + font.weight: Font.Medium + visible: BatteryService.batteryAvailable + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: headerInfoColumn.timeInfoText.length > 0 + + StyledText { + id: powerRateText + text: `${headerInfoColumn.isOnAC ? "+" : (headerInfoColumn.isDischarging ? "-" : "")}${Math.abs(BatteryService.changeRate).toFixed(1)}W` + font.pixelSize: Theme.fontSizeSmall + color: { + if (headerInfoColumn.isOnAC) { + return Theme.primary; + } + if (headerInfoColumn.isDischarging) { + return Theme.warning; + } + return Theme.surfaceTextMedium; + } + font.weight: Font.Medium + visible: headerInfoColumn.showPowerRate + } + + StyledText { + text: headerInfoColumn.timeInfoText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + elide: Text.ElideRight + width: parent.width - (powerRateText.visible ? (powerRateText.implicitWidth + parent.spacing) : 0) + } + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent" + anchors.top: parent.top + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeBatteryArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.close(); + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: BatteryService.batteryAvailable + + StyledRect { + width: (parent.width - Theme.spacingM) / 2 + height: 64 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.width: 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Health") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: BatteryService.batteryHealth + font.pixelSize: Theme.fontSizeLarge + color: { + if (BatteryService.batteryHealth === "N/A") { + return Theme.surfaceText; + } + const healthNum = parseInt(BatteryService.batteryHealth); + return healthNum < 80 ? Theme.error : Theme.surfaceText; + } + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + StyledRect { + width: (parent.width - Theme.spacingM) / 2 + height: 64 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.width: 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Capacity") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + // Individual battery details for multiple batteries + Column { + width: parent.width + spacing: Theme.spacingS + visible: !BatteryService.usePreferred && BatteryService.batteries.length > 1 + + StyledText { + text: I18n.tr("Individual Batteries") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + } + + Repeater { + model: ScriptModel { + values: BatteryService.batteries + } + + delegate: StyledRect { + required property var modelData + required property int index + + width: parent.width + height: batteryColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.width: 0 + + Column { + id: batteryColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + // Top row: name and percentage + Row { + width: parent.width + spacing: Theme.spacingM + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2 + + StyledText { + text: modelData.model || I18n.tr("Battery %1").arg(index + 1) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.nativePath + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + elide: Text.ElideMiddle + width: parent.width + } + } + + Item { + width: 1 + height: parent.height + } + + StyledText { + id: percentText + text: `${Math.round(100 * modelData.percentage)}%` + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + id: chargingIcon + name: modelData.state === UPowerDeviceState.Charging ? "bolt" : "" + size: Theme.iconSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: modelData.state === UPowerDeviceState.Charging + } + } + + // Bottom row: Health, Capacity and Time + Flow { + width: parent.width + spacing: Theme.spacingS + + StyledRect { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.width: 0 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + text: I18n.tr("Health") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (!modelData.healthSupported || modelData.healthPercentage <= 0) + return "N/A"; + return `${Math.round(modelData.healthPercentage)}%`; + } + font.pixelSize: Theme.fontSizeSmall + color: { + if (!modelData.healthSupported || modelData.healthPercentage <= 0) + return Theme.surfaceText; + return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText; + } + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + StyledRect { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.width: 0 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + text: I18n.tr("Capacity") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: modelData.energyCapacity > 0 ? `${modelData.energyCapacity.toFixed(1)}` : "N/A" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + StyledRect { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.width: 0 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0; + + if (!time || time <= 0 || time > 86400) + return "N/A"; + + const hours = Math.floor(time / 3600); + const minutes = Math.floor((time % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + } + } + } + + Item { + width: parent.width + height: profileButtonGroup.height * profileButtonGroup.scale + + DankButtonGroup { + id: profileButtonGroup + + property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + property int currentProfileIndex: { + if (typeof PowerProfiles === "undefined") + return 1; + return profileModel.findIndex(profile => root.isActiveProfile(profile)); + } + + scale: Math.min(1, parent.width / implicitWidth) + transformOrigin: Item.Center + anchors.horizontalCenter: parent.horizontalCenter + model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) + currentIndex: currentProfileIndex + selectionMode: "single" + onSelectionChanged: (index, selected) => { + if (!selected) + return; + root.setProfile(profileModel[index]); + } + } + } + + StyledRect { + width: parent.width + height: degradationContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3) + border.width: 0 + visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None + + Column { + id: degradationContent + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSize - Theme.spacingM + + StyledText { + text: I18n.tr("Power Profile Degradation") + font.pixelSize: Theme.fontSizeLarge + color: Theme.error + font.weight: Font.Medium + } + + StyledText { + text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) + wrapMode: Text.WordWrap + width: parent.width + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/DWLLayoutPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/DWLLayoutPopout.qml new file mode 100644 index 0000000..9daed8d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/DWLLayoutPopout.qml @@ -0,0 +1,302 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + layerNamespace: "dms:layout" + + property var triggerScreen: null + + function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) { + triggerX = x; + triggerY = y; + triggerWidth = width; + triggerSection = section; + triggerScreen = screen; + root.screen = screen; + + storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4); + storedBarSpacing = barSpacing !== undefined ? barSpacing : 4; + storedBarConfig = barConfig; + + const pos = barPosition !== undefined ? barPosition : 0; + const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0; + + setBarContext(pos, bottomGap); + + updateOutputState(); + } + + onScreenChanged: updateOutputState() + + function updateOutputState() { + if (screen && DwlService.dwlAvailable) { + outputState = DwlService.getOutputState(screen.name); + } else { + outputState = null; + } + } + + property var outputState: null + property string currentLayoutSymbol: outputState?.layoutSymbol || "" + + readonly property var layoutNames: ({ + "CT": I18n.tr("Center Tiling"), + "G": I18n.tr("Grid"), + "K": I18n.tr("Deck"), + "M": I18n.tr("Monocle"), + "RT": I18n.tr("Right Tiling"), + "S": I18n.tr("Scrolling"), + "T": I18n.tr("Tiling"), + "VG": I18n.tr("Vertical Grid"), + "VK": I18n.tr("Vertical Deck"), + "VS": I18n.tr("Vertical Scrolling"), + "VT": I18n.tr("Vertical Tiling") + }) + + readonly property var layoutIcons: ({ + "CT": "view_compact", + "G": "grid_view", + "K": "layers", + "M": "fullscreen", + "RT": "view_sidebar", + "S": "view_carousel", + "T": "view_quilt", + "VG": "grid_on", + "VK": "view_day", + "VS": "scrollable_header", + "VT": "clarify" + }) + + function getLayoutName(symbol) { + return layoutNames[symbol] || symbol; + } + + function getLayoutIcon(symbol) { + return layoutIcons[symbol] || "view_quilt"; + } + + Connections { + target: DwlService + function onStateChanged() { + updateOutputState(); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + updateOutputState(); + } + } + + Component.onCompleted: { + updateOutputState(); + } + + popupWidth: 300 + popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 550 + triggerWidth: 70 + positioning: "" + screen: triggerScreen + shouldBeVisible: false + + onBackgroundClicked: close() + + content: Component { + Rectangle { + id: layoutContent + + implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 + color: "transparent" + focus: true + + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus(); + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close(); + event.accepted = true; + } + } + + Connections { + target: root + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(() => { + layoutContent.forceActiveFocus(); + }); + } + } + } + + Column { + id: contentColumn + + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + height: 40 + spacing: Theme.spacingM + + DankIcon { + name: "view_quilt" + size: Theme.iconSizeLarge + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2 + + StyledText { + text: I18n.tr("Layout") + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Bold + } + + StyledText { + text: root.currentLayoutSymbol + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: closeLayoutArea.containsMouse ? Theme.errorHover : "transparent" + anchors.top: parent.top + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeLayoutArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeLayoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.close(); + } + } + } + } + + StyledText { + text: I18n.tr("Available Layouts") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: DwlService.layouts + + delegate: Rectangle { + required property string modelData + required property int index + + property bool isActive: modelData === root.currentLayoutSymbol + + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: layoutArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: root.getLayoutIcon(modelData) + size: 20 + color: parent.parent.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: root.getLayoutName(modelData) + font.pixelSize: Theme.fontSizeSmall + color: parent.parent.parent.isActive ? Theme.primary : Theme.surfaceText + font.weight: parent.parent.parent.isActive ? Font.Medium : Font.Normal + } + + StyledText { + text: modelData + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + } + + MouseArea { + id: layoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (!root.triggerScreen) { + return; + } + if (!DwlService.dwlAvailable) { + return; + } + + DwlService.setLayout(root.triggerScreen.name, index); + root.close(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + + StyledText { + text: I18n.tr("Right-click bar widget to cycle") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/VpnPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/VpnPopout.qml new file mode 100644 index 0000000..064f578 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Popouts/VpnPopout.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.Widgets + +DankPopout { + id: root + + layerNamespace: "dms:vpn" + + Ref { + service: DMSNetworkService + } + + property bool wasVisible: false + property var triggerScreen: null + + popupWidth: 380 + popupHeight: Math.min((screen ? screen.height : Screen.height) - 100, contentLoader.item ? contentLoader.item.implicitHeight : 320) + triggerWidth: 70 + screen: triggerScreen + shouldBeVisible: false + + onShouldBeVisibleChanged: { + if (shouldBeVisible && !wasVisible) { + DMSNetworkService.getState(); + } + wasVisible = shouldBeVisible; + } + + onBackgroundClicked: close() + + content: Component { + Rectangle { + id: content + + implicitHeight: contentColumn.height + Theme.spacingL * 2 + color: "transparent" + focus: true + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close(); + event.accepted = true; + } + } + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + RowLayout { + width: parent.width + height: 32 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("VPN Connections") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + Layout.fillWidth: true + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.close() + } + } + + VpnDetailContent { + width: parent.width + listHeight: 200 + parentPopout: root + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/RightSection.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/RightSection.qml new file mode 100644 index 0000000..58844bc --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/RightSection.qml @@ -0,0 +1,121 @@ +import QtQuick +import qs.Common + +Item { + id: root + + property var widgetsModel: null + property var components: null + property bool noBackground: false + required property var axis + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property real barSpacing: 4 + property var barConfig: null + property var blurBarWindow: null + property bool overrideAxisLayout: false + property bool forceVerticalLayout: false + + readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) + + implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 + implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 + + Loader { + id: layoutLoader + width: parent.width + height: parent.height + sourceComponent: root.isVertical ? columnComp : rowComp + } + + Component { + id: rowComp + Row { + readonly property real widgetSpacing: { + const baseSpacing = noBackground ? 2 : Theme.spacingXS; + const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return baseSpacing + (outlineThickness * 2); + } + spacing: widgetSpacing + anchors.right: parent ? parent.right : undefined + Repeater { + id: rowRepeater + model: root.widgetsModel + Item { + readonly property real rowSpacing: parent.widgetSpacing + property var itemData: modelData + width: widgetLoader.item ? widgetLoader.item.width : 0 + height: widgetLoader.item ? widgetLoader.item.height : 0 + WidgetHost { + id: widgetLoader + anchors.verticalCenter: parent.verticalCenter + widgetId: itemData.widgetId + widgetData: itemData + spacerSize: itemData.size || 20 + components: root.components + isInColumn: false + axis: root.axis + section: "right" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + barSpacing: root.barSpacing + barConfig: root.barConfig + blurBarWindow: root.blurBarWindow + isFirst: index === 0 + isLast: index === rowRepeater.count - 1 + sectionSpacing: parent.rowSpacing + isLeftBarEdge: false + isRightBarEdge: true + } + } + } + } + } + + Component { + id: columnComp + Column { + width: parent.width + readonly property real widgetSpacing: { + const baseSpacing = noBackground ? 2 : Theme.spacingXS; + const outlineThickness = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return baseSpacing + (outlineThickness * 2); + } + spacing: widgetSpacing + Repeater { + id: columnRepeater + model: root.widgetsModel + Item { + width: parent.width + readonly property real columnSpacing: parent.widgetSpacing + property var itemData: modelData + height: widgetLoader.item ? widgetLoader.item.height : 0 + WidgetHost { + id: widgetLoader + anchors.horizontalCenter: parent.horizontalCenter + widgetId: itemData.widgetId + widgetData: itemData + spacerSize: itemData.size || 20 + components: root.components + isInColumn: true + axis: root.axis + section: "right" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + barSpacing: root.barSpacing + barConfig: root.barConfig + blurBarWindow: root.blurBarWindow + isFirst: index === 0 + isLast: index === columnRepeater.count - 1 + sectionSpacing: parent.columnSpacing + isTopBarEdge: false + isBottomBarEdge: true + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/WidgetHost.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/WidgetHost.qml new file mode 100644 index 0000000..01d1c73 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/WidgetHost.qml @@ -0,0 +1,290 @@ +import QtQuick +import qs.Services + +Loader { + id: root + + property string widgetId: "" + property var widgetData: null + property int spacerSize: 20 + property var components: null + property bool isInColumn: false + property var axis: null + property string section: "center" + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property real barSpacing: 4 + property var barConfig: null + property var blurBarWindow: null + property bool isFirst: false + property bool isLast: false + property real sectionSpacing: 0 + property bool isLeftBarEdge: false + property bool isRightBarEdge: false + property bool isTopBarEdge: false + property bool isBottomBarEdge: false + property string _registeredScreenName: "" + + asynchronous: false + + readonly property bool orientationMatches: (axis?.isVertical ?? false) === isInColumn + + readonly property bool widgetEnabled: widgetData?.enabled !== false + + active: orientationMatches && getWidgetVisible(widgetId, DgopService.dgopAvailable) && (widgetId !== "music" || MprisController.activePlayer !== null) + sourceComponent: getWidgetComponent(widgetId, components) + + signal contentItemReady(var item) + + Binding { + target: root.item + when: root.item && !root.widgetEnabled + property: "visible" + value: false + restoreMode: Binding.RestoreBinding + } + + Binding { + target: root.item + when: root.item && "parentScreen" in root.item + property: "parentScreen" + value: root.parentScreen + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "section" in root.item + property: "section" + value: root.section + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "widgetThickness" in root.item + property: "widgetThickness" + value: root.widgetThickness + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "barThickness" in root.item + property: "barThickness" + value: root.barThickness + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "barSpacing" in root.item + property: "barSpacing" + value: root.barSpacing + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "barConfig" in root.item + property: "barConfig" + value: root.barConfig + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "blurBarWindow" in root.item + property: "blurBarWindow" + value: root.blurBarWindow + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "axis" in root.item + property: "axis" + value: root.axis + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "widgetData" in root.item + property: "widgetData" + value: root.widgetData + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isFirst" in root.item + property: "isFirst" + value: root.isFirst + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isLast" in root.item + property: "isLast" + value: root.isLast + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "sectionSpacing" in root.item + property: "sectionSpacing" + value: root.sectionSpacing + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isLeftBarEdge" in root.item + property: "isLeftBarEdge" + value: root.isLeftBarEdge + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isRightBarEdge" in root.item + property: "isRightBarEdge" + value: root.isRightBarEdge + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isTopBarEdge" in root.item + property: "isTopBarEdge" + value: root.isTopBarEdge + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "isBottomBarEdge" in root.item + property: "isBottomBarEdge" + value: root.isBottomBarEdge + restoreMode: Binding.RestoreNone + } + + onLoaded: { + if (!item) + return; + + contentItemReady(item); + + if (axis && "isVertical" in item) { + try { + item.isVertical = axis.isVertical; + } catch (e) {} + } + + if (item.pluginService !== undefined) { + var parts = widgetId.split(":"); + var pluginId = parts[0]; + var variantId = parts.length > 1 ? parts[1] : null; + + if (item.pluginId !== undefined) + item.pluginId = pluginId; + if (item.variantId !== undefined) + item.variantId = variantId; + if (item.variantData !== undefined && variantId) + item.variantData = PluginService.getPluginVariantData(pluginId, variantId); + item.pluginService = PluginService; + } + + if (item.popoutService !== undefined) + item.popoutService = PopoutService; + + registerWidgetIfEligible(); + } + + Component.onDestruction: { + unregisterWidget(); + } + + function registerWidgetIfEligible() { + if (!item || !widgetId || !parentScreen?.name) + return; + + const hasPopout = item.popoutTarget !== undefined || typeof item.triggerPopout === "function" || typeof item.clicked === "function"; + if (!hasPopout) + return; + + _registeredScreenName = parentScreen.name; + BarWidgetService.registerWidget(widgetId, _registeredScreenName, item); + } + + function unregisterWidget() { + if (!widgetId || !_registeredScreenName) + return; + + BarWidgetService.unregisterWidget(widgetId, _registeredScreenName); + _registeredScreenName = ""; + } + + function getWidgetComponent(widgetId, components) { + const componentMap = { + "launcherButton": components.launcherButtonComponent, + "workspaceSwitcher": components.workspaceSwitcherComponent, + "focusedWindow": components.focusedWindowComponent, + "runningApps": components.runningAppsComponent, + "clock": components.clockComponent, + "music": components.mediaComponent, + "weather": components.weatherComponent, + "systemTray": components.systemTrayComponent, + "privacyIndicator": components.privacyIndicatorComponent, + "clipboard": components.clipboardComponent, + "cpuUsage": components.cpuUsageComponent, + "memUsage": components.memUsageComponent, + "diskUsage": components.diskUsageComponent, + "cpuTemp": components.cpuTempComponent, + "gpuTemp": components.gpuTempComponent, + "notificationButton": components.notificationButtonComponent, + "battery": components.batteryComponent, + "controlCenterButton": components.controlCenterButtonComponent, + "capsLockIndicator": components.capsLockIndicatorComponent, + "idleInhibitor": components.idleInhibitorComponent, + "spacer": components.spacerComponent, + "separator": components.separatorComponent, + "network_speed_monitor": components.networkComponent, + "keyboard_layout_name": components.keyboardLayoutNameComponent, + "vpn": components.vpnComponent, + "notepadButton": components.notepadButtonComponent, + "colorPicker": components.colorPickerComponent, + "systemUpdate": components.systemUpdateComponent, + "layout": components.layoutComponent, + "powerMenuButton": components.powerMenuButtonComponent, + "appsDock": components.appsDockComponent + }; + + if (componentMap[widgetId]) { + return componentMap[widgetId]; + } + + var parts = widgetId.split(":"); + var pluginId = parts[0]; + + let pluginMap = PluginService.getWidgetComponents(); + return pluginMap[pluginId] || null; + } + + function getWidgetVisible(widgetId, dgopAvailable) { + const widgetVisibility = { + "cpuUsage": dgopAvailable, + "memUsage": dgopAvailable, + "cpuTemp": dgopAvailable, + "gpuTemp": dgopAvailable, + "network_speed_monitor": dgopAvailable, + "layout": CompositorService.isDwl && DwlService.dwlAvailable + }; + + return widgetVisibility[widgetId] ?? true; + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml new file mode 100644 index 0000000..294102d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDock.qml @@ -0,0 +1,1011 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + enableBackgroundHover: false + enableCursor: false + section: "left" + + property var widgetData: null + property var hoveredItem: null + property var topBar: null + property bool isAutoHideBar: false + property Item windowRoot: (Window.window ? Window.window.contentItem : null) + + property int draggedIndex: -1 + property int dropTargetIndex: -1 + property bool suppressShiftAnimation: false + property int pinnedAppCount: 0 + + property int maxVisibleApps: widgetData?.barMaxVisibleApps !== undefined ? widgetData.barMaxVisibleApps : SettingsData.barMaxVisibleApps + property int maxVisibleRunningApps: widgetData?.barMaxVisibleRunningApps !== undefined ? widgetData.barMaxVisibleRunningApps : SettingsData.barMaxVisibleRunningApps + property bool showOverflowBadge: widgetData?.barShowOverflowBadge !== undefined ? widgetData.barShowOverflowBadge : SettingsData.barShowOverflowBadge + property bool overflowExpanded: false + property int overflowItemCount: 0 + + onMaxVisibleAppsChanged: updateModel() + onMaxVisibleRunningAppsChanged: updateModel() + + readonly property real effectiveBarThickness: { + if (barThickness > 0 && barSpacing > 0) { + return barThickness + barSpacing; + } + const innerPadding = barConfig?.innerPadding ?? 4; + const spacing = barConfig?.spacing ?? 4; + return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing; + } + + readonly property var barBounds: { + if (!parentScreen || !barConfig) { + return { + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "wingSize": 0 + }; + } + const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1)); + return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig); + } + + readonly property real barY: barBounds.y + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + return effectiveBarThickness; + } + + return 0; + } + + // --- Dock Logic Helpers --- + function movePinnedApp(fromDockIndex, toDockIndex) { + if (fromDockIndex === toDockIndex) + return; + + const currentPinned = [...(SessionData.barPinnedApps || [])]; + if (fromDockIndex < 0 || fromDockIndex >= currentPinned.length || toDockIndex < 0 || toDockIndex >= currentPinned.length) { + return; + } + + const movedApp = currentPinned.splice(fromDockIndex, 1)[0]; + currentPinned.splice(toDockIndex, 0, movedApp); + + SessionData.setBarPinnedApps(currentPinned); + } + + property int _desktopEntriesUpdateTrigger: 0 + property int _toplevelsUpdateTrigger: 0 + property int _appIdSubstitutionsTrigger: 0 + + Connections { + target: CompositorService + function onToplevelsChanged() { + _toplevelsUpdateTrigger++; + updateModel(); + } + } + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + _desktopEntriesUpdateTrigger++; + } + } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + _appIdSubstitutionsTrigger++; + updateModel(); + } + function onRunningAppsCurrentWorkspaceChanged() { + updateModel(); + } + function onBarMaxVisibleAppsChanged() { + updateModel(); + } + function onBarMaxVisibleRunningAppsChanged() { + updateModel(); + } + } + + Connections { + target: SessionData + function onBarPinnedAppsChanged() { + root.suppressShiftAnimation = true; + root.draggedIndex = -1; + root.dropTargetIndex = -1; + updateModel(); + Qt.callLater(() => { + root.suppressShiftAnimation = false; + }); + } + } + + property var dockItems: [] + + function isOnScreen(toplevel, screenName) { + if (!toplevel.screens) + return false; + for (let i = 0; i < toplevel.screens.length; i++) { + if (toplevel.screens[i]?.name === screenName) + return true; + } + return false; + } + + function getCoreAppData(appId) { + if (typeof AppSearchService === "undefined") + return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].builtInPluginId === appId) + return coreApps[i]; + } + return null; + } + + function getCoreAppDataByTitle(windowTitle) { + if (typeof AppSearchService === "undefined" || !windowTitle) + return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].name === windowTitle) + return coreApps[i]; + } + return null; + } + + function createSeparator(key) { + return { + uniqueKey: key, + type: "separator", + appId: "__SEPARATOR__", + toplevel: null, + isPinned: false, + isRunning: false, + isInOverflow: false + }; + } + + function markAsOverflow(item) { + return { + uniqueKey: item.uniqueKey, + type: item.type, + appId: item.appId, + toplevel: item.toplevel, + isPinned: item.isPinned, + isRunning: item.isRunning, + windowCount: item.windowCount, + allWindows: item.allWindows, + isCoreApp: item.isCoreApp, + coreAppData: item.coreAppData, + isInOverflow: true + }; + } + + function markAsVisible(item) { + return { + uniqueKey: item.uniqueKey, + type: item.type, + appId: item.appId, + toplevel: item.toplevel, + isPinned: item.isPinned, + isRunning: item.isRunning, + windowCount: item.windowCount, + allWindows: item.allWindows, + isCoreApp: item.isCoreApp, + coreAppData: item.coreAppData, + isInOverflow: false + }; + } + + function buildBaseItems() { + const items = []; + const pinnedApps = [...(SessionData.barPinnedApps || [])]; + _toplevelsUpdateTrigger; + const allToplevels = CompositorService.sortedToplevels; + + let sortedToplevels = allToplevels; + if (SettingsData.runningAppsCurrentWorkspace && parentScreen) { + sortedToplevels = CompositorService.filterCurrentWorkspace(allToplevels, parentScreen.name) || []; + } + + const appGroups = new Map(); + + pinnedApps.forEach(rawAppId => { + const appId = Paths.moddedAppId(rawAppId); + const coreAppData = getCoreAppData(appId); + appGroups.set(appId, { + appId: appId, + isPinned: true, + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + }); + + sortedToplevels.forEach((toplevel, index) => { + const rawAppId = toplevel.appId || "unknown"; + let appId = Paths.moddedAppId(rawAppId); + + let coreAppData = null; + if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") { + coreAppData = getCoreAppDataByTitle(toplevel.title); + if (coreAppData) { + appId = coreAppData.builtInPluginId; + } + } + + if (!appGroups.has(appId)) { + appGroups.set(appId, { + appId: appId, + isPinned: false, + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + } + + appGroups.get(appId).windows.push({ + toplevel: toplevel, + index: index, + windowTitle: toplevel.title + }); + }); + + const pinnedGroups = []; + const unpinnedGroups = []; + + Array.from(appGroups.entries()).forEach(([appId, group]) => { + const firstWindow = group.windows.length > 0 ? group.windows[0] : null; + + const item = { + uniqueKey: "grouped_" + appId, + type: "grouped", + appId: appId, + toplevel: firstWindow ? firstWindow.toplevel : null, + isPinned: group.isPinned, + isRunning: group.windows.length > 0, + windowCount: group.windows.length, + allWindows: group.windows, + isCoreApp: group.isCoreApp || false, + coreAppData: group.coreAppData || null, + isInOverflow: false + }; + + if (group.isPinned) { + pinnedGroups.push(item); + } else { + unpinnedGroups.push(item); + } + }); + + pinnedGroups.forEach(item => items.push(item)); + + if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) { + items.push(createSeparator("separator_grouped")); + } + + unpinnedGroups.forEach(item => items.push(item)); + + root.pinnedAppCount = pinnedGroups.length; + return { + items, + pinnedCount: pinnedGroups.length, + runningCount: unpinnedGroups.length + }; + } + + function applyOverflow(baseResult) { + const { + items + } = baseResult; + const maxPinned = root.maxVisibleApps; + const maxRunning = root.maxVisibleRunningApps; + + const pinnedItems = items.filter(i => i.type === "grouped" && i.isPinned); + const runningItems = items.filter(i => i.type === "grouped" && i.isRunning && !i.isPinned); + + const pinnedOverflow = maxPinned > 0 && pinnedItems.length > maxPinned; + const runningOverflow = maxRunning > 0 && runningItems.length > maxRunning; + + if (!pinnedOverflow && !runningOverflow) { + root.overflowItemCount = 0; + return items.map(i => markAsVisible(i)); + } + + const visiblePinnedKeys = new Set(pinnedOverflow ? pinnedItems.slice(0, maxPinned).map(i => i.uniqueKey) : pinnedItems.map(i => i.uniqueKey)); + const visibleRunningKeys = new Set(runningOverflow ? runningItems.slice(0, maxRunning).map(i => i.uniqueKey) : runningItems.map(i => i.uniqueKey)); + + const overflowPinnedCount = pinnedOverflow ? pinnedItems.length - maxPinned : 0; + const overflowRunningCount = runningOverflow ? runningItems.length - maxRunning : 0; + const totalOverflow = overflowPinnedCount + overflowRunningCount; + root.overflowItemCount = totalOverflow; + + const finalItems = []; + let addedSeparator = false; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + switch (item.type) { + case "separator": + break; + case "grouped": + if (item.isPinned) { + if (visiblePinnedKeys.has(item.uniqueKey)) { + finalItems.push(markAsVisible(item)); + } else { + finalItems.push(markAsOverflow(item)); + } + } else if (item.isRunning) { + if (!addedSeparator && finalItems.length > 0) { + finalItems.push(createSeparator("separator_overflow")); + addedSeparator = true; + } + if (visibleRunningKeys.has(item.uniqueKey)) { + finalItems.push(markAsVisible(item)); + } else { + finalItems.push(markAsOverflow(item)); + } + } + break; + default: + finalItems.push(item); + break; + } + } + + if (totalOverflow > 0) { + const toggleIndex = finalItems.findIndex(i => i.type === "separator"); + const insertPos = toggleIndex >= 0 ? toggleIndex : finalItems.length; + finalItems.splice(insertPos, 0, { + uniqueKey: "overflow_toggle", + type: "overflow-toggle", + appId: "__OVERFLOW_TOGGLE__", + toplevel: null, + isPinned: false, + isRunning: false, + isInOverflow: false, + overflowCount: totalOverflow + }); + } + + return finalItems; + } + + function updateModel() { + const baseResult = buildBaseItems(); + dockItems = applyOverflow(baseResult); + } + + Component.onCompleted: updateModel() + + visible: dockItems.length > 0 + readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6 + + content: Component { + Item { + implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 + implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 + + Loader { + id: layoutLoader + anchors.centerIn: parent + sourceComponent: root.isVerticalOrientation ? columnLayout : rowLayout + } + } + } + + Component { + id: rowLayout + Row { + spacing: Theme.spacingXS + + Repeater { + id: repeater + model: ScriptModel { + values: root.dockItems + objectProp: "uniqueKey" + } + + delegate: dockDelegate + } + } + } + + Component { + id: columnLayout + Column { + spacing: Theme.spacingXS + + Repeater { + model: ScriptModel { + values: root.dockItems + objectProp: "uniqueKey" + } + delegate: dockDelegate + } + } + } + + Loader { + id: tooltipLoader + active: false + sourceComponent: DankTooltip {} + } + + Component { + id: dockDelegate + Item { + id: delegateItem + property bool isSeparator: modelData.type === "separator" + readonly property bool isOverflowToggle: modelData.type === "overflow-toggle" + readonly property bool isInOverflow: modelData.isInOverflow === true + + readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)) + readonly property real visualWidth: root.isVerticalOrientation ? root.barThickness : visualSize + readonly property real visualHeight: root.isVerticalOrientation ? visualSize : root.barThickness + + visible: !isInOverflow || root.overflowExpanded + opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1 + scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1 + + width: (isInOverflow && !root.overflowExpanded) ? 0 : visualWidth + height: (isInOverflow && !root.overflowExpanded) ? 0 : visualHeight + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + z: (dragHandler.dragging) ? 100 : 0 + + // --- Drag and Drop Shift Animation Logic --- + property real shiftOffset: { + if (root.draggedIndex < 0 || !modelData.isPinned || isSeparator) + return 0; + if (index === root.draggedIndex) + return 0; + + const dragIdx = root.draggedIndex; + const dropIdx = root.dropTargetIndex; + const myIdx = index; + const shiftAmount = visualSize + Theme.spacingXS; + + if (dropIdx < 0) + return 0; + if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) + return -shiftAmount; + if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) + return shiftAmount; + return 0; + } + + transform: Translate { + x: root.isVerticalOrientation ? 0 : delegateItem.shiftOffset + y: root.isVerticalOrientation ? delegateItem.shiftOffset : 0 + + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + Behavior on y { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + visible: isSeparator + width: root.isVerticalOrientation ? root.barThickness * 0.6 : 2 + height: root.isVerticalOrientation ? 2 : root.barThickness * 0.6 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + radius: 1 + anchors.centerIn: parent + } + + AppsDockOverflowButton { + visible: isOverflowToggle + anchors.centerIn: parent + width: delegateItem.visualWidth + height: delegateItem.visualHeight + iconSize: 24 + overflowCount: modelData.overflowCount || 0 + overflowExpanded: root.overflowExpanded + isVertical: root.isVerticalOrientation + showBadge: root.showOverflowBadge + z: 10 + onClicked: { + console.log("Overflow button clicked! Current state:", root.overflowExpanded); + root.overflowExpanded = !root.overflowExpanded; + console.log("New state:", root.overflowExpanded); + } + } + + Item { + id: appItem + visible: !isSeparator && !isOverflowToggle + anchors.fill: parent + + property bool isFocused: { + if (modelData.type === "grouped") { + return modelData.allWindows.some(w => w.toplevel && w.toplevel.activated); + } + return modelData.toplevel ? modelData.toplevel.activated : false; + } + + property var appId: modelData.appId + property int windowCount: modelData.windowCount || (modelData.isRunning ? 1 : 0) + property string windowTitle: { + if (modelData.type === "grouped") { + const active = modelData.allWindows.find(w => w.toplevel && w.toplevel.activated); + if (active) + return active.windowTitle || "(Unnamed)"; + if (modelData.allWindows.length > 0) + return modelData.allWindows[0].windowTitle || "(Unnamed)"; + return ""; + } + return modelData.toplevel ? (modelData.toplevel.title || "(Unnamed)") : ""; + } + + property string tooltipText: { + root._desktopEntriesUpdateTrigger; + const desktopEntry = appId ? DesktopEntries.heuristicLookup(appId) : null; + const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown"; + + if (modelData.type === "grouped" && windowCount > 1) { + return appName + " (" + windowCount + " windows)"; + } + return appName + (windowTitle ? " • " + windowTitle : ""); + } + + readonly property bool enlargeEnabled: (widgetData?.appsDockEnlargeOnHover !== undefined ? widgetData.appsDockEnlargeOnHover : SettingsData.appsDockEnlargeOnHover) + readonly property real enlargeScale: enlargeEnabled && mouseArea.containsMouse ? (widgetData?.appsDockEnlargePercentage !== undefined ? widgetData.appsDockEnlargePercentage : SettingsData.appsDockEnlargePercentage) / 100.0 : 1.0 + readonly property real baseIconSizeMultiplier: (widgetData?.appsDockIconSizePercentage !== undefined ? widgetData.appsDockIconSizePercentage : SettingsData.appsDockIconSizePercentage) / 100.0 + readonly property real effectiveIconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) * baseIconSizeMultiplier + + readonly property color activeOverlayColor: { + switch (SettingsData.appsDockActiveColorMode) { + case "secondary": + return Theme.secondary; + case "primaryContainer": + return Theme.primaryContainer; + case "error": + return Theme.error; + case "success": + return Theme.success; + default: + return Theme.primary; + } + } + + transform: Translate { + x: (dragHandler.dragging && !root.isVerticalOrientation) ? dragHandler.dragAxisOffset : 0 + y: (dragHandler.dragging && root.isVerticalOrientation) ? dragHandler.dragAxisOffset : 0 + } + + Rectangle { + id: visualContent + width: root.isVerticalOrientation ? root.iconCellSize : delegateItem.visualSize + height: root.isVerticalOrientation ? delegateItem.visualSize : root.iconCellSize + anchors.centerIn: parent + radius: Theme.cornerRadius + color: { + const colorizeEnabled = (widgetData?.appsDockColorizeActive !== undefined ? widgetData.appsDockColorizeActive : SettingsData.appsDockColorizeActive); + + if (appItem.isFocused && colorizeEnabled) { + return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3); + } + return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; + } + + border.width: dragHandler.dragging ? 2 : 0 + border.color: Theme.primary + opacity: dragHandler.dragging ? 0.8 : 1.0 + + AppIconRenderer { + id: coreIcon + readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left + anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS + anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined + anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0 + anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined + + iconSize: appItem.effectiveIconSize + materialIconSizeAdjustment: 0 + iconValue: { + if (!modelData || !modelData.isCoreApp || !modelData.coreAppData) + return ""; + const appId = modelData.coreAppData.id || modelData.coreAppData.builtInPluginId; + if ((appId === "dms_settings" || appId === "dms_notepad" || appId === "dms_sysmon") && modelData.coreAppData.cornerIcon) { + return "material:" + modelData.coreAppData.cornerIcon; + } + return modelData.coreAppData.icon || ""; + } + colorOverride: Theme.widgetIconColor + fallbackText: "?" + visible: iconValue !== "" + z: 2 + + transformOrigin: Item.Center + scale: appItem.enlargeScale + Behavior on scale { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + IconImage { + id: iconImg + readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left + anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS + anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined + anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0 + anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined + + width: appItem.effectiveIconSize + height: appItem.effectiveIconSize + source: { + root._desktopEntriesUpdateTrigger; + root._appIdSubstitutionsTrigger; + if (!appItem.appId) + return ""; + if (modelData.isCoreApp) + return ""; + const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId); + return Paths.getAppIcon(appItem.appId, desktopEntry); + } + smooth: true + mipmap: true + asynchronous: true + visible: status === Image.Ready && !coreIcon.visible + layer.enabled: appItem.appId === "org.quickshell" || appItem.appId === "com.danklinux.dms" + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + z: 2 + + transformOrigin: Item.Center + scale: appItem.enlargeScale + Behavior on scale { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + DankIcon { + readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + anchors.left: (root.isVerticalOrientation || isCompact) ? undefined : parent.left + anchors.leftMargin: (root.isVerticalOrientation || isCompact) ? 0 : Theme.spacingXS + anchors.top: (root.isVerticalOrientation && !isCompact) ? parent.top : undefined + anchors.topMargin: (root.isVerticalOrientation && !isCompact) ? Theme.spacingXS : 0 + anchors.centerIn: (root.isVerticalOrientation || isCompact) ? parent : undefined + + size: appItem.effectiveIconSize + name: "sports_esports" + color: Theme.widgetTextColor + visible: !iconImg.visible && !coreIcon.visible && Paths.isSteamApp(appItem.appId) + + transformOrigin: Item.Center + scale: appItem.enlargeScale + Behavior on scale { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + Text { + anchors.centerIn: parent + visible: !iconImg.visible && !coreIcon.visible && !Paths.isSteamApp(appItem.appId) + text: { + root._desktopEntriesUpdateTrigger; + if (!appItem.appId) + return "?"; + const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId); + const appName = Paths.getAppName(appItem.appId, desktopEntry); + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.widgetTextColor + + transformOrigin: Item.Center + scale: appItem.enlargeScale + Behavior on scale { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2 + anchors.bottomMargin: -2 + width: 14 + height: 14 + radius: 7 + color: Theme.primary + visible: modelData.type === "grouped" && appItem.windowCount > 1 && (widgetData?.barShowOverflowBadge !== undefined ? widgetData.barShowOverflowBadge : SettingsData.barShowOverflowBadge) + z: 10 + + StyledText { + anchors.centerIn: parent + text: appItem.windowCount > 9 ? "9+" : appItem.windowCount + font.pixelSize: 9 + color: Theme.surface + } + } + + StyledText { + visible: !root.isVerticalOrientation && !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + anchors.left: iconImg.right + anchors.leftMargin: Theme.spacingXS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: appItem.windowTitle || appItem.appId + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + elide: Text.ElideRight + maximumLineCount: 1 + } + + Rectangle { + visible: modelData.isRunning && !(widgetData?.appsDockHideIndicators !== undefined ? widgetData.appsDockHideIndicators : SettingsData.appsDockHideIndicators) + width: root.isVerticalOrientation ? 2 : 20 + height: root.isVerticalOrientation ? 20 : 2 + radius: 1 + color: appItem.isFocused ? Theme.primary : Theme.surfaceText + opacity: appItem.isFocused ? 1 : 0.5 + + anchors.bottom: root.isVerticalOrientation ? undefined : parent.bottom + anchors.right: root.isVerticalOrientation ? parent.right : undefined + anchors.horizontalCenter: root.isVerticalOrientation ? undefined : parent.horizontalCenter + anchors.verticalCenter: root.isVerticalOrientation ? parent.verticalCenter : undefined + + anchors.margins: 0 + z: 5 + } + } + } + + // Handler for Drag Logic + 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: 500 + repeat: false + onTriggered: { + if (modelData.isPinned) { + dragHandler.longPressing = true; + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && modelData.isPinned) { + 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.movePinnedApp(root.draggedIndex, root.dropTargetIndex); + } + + dragHandler.longPressing = false; + dragHandler.dragging = false; + dragHandler.dragAxisOffset = 0; + root.draggedIndex = -1; + root.dropTargetIndex = -1; + + if (wasDragging || mouse.button !== Qt.LeftButton) + return; + + if (wasDragging || mouse.button !== Qt.LeftButton) + return; + + if (modelData.type === "grouped") { + if (modelData.windowCount === 0) { + if (modelData.isCoreApp && modelData.coreAppData) { + AppSearchService.executeCoreApp(modelData.coreAppData); + } else { + const moddedId = Paths.moddedAppId(modelData.appId); + const desktopEntry = DesktopEntries.heuristicLookup(moddedId); + if (desktopEntry) + SessionService.launchDesktopEntry(desktopEntry); + } + } else if (modelData.windowCount === 1) { + if (modelData.allWindows[0].toplevel) + modelData.allWindows[0].toplevel.activate(); + } else { + let currentIndex = -1; + for (var i = 0; i < modelData.allWindows.length; i++) { + if (modelData.allWindows[i].toplevel.activated) { + currentIndex = i; + break; + } + } + const nextIndex = (currentIndex + 1) % modelData.allWindows.length; + modelData.allWindows[nextIndex].toplevel.activate(); + } + } + } + + onPositionChanged: mouse => { + if (dragHandler.longPressing && !dragHandler.dragging) { + const distance = Math.sqrt(Math.pow(mouse.x - dragHandler.dragStartPos.x, 2) + Math.pow(mouse.y - dragHandler.dragStartPos.y, 2)); + if (distance > 5) { + dragHandler.dragging = true; + root.draggedIndex = index; + root.dropTargetIndex = index; + } + } + + if (!dragHandler.dragging) + return; + + const axisOffset = root.isVerticalOrientation ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x); + dragHandler.dragAxisOffset = axisOffset; + + const itemSize = (root.isVerticalOrientation ? delegateItem.height : delegateItem.width) + Theme.spacingXS; + const slotOffset = Math.round(axisOffset / itemSize); + const newTargetIndex = Math.max(0, Math.min(root.pinnedAppCount - 1, index + slotOffset)); + + if (newTargetIndex !== root.dropTargetIndex) { + root.dropTargetIndex = newTargetIndex; + } + } + + onEntered: { + root.hoveredItem = delegateItem; + if (isSeparator || isOverflowToggle) + return; + + tooltipLoader.active = true; + if (tooltipLoader.item) { + if (root.isVerticalOrientation) { + const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const isLeft = root.axis?.edge === "left"; + const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + const screenRelativeY = globalPos.y - screenY + root.minTooltipY; + tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft); + } else { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height); + const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; + const isBottom = root.axis?.edge === "bottom"; + const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); + tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false); + } + } + } + onExited: { + if (root.hoveredItem === delegateItem) { + root.hoveredItem = null; + if (tooltipLoader.item) + tooltipLoader.item.hide(); + tooltipLoader.active = false; + } + } + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + contextMenuLoader.active = true; + + if (contextMenuLoader.item) { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const isBarVertical = root.axis?.isVertical ?? false; + const barEdge = root.axis?.edge ?? "top"; + + let x = globalPos.x - screenX; + let y = globalPos.y - screenY; + + switch (barEdge) { + case "bottom": + y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.barThickness - root.barSpacing; + break; + case "top": + y = root.barThickness + root.barSpacing; + break; + case "left": + x = root.barThickness + root.barSpacing; + break; + case "right": + x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.barThickness - root.barSpacing; + break; + } + + const shouldHidePin = modelData.appId === "org.quickshell" || modelData.appId === "com.danklinux.dms"; + const moddedId = Paths.moddedAppId(modelData.appId); + const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null; + + contextMenuLoader.item.showAt(x, y, isBarVertical, barEdge, modelData, shouldHidePin, desktopEntry, root.parentScreen); + } + } + } + } + } + } + } + + Loader { + id: contextMenuLoader + active: false + source: "AppsDockContextMenu.qml" + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml new file mode 100644 index 0000000..46c4f69 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml @@ -0,0 +1,486 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + WlrLayershell.namespace: "dms:dock-context-menu" + + property var appData: null + property var anchorItem: null + property int margin: 10 + property bool hidePin: false + property var desktopEntry: null + property bool isDmsWindow: appData?.appId === "org.quickshell" || appData?.appId === "com.danklinux.dms" + + property bool isVertical: false + property string edge: "top" + property point anchorPos: Qt.point(0, 0) + + function showAt(x, y, vertical, barEdge, data, hidePinOption, entry, targetScreen) { + if (targetScreen) { + root.screen = targetScreen; + } + + anchorPos = Qt.point(x, y); + isVertical = vertical ?? false; + edge = barEdge ?? "top"; + + appData = data; + hidePin = hidePinOption || false; + desktopEntry = entry || null; + + visible = true; + + if (targetScreen) { + TrayMenuManager.registerMenu(targetScreen.name, root); + } + } + + function close() { + visible = false; + + if (root.screen) { + TrayMenuManager.unregisterMenu(root.screen.name); + } + } + + screen: null + visible: false + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + anchors { + top: true + left: true + right: true + bottom: true + } + + Component.onDestruction: { + if (root.screen) { + TrayMenuManager.unregisterMenu(root.screen.name); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + root.close(); + } + } + + Rectangle { + id: menuContainer + + x: { + if (root.isVertical) { + if (root.edge === "left") { + return Math.min(root.width - width - 10, root.anchorPos.x); + } else { + return Math.max(10, root.anchorPos.x - width); + } + } else { + const left = 10; + const right = root.width - width - 10; + const want = root.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + } + y: { + if (root.isVertical) { + const top = 10; + const bottom = root.height - height - 10; + const want = root.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); + } else { + if (root.edge === "top") { + return Math.min(root.height - height - 10, root.anchorPos.y); + } else { + return Math.max(10, root.anchorPos.y - height); + } + } + } + + width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)) + height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + opacity: root.visible ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: -1 + } + + Column { + id: menuColumn + width: parent.width - Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + spacing: 1 + + // Window list for grouped apps + Repeater { + model: { + if (!root.appData || root.appData.type !== "grouped") + return []; + + const toplevels = []; + const allToplevels = ToplevelManager.toplevels.values; + for (let i = 0; i < allToplevels.length; i++) { + const toplevel = allToplevels[i]; + if (toplevel.appId === root.appData.appId) { + toplevels.push(toplevel); + } + } + return toplevels; + } + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: closeButton.left + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Rectangle { + id: closeButton + anchors.right: parent.right + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: 20 + height: 20 + radius: 10 + color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData && modelData.close) { + modelData.close(); + } + root.close(); + } + } + } + + DankRipple { + id: windowRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: windowArea + anchors.fill: parent + anchors.rightMargin: 24 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => windowRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (modelData && modelData.activate) { + modelData.activate(); + } + root.close(); + } + } + } + } + + Rectangle { + visible: { + if (!root.appData) + return false; + if (root.appData.type !== "grouped") + return false; + return root.appData.windowCount > 0; + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Repeater { + model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : [] + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: actionArea.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.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Item { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + visible: modelData.icon && modelData.icon !== "" + + IconImage { + anchors.fill: parent + source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : "" + smooth: true + asynchronous: true + visible: status === Image.Ready + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + DankRipple { + id: actionRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: actionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => actionRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (modelData) { + SessionService.launchDesktopAction(root.desktopEntry, modelData); + } + root.close(); + } + } + } + } + + Rectangle { + visible: { + if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) { + return false; + } + return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand); + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Rectangle { + visible: !root.hidePin + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + DankRipple { + id: pinRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: pinArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (!root.appData) { + return; + } + if (root.appData.isPinned) { + SessionData.removeBarPinnedApp(root.appData.appId); + } else { + SessionData.addBarPinnedApp(root.appData.appId); + } + root.close(); + } + } + } + + Rectangle { + visible: { + const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand; + const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)); + const hasPinOption = !root.hidePin; + const hasContentAbove = hasPinOption || hasNvidia; + return hasContentAbove && hasWindow; + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Rectangle { + visible: !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Launch on dGPU") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + DankRipple { + id: nvidiaRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: nvidiaArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => nvidiaRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (root.desktopEntry) { + SessionService.launchDesktopEntry(root.desktopEntry, true); + } + root.close(); + } + } + } + + Rectangle { + visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)) + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: { + if (root.appData && root.appData.type === "grouped") { + return I18n.tr("Close All Windows"); + } + return I18n.tr("Close Window"); + } + font.pixelSize: Theme.fontSizeSmall + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + DankRipple { + id: closeRipple + rippleColor: Theme.error + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => closeRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (root.appData?.type === "window") { + root.appData?.toplevel?.close(); + } else if (root.appData?.type === "grouped") { + root.appData?.allWindows?.forEach(window => window.toplevel?.close()); + } + root.close(); + } + } + } + } + } + + MouseArea { + anchors.fill: parent + z: -1 + onClicked: root.close() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml new file mode 100644 index 0000000..82c78d0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AppsDockOverflowButton.qml @@ -0,0 +1,76 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: root + + property real iconSize: 24 + property int overflowCount: 0 + property bool overflowExpanded: false + property bool isVertical: false + property bool showBadge: true + + signal clicked + + Rectangle { + id: buttonBackground + anchors.centerIn: parent + width: root.iconSize + height: root.iconSize + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, mouseArea.containsMouse ? 0.2 : 0.1) + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + + DankIcon { + id: arrowIcon + anchors.centerIn: parent + size: root.iconSize * 0.6 + name: "expand_more" + color: Theme.widgetIconColor + rotation: isVertical ? (overflowExpanded ? 180 : 0) : (overflowExpanded ? 90 : -90) + + Behavior on rotation { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + } + + Rectangle { + visible: overflowCount > 0 && !overflowExpanded && root.showBadge + anchors.right: buttonBackground.right + anchors.top: buttonBackground.top + anchors.rightMargin: -4 + anchors.topMargin: -4 + width: Math.max(18, badgeText.width + 8) + height: 18 + radius: 9 + color: Theme.primary + z: 10 + + StyledText { + id: badgeText + anchors.centerIn: parent + text: `+${overflowCount}` + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.onPrimary + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml new file mode 100644 index 0000000..52ace33 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/AudioVisualization.qml @@ -0,0 +1,94 @@ +import QtQuick +import Quickshell.Services.Mpris +import qs.Common +import qs.Services + +Item { + id: root + + readonly property MprisPlayer activePlayer: MprisController.activePlayer + readonly property bool hasActiveMedia: activePlayer !== null + readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing + + width: 20 + height: Theme.iconSize + + Loader { + active: isPlaying + + sourceComponent: Component { + Ref { + service: CavaService + } + } + } + + readonly property real maxBarHeight: Theme.iconSize - 2 + readonly property real minBarHeight: 3 + readonly property real heightRange: maxBarHeight - minBarHeight + property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight] + + Timer { + id: fallbackTimer + + running: !CavaService.cavaAvailable && isPlaying + interval: 500 + repeat: true + onTriggered: { + CavaService.values = [Math.random() * 20 + 5, Math.random() * 25 + 8, Math.random() * 22 + 6, Math.random() * 20 + 5, Math.random() * 22 + 6, Math.random() * 25 + 8]; + } + } + + Connections { + target: CavaService + function onValuesChanged() { + if (!root.isPlaying) { + root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight]; + return; + } + + const newHeights = []; + for (let i = 0; i < 6; i++) { + if (CavaService.values.length <= i) { + newHeights.push(root.minBarHeight); + continue; + } + + const rawLevel = CavaService.values[i]; + if (rawLevel <= 0) { + newHeights.push(root.minBarHeight); + } else if (rawLevel >= 100) { + newHeights.push(root.maxBarHeight); + } else { + newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange); + } + } + root.barHeights = newHeights; + } + } + + Row { + anchors.centerIn: parent + spacing: 1.5 + + Repeater { + model: 6 + + Rectangle { + width: 2 + height: root.barHeights[index] + radius: 1.5 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + Behavior on height { + enabled: root.isPlaying && !CavaService.cavaAvailable + NumberAnimation { + duration: 100 + easing.type: Easing.Linear + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml new file mode 100644 index 0000000..51d4b54 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Battery.qml @@ -0,0 +1,165 @@ +import QtQuick +import Quickshell.Services.UPower +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: battery + + property bool batteryPopupVisible: false + property var popoutTarget: null + + property real touchpadAccumulator: 0 + + readonly property int barPosition: { + switch (axis?.edge) { + case "top": + return 0; + case "bottom": + return 1; + case "left": + return 2; + case "right": + return 3; + default: + return 0; + } + } + + signal toggleBatteryPopup + + visible: true + + content: Component { + Item { + implicitWidth: battery.isVerticalOrientation ? (battery.widgetThickness - battery.horizontalPadding * 2) : batteryContent.implicitWidth + implicitHeight: battery.isVerticalOrientation ? batteryColumn.implicitHeight : (battery.widgetThickness - battery.horizontalPadding * 2) + + Column { + id: batteryColumn + visible: battery.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: BatteryService.getBatteryIcon() + size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (!BatteryService.batteryAvailable) { + return Theme.widgetIconColor; + } + + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary; + } + + return Theme.widgetIconColor; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: BatteryService.batteryLevel.toString() + font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + visible: BatteryService.batteryAvailable + } + } + + Row { + id: batteryContent + visible: !battery.isVerticalOrientation + anchors.centerIn: parent + spacing: (barConfig?.noBackground ?? false) ? 1 : 2 + + DankIcon { + name: BatteryService.getBatteryIcon() + size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (!BatteryService.batteryAvailable) { + return Theme.widgetIconColor; + } + + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error; + } + + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary; + } + + return Theme.widgetIconColor; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: `${BatteryService.batteryLevel}%` + font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + } + } + } + } + + MouseArea { + x: -battery.leftMargin + y: -battery.topMargin + width: battery.width + battery.leftMargin + battery.rightMargin + height: battery.height + battery.topMargin + battery.bottomMargin + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + battery.triggerRipple(this, mouse.x, mouse.y); + toggleBatteryPopup(); + } + onWheel: wheel => { + var delta = wheel.angleDelta.y; + if (delta === 0) + return; + + // Check if this is a touchpad + if (delta !== 120 && delta !== -120) { + touchpadAccumulator += delta; + console.info("Acc: "+touchpadAccumulator); + if (Math.abs(touchpadAccumulator) < 500) + return; + delta = touchpadAccumulator; + touchpadAccumulator = 0; + } + console.info("Trigger! Delta: "+delta) + + // This is after the other delta checks so it only shows on valid Y scroll + if (typeof PowerProfiles === "undefined") { + ToastService.showError("power-profiles-daemon not available"); + return; + } + + // Get list of profiles, and current index + const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []); + var index = profiles.findIndex(profile => PowerProfiles.profile === profile); + + // Step once based on mouse wheel direction + if (delta > 0) index += 1; + else index -= 1; + + // Already at end of list, can't go further + if (index < 0 || index >= profiles.length) return; + + // Set new profile + PowerProfiles.profile = profiles[index]; + if (PowerProfiles.profile !== profiles[index]) { + ToastService.showError("Failed to set power profile"); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml new file mode 100644 index 0000000..ada41dd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CapsLockIndicator.qml @@ -0,0 +1,62 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + opacity: DMSService.capsLockState ? 1 : 0 + + states: [ + State { + name: "hidden_horizontal" + when: !DMSService.capsLockState && !isVerticalOrientation + PropertyChanges { + target: root + width: 0 + } + }, + State { + name: "hidden_vertical" + when: !DMSService.capsLockState && isVerticalOrientation + PropertyChanges { + target: root + height: 0 + } + } + ] + + transitions: [ + Transition { + NumberAnimation { + properties: "width,height" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + ] + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + anchors.centerIn: parent + name: "shift_lock" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.primary + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml new file mode 100644 index 0000000..e970662 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ClipboardButton.qml @@ -0,0 +1,314 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + property bool isAutoHideBar: false + + signal clipboardClicked + signal showSavedItemsRequested + signal clearAllRequested + + readonly property real minTooltipY: { + if (!parentScreen || !(axis?.isVertical ?? false)) { + return 0; + } + if (isAutoHideBar) { + return 0; + } + if (parentScreen.y > 0) { + return barThickness + barSpacing; + } + return 0; + } + + function clamp(value, minValue, maxValue) { + return Math.max(minValue, Math.min(maxValue, value)); + } + + function openContextMenu() { + const screen = root.parentScreen || Screen; + const screenX = screen.x || 0; + const screenY = screen.y || 0; + const isVertical = root.axis?.isVertical ?? false; + const edge = root.axis?.edge ?? "top"; + const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS); + + const globalPos = root.mapToGlobal(root.width / 2, root.height / 2); + const relativeX = globalPos.x - screenX; + const relativeY = globalPos.y - screenY; + + let anchorX = relativeX; + let anchorY = relativeY; + + if (isVertical) { + anchorX = edge === "left" ? (root.barThickness + root.barSpacing + gap) : (screen.width - (root.barThickness + root.barSpacing + gap)); + anchorY = relativeY + root.minTooltipY; + } else { + anchorX = relativeX; + anchorY = edge === "bottom" ? (screen.height - (root.barThickness + root.barSpacing + gap)) : (root.barThickness + root.barSpacing + gap); + } + + contextMenuWindow.showAt(anchorX, anchorY, isVertical, edge, screen); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y) + onClicked: function (mouse) { + switch (mouse.button) { + case Qt.RightButton: + openContextMenu(); + break; + case Qt.LeftButton: + clipboardClicked(); + break; + } + } + } + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + anchors.centerIn: parent + name: "content_paste" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetIconColor + } + } + } + + PanelWindow { + id: contextMenuWindow + + WindowBlur { + targetWindow: contextMenuWindow + blurX: menuContainer.x + blurY: menuContainer.y + blurWidth: contextMenuWindow.visible ? menuContainer.width : 0 + blurHeight: contextMenuWindow.visible ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + + WlrLayershell.namespace: "dms:clipboard-context-menu" + + property bool isVertical: false + property string edge: "top" + property point anchorPos: Qt.point(0, 0) + + function showAt(x, y, vertical, barEdge, targetScreen) { + if (targetScreen) { + contextMenuWindow.screen = targetScreen; + } + + anchorPos = Qt.point(x, y); + isVertical = vertical ?? false; + edge = barEdge ?? "top"; + + visible = true; + + if (contextMenuWindow.screen) { + TrayMenuManager.registerMenu(contextMenuWindow.screen.name, contextMenuWindow); + } + } + + function closeMenu() { + visible = false; + + if (contextMenuWindow.screen) { + TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name); + } + } + + screen: null + visible: false + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + anchors { + top: true + left: true + right: true + bottom: true + } + + Component.onDestruction: { + if (contextMenuWindow.screen) { + TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + contextMenuWindow.closeMenu(); + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: contextMenuWindow.closeMenu() + } + + Rectangle { + id: menuContainer + + x: { + if (contextMenuWindow.isVertical) { + if (contextMenuWindow.edge === "left") { + return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x); + } + return Math.max(10, contextMenuWindow.anchorPos.x - width); + } + const left = 10; + const right = contextMenuWindow.width - width - 10; + const want = contextMenuWindow.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + y: { + if (contextMenuWindow.isVertical) { + const top = 10; + const bottom = contextMenuWindow.height - height - 10; + const want = contextMenuWindow.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); + } + if (contextMenuWindow.edge === "top") { + return Math.min(contextMenuWindow.height - height - 10, contextMenuWindow.anchorPos.y); + } + return Math.max(10, contextMenuWindow.anchorPos.y - height); + } + + width: Math.min(240, Math.max(170, menuColumn.implicitWidth + Theme.spacingS * 2)) + height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2) + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + + opacity: contextMenuWindow.visible ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: -1 + } + + Column { + id: menuColumn + width: parent.width - Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + spacing: 1 + + Rectangle { + id: clearAllItem + width: parent.width + height: 30 + radius: Theme.cornerRadius + color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "delete_sweep" + size: 16 + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Clear All") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } + + MouseArea { + id: clearAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + contextMenuWindow.closeMenu(); + root.clearAllRequested(); + } + } + } + + Rectangle { + id: savedItemsItem + width: parent.width + height: 30 + radius: Theme.cornerRadius + color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "push_pin" + size: 16 + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Show Saved Items") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } + + MouseArea { + id: savedItemsArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + contextMenuWindow.closeMenu(); + root.showSavedItemsRequested(); + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml new file mode 100644 index 0000000..c49221e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Clock.qml @@ -0,0 +1,331 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.Plugins +import qs.Widgets + +BasePill { + id: root + + property var widgetData: null + property bool compactMode: false + signal clockClicked + + onClicked: clockClicked() + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : clockRow.implicitWidth + implicitHeight: root.isVerticalOrientation ? clockColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + readonly property bool compact: widgetData?.clockCompactMode !== undefined ? widgetData.clockCompactMode : SettingsData.clockCompactMode + + Column { + id: clockColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 0 + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: { + const hours = systemClock?.date?.getHours(); + if (SettingsData.use24HourClock) + return String(hours).padStart(2, '0').charAt(0); + const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + return String(display).padStart(2, '0').charAt(0); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + + StyledText { + text: { + const hours = systemClock?.date?.getHours(); + if (SettingsData.use24HourClock) + return String(hours).padStart(2, '0').charAt(1); + const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + return String(display).padStart(2, '0').charAt(1); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + } + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0) + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + + StyledText { + text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1) + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + } + + Row { + visible: SettingsData.showSeconds + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0) + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + + StyledText { + text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1) + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + } + + Item { + width: parent.width + height: Theme.spacingM + anchors.horizontalCenter: parent.horizontalCenter + visible: !compact + + Rectangle { + width: parent.width * 0.6 + height: 1 + color: Theme.outlineButton + anchors.centerIn: parent + } + } + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + visible: !compact + + StyledText { + text: { + const locale = I18n.locale(); + const dateFormatShort = locale.dateFormat(Locale.ShortFormat); + const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M'); + const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0'); + return value.charAt(0); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.primary + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + + StyledText { + text: { + const locale = I18n.locale(); + const dateFormatShort = locale.dateFormat(Locale.ShortFormat); + const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M'); + const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0'); + return value.charAt(1); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.primary + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + } + + Row { + spacing: 0 + anchors.horizontalCenter: parent.horizontalCenter + visible: !compact + + StyledText { + text: { + const locale = I18n.locale(); + const dateFormatShort = locale.dateFormat(Locale.ShortFormat); + const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M'); + const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0'); + return value.charAt(0); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.primary + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + + StyledText { + text: { + const locale = I18n.locale(); + const dateFormatShort = locale.dateFormat(Locale.ShortFormat); + const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M'); + const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0'); + return value.charAt(1); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.primary + width: Math.round(font.pixelSize * 0.6) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + } + } + } + + Row { + id: clockRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingS + + property real fontSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + property real digitWidth: fontSize * 0.6 + + property string hoursStr: { + const hours = systemClock?.date?.getHours() ?? 0; + if (SettingsData.use24HourClock) + return String(hours).padStart(2, '0'); + const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + if (SettingsData.padHours12Hour) + return String(display).padStart(2, '0'); + return String(display); + } + property string minutesStr: String(systemClock?.date?.getMinutes() ?? 0).padStart(2, '0') + property string secondsStr: String(systemClock?.date?.getSeconds() ?? 0).padStart(2, '0') + property string ampmStr: { + if (SettingsData.use24HourClock) + return ""; + const hours = systemClock?.date?.getHours() ?? 0; + return hours >= 12 ? " PM" : " AM"; + } + + Row { + spacing: 0 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + visible: clockRow.hoursStr.length > 1 + text: clockRow.hoursStr.charAt(0) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: clockRow.hoursStr.length > 1 ? clockRow.hoursStr.charAt(1) : clockRow.hoursStr.charAt(0) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: ":" + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + } + + StyledText { + text: clockRow.minutesStr.charAt(0) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: clockRow.minutesStr.charAt(1) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + visible: SettingsData.showSeconds + text: ":" + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + } + + StyledText { + visible: SettingsData.showSeconds + text: clockRow.secondsStr.charAt(0) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + visible: SettingsData.showSeconds + text: clockRow.secondsStr.charAt(1) + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + width: clockRow.digitWidth + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + visible: !SettingsData.use24HourClock + text: clockRow.ampmStr + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + } + } + + StyledText { + id: middleDot + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: !compact + } + + StyledText { + id: dateText + text: { + if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) { + return systemClock?.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat); + } + return systemClock?.date?.toLocaleDateString(I18n.locale(), "ddd d"); + } + font.pixelSize: clockRow.fontSize + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + visible: !compact + } + } + + SystemClock { + id: systemClock + precision: SettingsData.showSeconds ? SystemClock.Seconds : SystemClock.Minutes + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml new file mode 100644 index 0000000..d3330cd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ColorPicker.qml @@ -0,0 +1,37 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + + signal colorPickerRequested + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + anchors.centerIn: parent + name: "palette" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: root.isActive ? Theme.primary : Theme.surfaceText + } + } + } + + MouseArea { + z: 1 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + root.colorPickerRequested(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml new file mode 100644 index 0000000..5413d20 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml @@ -0,0 +1,830 @@ +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + property var popoutTarget: null + property var widgetData: null + property string screenName: "" + property string screenModel: "" + property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon + property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon + property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon + property bool showAudioPercent: widgetData?.showAudioPercent !== undefined ? widgetData.showAudioPercent : SettingsData.controlCenterShowAudioPercent + property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon + property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon + property bool showBrightnessPercent: widgetData?.showBrightnessPercent !== undefined ? widgetData.showBrightnessPercent : SettingsData.controlCenterShowBrightnessPercent + property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon + property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent + property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon + property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon + property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon + property real touchpadThreshold: 100 + property real micAccumulator: 0 + property real volumeAccumulator: 0 + property real brightnessAccumulator: 0 + readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + property var _hRow: null + property var _vCol: null + property var _hAudio: null + property var _hBrightness: null + property var _hMic: null + property var _vAudio: null + property var _vBrightness: null + property var _vMic: null + property var _interactionDelegates: [] + readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"] + readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder() + readonly property var controlCenterRenderModel: getControlCenterRenderModel() + + onIsVerticalOrientationChanged: root.clearInteractionRefs() + + onWheel: function (wheelEvent) { + const delta = wheelEvent.angleDelta.y; + if (delta === 0) + return; + + root.refreshInteractionRefs(); + + const rootX = wheelEvent.x - root.leftMargin; + const rootY = wheelEvent.y - root.topMargin; + + if (root.isVerticalOrientation && _vCol) { + const pos = root.mapToItem(_vCol, rootX, rootY); + if (_vBrightness?.visible && pos.y >= _vBrightness.y && pos.y < _vBrightness.y + _vBrightness.height) { + root.handleBrightnessWheel(delta); + } else if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) { + root.handleMicWheel(delta); + } else { + root.handleVolumeWheel(delta); + } + } else if (_hRow) { + const pos = root.mapToItem(_hRow, rootX, rootY); + if (_hBrightness?.visible && pos.x >= _hBrightness.x && pos.x < _hBrightness.x + _hBrightness.width) { + root.handleBrightnessWheel(delta); + } else if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) { + root.handleMicWheel(delta); + } else { + root.handleVolumeWheel(delta); + } + } else { + root.handleVolumeWheel(delta); + } + wheelEvent.accepted = true; + } + + onRightClicked: function (rootX, rootY) { + root.refreshInteractionRefs(); + + if (root.isVerticalOrientation && _vCol) { + const pos = root.mapToItem(_vCol, rootX, rootY); + if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { + AudioService.toggleMute(); + return; + } + if (_vMic?.visible && pos.y >= _vMic.y && pos.y < _vMic.y + _vMic.height) { + AudioService.toggleMicMute(); + return; + } + } else if (_hRow) { + const pos = root.mapToItem(_hRow, rootX, rootY); + if (_hAudio?.visible && pos.x >= _hAudio.x && pos.x < _hAudio.x + _hAudio.width) { + AudioService.toggleMute(); + return; + } + if (_hMic?.visible && pos.x >= _hMic.x && pos.x < _hMic.x + _hMic.width) { + AudioService.toggleMicMute(); + return; + } + } + } + + Loader { + active: root.showPrinterIcon + sourceComponent: Component { + Ref { + service: CupsService + } + } + } + + function getNetworkIconName() { + if (NetworkService.wifiToggling) + return "sync"; + switch (NetworkService.networkStatus) { + case "ethernet": + return "lan"; + case "vpn": + return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon; + default: + return NetworkService.wifiSignalIcon; + } + } + + function getNetworkIconColor() { + if (NetworkService.wifiToggling) + return Theme.primary; + return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText; + } + + function getVolumeIconName() { + if (!AudioService.sink?.audio) + return "volume_up"; + if (AudioService.sink.audio.muted) + return "volume_off"; + if (AudioService.sink.audio.volume === 0) + return "volume_mute"; + if (AudioService.sink.audio.volume * 100 < 33) + return "volume_down"; + return "volume_up"; + } + + function getMicIconName() { + if (!AudioService.source?.audio) + return "mic"; + if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0) + return "mic_off"; + return "mic"; + } + + function getMicIconColor() { + if (!AudioService.source?.audio) + return Theme.surfaceText; + if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0) + return Theme.surfaceText; + return Theme.widgetIconColor; + } + + function getBrightnessIconName() { + const deviceName = getPinnedBrightnessDevice(); + if (!deviceName) + return "brightness_medium"; + const level = DisplayService.getDeviceBrightness(deviceName); + if (level <= 33) + return "brightness_low"; + if (level <= 66) + return "brightness_medium"; + return "brightness_high"; + } + + function getScreenPinKey() { + if (!root.screenName) + return ""; + const screen = Quickshell.screens.find(s => s.name === root.screenName); + if (screen) { + return SettingsData.getScreenDisplayName(screen); + } + if (SettingsData.displayNameMode === "model" && root.screenModel && root.screenModel.length > 0) { + return root.screenModel; + } + return root.screenName; + } + + function getPinnedBrightnessDevice() { + const pinKey = getScreenPinKey(); + if (!pinKey) + return ""; + const pins = SettingsData.brightnessDevicePins || {}; + return pins[pinKey] || ""; + } + + function hasPinnedBrightnessDevice() { + return getPinnedBrightnessDevice().length > 0; + } + + function handleVolumeWheel(delta) { + if (!AudioService.sink?.audio) + return; + + var step = 5; + const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; + if (!isMouseWheel) { + step = 1; + volumeAccumulator += delta; + if (Math.abs(volumeAccumulator) < touchpadThreshold) + return; + + delta = volumeAccumulator; + volumeAccumulator = 0; + } + + const maxVol = AudioService.sinkMaxVolume; + const currentVolume = AudioService.sink.audio.volume * 100; + const newVolume = delta > 0 ? Math.min(maxVol, currentVolume + step) : Math.max(0, currentVolume - step); + AudioService.sink.audio.muted = false; + AudioService.sink.audio.volume = newVolume / 100; + AudioService.playVolumeChangeSoundIfEnabled(); + } + + function handleMicWheel(delta) { + if (!AudioService.source?.audio) + return; + + var step = 5; + const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; + if (!isMouseWheel) { + step = 1; + micAccumulator += delta; + if (Math.abs(micAccumulator) < touchpadThreshold) + return; + + delta = micAccumulator; + micAccumulator = 0; + } + + const currentVolume = AudioService.source.audio.volume * 100; + const newVolume = delta > 0 ? Math.min(100, currentVolume + step) : Math.max(0, currentVolume - step); + AudioService.source.audio.muted = false; + AudioService.source.audio.volume = newVolume / 100; + } + + function handleBrightnessWheel(delta) { + const deviceName = getPinnedBrightnessDevice(); + if (!deviceName) { + return; + } + + var step = 5; + const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0; + if (!isMouseWheel) { + step = 1; + brightnessAccumulator += delta; + if (Math.abs(brightnessAccumulator) < touchpadThreshold) + return; + + delta = brightnessAccumulator; + brightnessAccumulator = 0; + } + + const currentBrightness = DisplayService.getDeviceBrightness(deviceName); + const newBrightness = delta > 0 ? Math.min(100, currentBrightness + step) : Math.max(1, currentBrightness - step); + DisplayService.setBrightness(newBrightness, deviceName); + } + + function getBrightness() { + const deviceName = getPinnedBrightnessDevice(); + if (!deviceName) { + return; + } + return DisplayService.getDeviceBrightness(deviceName) / 100; + } + + function getBatteryIconColor() { + if (!BatteryService.batteryAvailable) + return Theme.widgetIconColor; + if (BatteryService.isLowBattery && !BatteryService.isCharging) + return Theme.error; + if (BatteryService.isCharging || BatteryService.isPluggedIn) + return Theme.primary; + return Theme.widgetIconColor; + } + + function hasPrintJobs() { + return CupsService.getTotalJobsNum() > 0; + } + + function getControlCenterIconSize() { + return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale); + } + + function getEffectiveControlCenterGroupOrder() { + const knownIds = root.defaultControlCenterGroupOrder; + const savedOrder = root.widgetData?.controlCenterGroupOrder; + const result = []; + const seen = {}; + + if (savedOrder && typeof savedOrder.length === "number") { + for (let i = 0; i < savedOrder.length; ++i) { + const groupId = savedOrder[i]; + if (knownIds.indexOf(groupId) === -1 || seen[groupId]) + continue; + + seen[groupId] = true; + result.push(groupId); + } + } + + for (let i = 0; i < knownIds.length; ++i) { + const groupId = knownIds[i]; + if (seen[groupId]) + continue; + + seen[groupId] = true; + result.push(groupId); + } + + return result; + } + + function isGroupVisible(groupId) { + switch (groupId) { + case "screenSharing": + return root.showScreenSharingIcon && NiriService.hasCasts; + case "network": + return root.showNetworkIcon && NetworkService.networkAvailable; + case "vpn": + return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected; + case "bluetooth": + return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled; + case "audio": + return root.showAudioIcon; + case "microphone": + return root.showMicIcon; + case "brightness": + return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice(); + case "battery": + return root.showBatteryIcon && BatteryService.batteryAvailable; + case "printer": + return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs(); + default: + return false; + } + } + + function isCompositeGroup(groupId) { + return groupId === "audio" || groupId === "microphone" || groupId === "brightness"; + } + + function getControlCenterRenderModel() { + return root.effectiveControlCenterGroupOrder.map(groupId => ({ + "id": groupId, + "visible": root.isGroupVisible(groupId), + "composite": root.isCompositeGroup(groupId) + })); + } + + function clearInteractionRefs() { + root._hAudio = null; + root._hBrightness = null; + root._hMic = null; + root._vAudio = null; + root._vBrightness = null; + root._vMic = null; + } + + function registerInteractionDelegate(isVertical, item) { + if (!item) + return; + + for (let i = 0; i < root._interactionDelegates.length; ++i) { + const entry = root._interactionDelegates[i]; + if (entry && entry.item === item) { + entry.isVertical = isVertical; + return; + } + } + + root._interactionDelegates = root._interactionDelegates.concat([ + { + "isVertical": isVertical, + "item": item + } + ]); + } + + function unregisterInteractionDelegate(item) { + if (!item) + return; + + root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item); + } + + function refreshInteractionRefs() { + root.clearInteractionRefs(); + + for (let i = 0; i < root._interactionDelegates.length; ++i) { + const entry = root._interactionDelegates[i]; + const item = entry?.item; + if (!item || !item.visible) + continue; + + const groupId = item.interactionGroupId; + if (entry.isVertical) { + if (groupId === "audio") + root._vAudio = item; + else if (groupId === "microphone") + root._vMic = item; + else if (groupId === "brightness") + root._vBrightness = item; + } else { + if (groupId === "audio") + root._hAudio = item; + else if (groupId === "microphone") + root._hMic = item; + else if (groupId === "brightness") + root._hBrightness = item; + } + } + } + + function hasNoVisibleIcons() { + return !root.controlCenterRenderModel.some(entry => entry.visible); + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth + implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + Component.onCompleted: { + root._hRow = controlIndicators; + root._vCol = controlColumn; + root.clearInteractionRefs(); + } + + Column { + id: controlColumn + visible: root.isVerticalOrientation + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXS + + Repeater { + model: root.controlCenterRenderModel + Item { + id: verticalGroupItem + required property var modelData + required property int index + property string interactionGroupId: modelData.id + + width: parent.width + height: { + switch (modelData.id) { + case "audio": + return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0); + case "microphone": + return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0); + case "brightness": + return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0); + default: + return root.vIconSize; + } + } + visible: modelData.visible + + Component.onCompleted: { + root.registerInteractionDelegate(true, verticalGroupItem); + root.refreshInteractionRefs(); + } + Component.onDestruction: { + if (root) { + root.unregisterInteractionDelegate(verticalGroupItem); + root.refreshInteractionRefs(); + } + } + onVisibleChanged: root.refreshInteractionRefs() + onInteractionGroupIdChanged: { + root.refreshInteractionRefs(); + } + + DankIcon { + anchors.centerIn: parent + visible: !verticalGroupItem.modelData.composite + name: { + switch (verticalGroupItem.modelData.id) { + case "screenSharing": + return "screen_record"; + case "network": + return root.getNetworkIconName(); + case "vpn": + return "vpn_lock"; + case "bluetooth": + return "bluetooth"; + case "battery": + return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); + case "printer": + return "print"; + default: + return "settings"; + } + } + size: root.vIconSize + color: { + switch (verticalGroupItem.modelData.id) { + case "screenSharing": + return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; + case "network": + return root.getNetworkIconColor(); + case "vpn": + return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; + case "bluetooth": + return BluetoothService.connected ? Theme.primary : Theme.surfaceText; + case "battery": + return root.getBatteryIconColor(); + case "printer": + return Theme.primary; + default: + return Theme.widgetIconColor; + } + } + } + + DankIcon { + id: audioIconV + visible: verticalGroupItem.modelData.id === "audio" + name: root.getVolumeIconName() + size: root.vIconSize + color: Theme.widgetIconColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } + + NumericText { + id: audioPercentV + visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) + text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioIconV.bottom + anchors.topMargin: 2 + } + + DankIcon { + id: micIconV + visible: verticalGroupItem.modelData.id === "microphone" + name: root.getMicIconName() + size: root.vIconSize + color: root.getMicIconColor() + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } + + NumericText { + id: micPercentV + visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume) + text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: micIconV.bottom + anchors.topMargin: 2 + } + + DankIcon { + id: brightnessIconV + visible: verticalGroupItem.modelData.id === "brightness" + name: root.getBrightnessIconName() + size: root.vIconSize + color: Theme.widgetIconColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } + + NumericText { + id: brightnessPercentV + visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness()) + text: Math.round(getBrightness() * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: brightnessIconV.bottom + anchors.topMargin: 2 + } + } + } + + Item { + width: parent.width + height: root.vIconSize + visible: root.hasNoVisibleIcons() + + DankIcon { + name: "settings" + size: root.vIconSize + color: root.isActive ? Theme.primary : Theme.widgetIconColor + anchors.centerIn: parent + } + } + } + + Row { + id: controlIndicators + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + Repeater { + model: root.controlCenterRenderModel + + Item { + id: horizontalGroupItem + required property var modelData + required property int index + property string interactionGroupId: modelData.id + + width: { + switch (modelData.id) { + case "audio": + return audioGroup.width; + case "microphone": + return micGroup.width; + case "brightness": + return brightnessGroup.width; + default: + return root.getControlCenterIconSize(); + } + } + implicitWidth: width + height: root.widgetThickness - root.horizontalPadding * 2 + visible: modelData.visible + + Component.onCompleted: { + root.registerInteractionDelegate(false, horizontalGroupItem); + root.refreshInteractionRefs(); + } + Component.onDestruction: { + if (root) { + root.unregisterInteractionDelegate(horizontalGroupItem); + root.refreshInteractionRefs(); + } + } + onVisibleChanged: root.refreshInteractionRefs() + onInteractionGroupIdChanged: { + root.refreshInteractionRefs(); + } + + DankIcon { + id: iconOnlyItem + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + visible: !horizontalGroupItem.modelData.composite + name: { + switch (horizontalGroupItem.modelData.id) { + case "screenSharing": + return "screen_record"; + case "network": + return root.getNetworkIconName(); + case "vpn": + return "vpn_lock"; + case "bluetooth": + return "bluetooth"; + case "battery": + return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); + case "printer": + return "print"; + default: + return "settings"; + } + } + size: root.getControlCenterIconSize() + color: { + switch (horizontalGroupItem.modelData.id) { + case "screenSharing": + return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; + case "network": + return root.getNetworkIconColor(); + case "vpn": + return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; + case "bluetooth": + return BluetoothService.connected ? Theme.primary : Theme.surfaceText; + case "battery": + return root.getBatteryIconColor(); + case "printer": + return Theme.primary; + default: + return Theme.widgetIconColor; + } + } + } + + Rectangle { + id: audioGroup + width: audioContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "audio" + + Row { + id: audioContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: audioIcon + name: root.getVolumeIconName() + size: root.getControlCenterIconSize() + color: Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: audioPercent + visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) + text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } + + Rectangle { + id: micGroup + width: micContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "microphone" + + Row { + id: micContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: micIcon + name: root.getMicIconName() + size: root.getControlCenterIconSize() + color: root.getMicIconColor() + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: micPercent + visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume) + text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } + + Rectangle { + id: brightnessGroup + width: brightnessContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "brightness" + + Row { + id: brightnessContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: brightnessIcon + name: root.getBrightnessIconName() + size: root.getControlCenterIconSize() + color: Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: brightnessPercent + visible: root.showBrightnessPercent && isFinite(getBrightness()) + text: Math.round(getBrightness() * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } + } + } + + DankIcon { + name: "settings" + size: root.getControlCenterIconSize() + color: root.isActive ? Theme.primary : Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + visible: root.hasNoVisibleIcons() + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.NoButton + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml new file mode 100644 index 0000000..212f9b5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuMonitor.qml @@ -0,0 +1,146 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property var popoutTarget: null + property var widgetData: null + property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true + + signal cpuClicked + + Component.onCompleted: { + DgopService.addRef(["cpu"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu"]); + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuContent.implicitWidth + implicitHeight: root.isVerticalOrientation ? cpuColumn.implicitHeight : cpuContent.implicitHeight + + Column { + id: cpuColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "memory" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.cpuUsage > 80) { + return Theme.tempDanger; + } + + if (DgopService.cpuUsage > 60) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) { + return "--"; + } + + return DgopService.cpuUsage.toFixed(0); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: cpuContent + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + id: cpuIcon + name: "memory" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.cpuUsage > 80) { + return Theme.tempDanger; + } + + if (DgopService.cpuUsage > 60) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: textBox + anchors.verticalCenter: parent.verticalCenter + + implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuCurrent.width) : cpuCurrent.width + implicitHeight: cpuText.implicitHeight + + width: implicitWidth + height: implicitHeight + + StyledTextMetrics { + id: cpuBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: "100%" + } + + StyledTextMetrics { + id: cpuCurrent + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: cpuText.text + } + + StyledText { + id: cpuText + text: { + const v = DgopService.cpuUsage; + if (v === undefined || v === null || v === 0) { + return "--%"; + } + return v.toFixed(0) + "%"; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + DgopService.setSortBy("cpu"); + cpuClicked(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml new file mode 100644 index 0000000..4066387 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/CpuTemperature.qml @@ -0,0 +1,140 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property var popoutTarget: null + property var widgetData: null + property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true + + signal cpuTempClicked + + Component.onCompleted: { + DgopService.addRef(["cpu"]); + } + Component.onDestruction: { + DgopService.removeRef(["cpu"]); + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuTempRow.implicitWidth + implicitHeight: root.isVerticalOrientation ? cpuTempColumn.implicitHeight : cpuTempRow.implicitHeight + + Column { + id: cpuTempColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "device_thermostat" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.cpuTemperature > 85) { + return Theme.tempDanger; + } + + if (DgopService.cpuTemperature > 69) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) { + return "--"; + } + + return Math.round(DgopService.cpuTemperature).toString(); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: cpuTempRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + id: cpuTempIcon + name: "device_thermostat" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.cpuTemperature > 85) { + return Theme.tempDanger; + } + + if (DgopService.cpuTemperature > 69) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: textBox + anchors.verticalCenter: parent.verticalCenter + + implicitWidth: root.minimumWidth ? Math.max(tempBaseline.width, cpuTempText.paintedWidth) : cpuTempText.paintedWidth + implicitHeight: cpuTempText.implicitHeight + + width: implicitWidth + height: implicitHeight + + StyledTextMetrics { + id: tempBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: "88°" + } + + StyledText { + id: cpuTempText + text: { + if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) { + return "--°"; + } + + return Math.round(DgopService.cpuTemperature) + "°"; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + DgopService.setSortBy("cpu"); + cpuTempClicked(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml new file mode 100644 index 0000000..f810cb6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DWLLayout.qml @@ -0,0 +1,110 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: layout + + property bool layoutPopupVisible: false + property var popoutTarget: null + + signal toggleLayoutPopup() + + visible: CompositorService.isDwl && DwlService.dwlAvailable + + property var outputState: parentScreen ? DwlService.getOutputState(parentScreen.name) : null + property string currentLayoutSymbol: outputState?.layoutSymbol || "" + property int currentLayoutIndex: outputState?.layout || 0 + + readonly property var layoutIcons: ({ + "CT": "view_compact", + "G": "grid_view", + "K": "layers", + "M": "fullscreen", + "RT": "view_sidebar", + "S": "view_carousel", + "T": "view_quilt", + "VG": "grid_on", + "VK": "view_day", + "VS": "scrollable_header", + "VT": "clarify" + }) + + function getLayoutIcon(symbol) { + return layoutIcons[symbol] || "view_quilt" + } + + Connections { + target: DwlService + function onStateChanged() { + outputState = parentScreen ? DwlService.getOutputState(parentScreen.name) : null + } + } + + content: Component { + Item { + implicitWidth: layout.isVerticalOrientation ? (layout.widgetThickness - layout.horizontalPadding * 2) : layoutContent.implicitWidth + implicitHeight: layout.isVerticalOrientation ? layoutColumn.implicitHeight : (layout.widgetThickness - layout.horizontalPadding * 2) + + Column { + id: layoutColumn + visible: layout.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: layout.getLayoutIcon(layout.currentLayoutSymbol) + size: Theme.barIconSize(layout.barThickness, undefined, layout.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: layout.currentLayoutSymbol + font.pixelSize: Theme.barTextSize(layout.barThickness, layout.barConfig?.fontScale, layout.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: layoutContent + visible: !layout.isVerticalOrientation + anchors.centerIn: parent + spacing: (barConfig?.noBackground ?? false) ? 1 : 2 + + DankIcon { + name: layout.getLayoutIcon(layout.currentLayoutSymbol) + size: Theme.barIconSize(layout.barThickness, -4, layout.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: layout.currentLayoutSymbol + font.pixelSize: Theme.barTextSize(layout.barThickness, layout.barConfig?.fontScale, layout.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + onClicked: { + toggleLayoutPopup() + } + + onRightClicked: { + if (!parentScreen || !DwlService.dwlAvailable || DwlService.layouts.length === 0) { + return + } + + const currentIndex = layout.currentLayoutIndex + const nextIndex = (currentIndex + 1) % DwlService.layouts.length + + DwlService.setLayout(parentScreen.name, nextIndex) + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml new file mode 100644 index 0000000..d6e6d1b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/DiskUsage.qml @@ -0,0 +1,255 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property var widgetData: null + property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/" + property int diskUsageMode: (widgetData && widgetData.diskUsageMode !== undefined) ? widgetData.diskUsageMode : 0 + property bool isHovered: mouseArea.containsMouse + property bool isAutoHideBar: false + + property var selectedMount: { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return null; + } + + const currentMountPath = root.mountPath || "/"; + + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === currentMountPath) { + return DgopService.diskMounts[i]; + } + } + + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === "/") { + return DgopService.diskMounts[i]; + } + } + + return DgopService.diskMounts[0] || null; + } + + property real diskUsagePercent: { + if (!selectedMount || !selectedMount.percent) { + return 0; + } + const percentStr = selectedMount.percent.replace("%", ""); + return parseFloat(percentStr) || 0; + } + + Component.onCompleted: { + DgopService.addRef(["diskmounts"]); + } + Component.onDestruction: { + DgopService.removeRef(["diskmounts"]); + } + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + const spacing = barConfig?.spacing ?? 4; + const offset = barThickness + spacing; + return offset; + } + + return 0; + } + + Connections { + function onWidgetDataChanged() { + root.mountPath = Qt.binding(() => { + return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/"; + }); + + root.selectedMount = Qt.binding(() => { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return null; + } + + const currentMountPath = root.mountPath || "/"; + + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === currentMountPath) { + return DgopService.diskMounts[i]; + } + } + + for (let i = 0; i < DgopService.diskMounts.length; i++) { + if (DgopService.diskMounts[i].mount === "/") { + return DgopService.diskMounts[i]; + } + } + + return DgopService.diskMounts[0] || null; + }); + } + + target: SettingsData + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : diskContent.implicitWidth + implicitHeight: root.isVerticalOrientation ? diskColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + Column { + id: diskColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "storage" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (root.diskUsagePercent > 90) { + return Theme.tempDanger; + } + if (root.diskUsagePercent > 75) { + return Theme.tempWarning; + } + return Theme.surfaceText; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) { + return "--"; + } + if (!root.selectedMount) return "--"; + switch (root.diskUsageMode) { + case 1: return root.selectedMount.size || "--"; + case 2: return root.selectedMount.avail || "--"; + case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--"); + default: return root.diskUsagePercent.toFixed(0); + } + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: diskContent + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: 3 + + DankIcon { + name: "storage" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (root.diskUsagePercent > 90) { + return Theme.tempDanger; + } + if (root.diskUsagePercent > 75) { + return Theme.tempWarning; + } + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (!root.selectedMount) { + return "--"; + } + return root.selectedMount.mount; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + } + + StyledText { + text: { + if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) { + return "--%"; + } + if (!root.selectedMount) return "--%"; + switch (root.diskUsageMode) { + case 1: return root.selectedMount.size || "--"; + case 2: return root.selectedMount.avail || "--"; + case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--"); + default: return root.diskUsagePercent.toFixed(0) + "%"; + } + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + + StyledTextMetrics { + id: diskBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: { + switch (root.diskUsageMode) { + case 3: return "888.8G / 888.8G"; + case 1: + case 2: return "888.8G"; + default: return "100%"; + } + } + } + + width: Math.max(diskBaseline.width, paintedWidth) + } + } + } + } + + Loader { + id: tooltipLoader + active: false + sourceComponent: DankTooltip {} + } + + MouseArea { + id: mouseArea + z: 1 + anchors.fill: parent + hoverEnabled: root.isVerticalOrientation + onEntered: { + if (root.isVerticalOrientation && root.selectedMount) { + tooltipLoader.active = true; + if (tooltipLoader.item) { + const globalPos = mapToGlobal(width / 2, height / 2); + const currentScreen = root.parentScreen || Screen; + const screenX = currentScreen ? currentScreen.x : 0; + const screenY = currentScreen ? currentScreen.y : 0; + const relativeY = globalPos.y - screenY; + const adjustedY = relativeY + root.minTooltipY; + const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + const isLeft = root.axis?.edge === "left"; + tooltipLoader.item.show(root.selectedMount.mount, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft); + } + } + } + onExited: { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml new file mode 100644 index 0000000..a4ad4d2 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/FocusedApp.qml @@ -0,0 +1,317 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Hyprland +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property var widgetData: null + property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode + property int availableWidth: 400 + readonly property int maxNormalWidth: 456 + readonly property int maxCompactWidth: 288 + property Toplevel activeWindow: null + property var activeDesktopEntry: null + property bool isHovered: mouseArea.containsMouse + property bool isAutoHideBar: false + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + return barThickness + (barSpacing || 4); + } + + return 0; + } + + function updateActiveWindow() { + const active = ToplevelManager.activeToplevel; + + if (!active) { + // Only clear if our tracked window is no longer alive + if (activeWindow) { + const alive = ToplevelManager.toplevels?.values; + if (alive && !Array.from(alive).some(t => t === activeWindow)) + activeWindow = null; + } + return; + } + + if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) { + activeWindow = active; + } + // else: active window is on a different screen so keep the previous value + } + + Component.onCompleted: { + updateActiveWindow(); + updateDesktopEntry(); + } + + Connections { + target: ToplevelManager + function onActiveToplevelChanged() { + root.updateActiveWindow(); + } + } + + Connections { + target: CompositorService + function onToplevelsChanged() { + root.updateActiveWindow(); + } + } + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + root.updateDesktopEntry(); + } + } + + Connections { + target: root + function onActiveWindowChanged() { + root.updateDesktopEntry(); + } + } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + root.updateDesktopEntry(); + } + } + + function updateDesktopEntry() { + if (activeWindow && activeWindow.appId) { + const moddedId = Paths.moddedAppId(activeWindow.appId); + activeDesktopEntry = DesktopEntries.heuristicLookup(moddedId); + } else { + activeDesktopEntry = null; + } + } + readonly property bool hasWindowsOnCurrentWorkspace: { + if (CompositorService.isNiri) { + let currentWorkspaceId = null; + for (var i = 0; i < NiriService.allWorkspaces.length; i++) { + const ws = NiriService.allWorkspaces[i]; + if (ws.is_focused) { + currentWorkspaceId = ws.id; + break; + } + } + + if (!currentWorkspaceId) { + return false; + } + + const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId); + return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId); + } + + if (CompositorService.isHyprland) { + if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) { + return false; + } + + try { + if (!Hyprland.toplevels) + return false; + const hyprlandToplevels = Array.from(Hyprland.toplevels.values); + const activeHyprToplevel = hyprlandToplevels.find(t => t?.wayland === activeWindow); + + if (!activeHyprToplevel || !activeHyprToplevel.workspace) { + return false; + } + + return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id; + } catch (e) { + return false; + } + } + + return activeWindow && (activeWindow.title || activeWindow.appId); + } + + width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0 + height: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? visualHeight : barThickness) : 0 + visible: hasWindowsOnCurrentWorkspace + + content: Component { + Item { + implicitWidth: { + if (!root.hasWindowsOnCurrentWorkspace) + return 0; + if (root.isVerticalOrientation) + return root.widgetThickness - root.horizontalPadding * 2; + const baseWidth = contentRow.implicitWidth; + return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2); + } + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + clip: false + + IconImage { + id: appIcon + anchors.centerIn: parent + width: 18 + height: 18 + visible: root.isVerticalOrientation && activeWindow && status === Image.Ready + source: { + if (!activeWindow || !activeWindow.appId) + return ""; + return Paths.getAppIcon(activeWindow.appId, activeDesktopEntry); + } + smooth: true + mipmap: true + asynchronous: true + layer.enabled: activeWindow && (activeWindow.appId === "org.quickshell" || activeWindow.appId === "com.danklinux.dms") + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + } + + DankIcon { + anchors.centerIn: parent + size: 18 + name: "sports_esports" + color: Theme.widgetTextColor + visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId) + } + + Text { + anchors.centerIn: parent + visible: root.isVerticalOrientation && activeWindow && activeWindow.appId && appIcon.status !== Image.Ready && !Paths.isSteamApp(activeWindow.appId) + text: { + if (!activeWindow || !activeWindow.appId) + return "?"; + const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry); + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.widgetTextColor + } + + Row { + id: contentRow + anchors.centerIn: parent + spacing: Theme.spacingS + visible: !root.isVerticalOrientation + + StyledText { + id: appText + text: { + if (!activeWindow || !activeWindow.appId) + return ""; + return Paths.getAppName(activeWindow.appId, activeDesktopEntry); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 80 : 180) + visible: !compactMode && text.length > 0 + } + + StyledText { + text: "•" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: !compactMode && appText.text && titleText.text + } + + StyledText { + id: titleText + text: { + const title = activeWindow && activeWindow.title ? activeWindow.title : ""; + const appName = appText.text; + + if (compactMode) { + if (!title || title === appName) + return title || appName; + if (title.endsWith(appName)) + return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName; + return title; + } + + if (!title || !appName) + return title; + + if (title.endsWith(appName)) + return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, ""); + + return title; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 280 : 250) + visible: text.length > 0 + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: root.isVerticalOrientation + acceptedButtons: Qt.NoButton + onEntered: { + if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) { + tooltipLoader.active = true; + if (tooltipLoader.item) { + const globalPos = mapToGlobal(width / 2, height / 2); + const currentScreen = root.parentScreen; + const screenX = currentScreen ? currentScreen.x : 0; + const screenY = currentScreen ? currentScreen.y : 0; + const relativeY = globalPos.y - screenY; + // Add minTooltipY offset to account for top bar + const adjustedY = relativeY + root.minTooltipY; + const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS); + + const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry); + const title = activeWindow.title || ""; + const tooltipText = appName + (title ? " • " + title : ""); + + const isLeft = root.axis?.edge === "left"; + tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft); + } + } + } + onExited: { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + } + } + + Loader { + id: tooltipLoader + active: false + sourceComponent: DankTooltip {} + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml new file mode 100644 index 0000000..cf07f3e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/GpuTemperature.qml @@ -0,0 +1,224 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property var popoutTarget: null + property var widgetData: null + property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0 + property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true + + signal gpuTempClicked + + property real displayTemp: { + if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { + return 0; + } + + if (selectedGpuIndex >= 0 && selectedGpuIndex < DgopService.availableGpus.length) { + return DgopService.availableGpus[selectedGpuIndex].temperature || 0; + } + + return 0; + } + + function updateWidgetPciId(pciId) { + const sections = ["left", "center", "right"]; + const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default"); + if (!defaultBar) + return; + for (let s = 0; s < sections.length; s++) { + const sectionId = sections[s]; + let widgets = []; + if (sectionId === "left") { + widgets = (defaultBar.leftWidgets || []).slice(); + } else if (sectionId === "center") { + widgets = (defaultBar.centerWidgets || []).slice(); + } else if (sectionId === "right") { + widgets = (defaultBar.rightWidgets || []).slice(); + } + for (let i = 0; i < widgets.length; i++) { + const widget = widgets[i]; + if (typeof widget === "object" && widget.id === "gpuTemp" && (!widget.pciId || widget.pciId === "")) { + widgets[i] = { + "id": widget.id, + "enabled": widget.enabled !== undefined ? widget.enabled : true, + "selectedGpuIndex": 0, + "pciId": pciId + }; + if (sectionId === "left") { + SettingsData.setDankBarLeftWidgets(widgets); + } else if (sectionId === "center") { + SettingsData.setDankBarCenterWidgets(widgets); + } else if (sectionId === "right") { + SettingsData.setDankBarRightWidgets(widgets); + } + return; + } + } + } + } + + Component.onCompleted: { + DgopService.addRef(["gpu"]); + if (widgetData && widgetData.pciId) { + DgopService.addGpuPciId(widgetData.pciId); + } else { + autoSaveTimer.running = true; + } + } + Component.onDestruction: { + DgopService.removeRef(["gpu"]); + if (widgetData && widgetData.pciId) { + DgopService.removeGpuPciId(widgetData.pciId); + } + } + + Connections { + function onWidgetDataChanged() { + root.selectedGpuIndex = Qt.binding(() => { + return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0; + }); + } + + target: SettingsData + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : gpuTempRow.implicitWidth + implicitHeight: root.isVerticalOrientation ? gpuTempColumn.implicitHeight : gpuTempRow.implicitHeight + + Column { + id: gpuTempColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "auto_awesome_mosaic" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (root.displayTemp > 80) { + return Theme.tempDanger; + } + + if (root.displayTemp > 65) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) { + return "--"; + } + + return Math.round(root.displayTemp).toString(); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: gpuTempRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + id: gpuTempIcon + name: "auto_awesome_mosaic" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (root.displayTemp > 80) { + return Theme.tempDanger; + } + + if (root.displayTemp > 65) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: textBox + anchors.verticalCenter: parent.verticalCenter + + implicitWidth: root.minimumWidth ? Math.max(gpuTempBaseline.width, gpuTempText.paintedWidth) : gpuTempText.paintedWidth + implicitHeight: gpuTempText.implicitHeight + + width: implicitWidth + height: implicitHeight + + StyledTextMetrics { + id: gpuTempBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: "88°" + } + + StyledText { + id: gpuTempText + text: { + if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) { + return "--°"; + } + + return Math.round(root.displayTemp) + "°"; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + DgopService.setSortBy("cpu"); + gpuTempClicked(); + } + } + + Timer { + id: autoSaveTimer + + interval: 100 + running: false + onTriggered: { + if (DgopService.availableGpus && DgopService.availableGpus.length > 0) { + const firstGpu = DgopService.availableGpus[0]; + if (firstGpu && firstGpu.pciId) { + updateWidgetPciId(firstGpu.pciId); + DgopService.addGpuPciId(firstGpu.pciId); + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml new file mode 100644 index 0000000..a8e5993 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/IdleInhibitor.qml @@ -0,0 +1,36 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + anchors.centerIn: parent + name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + } + } + } + + MouseArea { + z: 1 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + } + onClicked: { + SessionService.toggleIdleInhibit(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml new file mode 100644 index 0000000..8b49a7d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/KeyboardLayoutName.qml @@ -0,0 +1,268 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property var widgetData: null + property bool compactMode: widgetData?.keyboardLayoutNameCompactMode !== undefined ? widgetData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode + readonly property var langCodes: ({ + "afrikaans": "af", + "albanian": "sq", + "amharic": "am", + "arabic": "ar", + "armenian": "hy", + "azerbaijani": "az", + "basque": "eu", + "belarusian": "be", + "bengali": "bn", + "bosnian": "bs", + "bulgarian": "bg", + "burmese": "my", + "catalan": "ca", + "chinese": "zh", + "croatian": "hr", + "czech": "cs", + "danish": "da", + "dutch": "nl", + "english": "en", + "esperanto": "eo", + "estonian": "et", + "filipino": "fil", + "finnish": "fi", + "french": "fr", + "galician": "gl", + "georgian": "ka", + "german": "de", + "greek": "el", + "gujarati": "gu", + "hausa": "ha", + "hebrew": "he", + "hindi": "hi", + "hungarian": "hu", + "icelandic": "is", + "igbo": "ig", + "indonesian": "id", + "irish": "ga", + "italian": "it", + "japanese": "ja", + "javanese": "jv", + "kannada": "kn", + "kazakh": "kk", + "khmer": "km", + "korean": "ko", + "kurdish": "ku", + "kyrgyz": "ky", + "lao": "lo", + "latvian": "lv", + "lithuanian": "lt", + "luxembourgish": "lb", + "macedonian": "mk", + "malay": "ms", + "malayalam": "ml", + "maltese": "mt", + "maori": "mi", + "marathi": "mr", + "mongolian": "mn", + "nepali": "ne", + "norwegian": "no", + "pashto": "ps", + "persian": "fa", + "iranian": "fa", + "farsi": "fa", + "polish": "pl", + "portuguese": "pt", + "punjabi": "pa", + "romanian": "ro", + "russian": "ru", + "serbian": "sr", + "sindhi": "sd", + "sinhala": "si", + "slovak": "sk", + "slovenian": "sl", + "somali": "so", + "spanish": "es", + "swahili": "sw", + "swedish": "sv", + "tajik": "tg", + "tamil": "ta", + "tatar": "tt", + "telugu": "te", + "thai": "th", + "tibetan": "bo", + "turkish": "tr", + "turkmen": "tk", + "ukrainian": "uk", + "urdu": "ur", + "uyghur": "ug", + "uzbek": "uz", + "vietnamese": "vi", + "welsh": "cy", + "yiddish": "yi", + "yoruba": "yo", + "zulu": "zu" + }) + readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"] + property string currentLayout: { + if (CompositorService.isNiri) { + return NiriService.getCurrentKeyboardLayoutName(); + } else if (CompositorService.isDwl) { + return DwlService.currentKeyboardLayout; + } + return ""; + } + property string hyprlandKeyboard: "" + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth + implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + Column { + id: contentColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "keyboard" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (!root.currentLayout) + return ""; + const lang = root.currentLayout.split(" ")[0].toLowerCase(); + const code = root.langCodes[lang] || lang.substring(0, 2); + return code.toUpperCase(); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: contentRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingS + + StyledText { + text: { + if (!root.currentLayout) + return ""; + if (root.compactMode && !CompositorService.isHyprland) { + const match = root.currentLayout.match(/^(\S+)(?:.*\(([^)]+)\))?/); + if (match) { + const lang = match[1].toLowerCase(); + const code = root.langCodes[lang] || lang.substring(0, 2); + if (match[2]) { + const variant = match[2].trim(); + const isValid = root.validVariants.some(v => variant.toUpperCase().includes(v.toUpperCase())) || variant.length <= 3; + if (isValid) + return code + "-" + variant; + } + return code.toUpperCase(); + } + return root.currentLayout.substring(0, 2).toUpperCase(); + } + return root.currentLayout; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + MouseArea { + z: 1 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + } + onClicked: { + if (CompositorService.isNiri) { + NiriService.cycleKeyboardLayout(); + } else if (CompositorService.isHyprland) { + Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]); + } else if (CompositorService.isDwl) { + Quickshell.execDetached(["mmsg", "-d", "switch_keyboard_layout"]); + } + } + } + + Connections { + target: CompositorService.isHyprland ? Hyprland : null + enabled: CompositorService.isHyprland + + function onRawEvent(event) { + if (event.name === "activelayout") { + updateLayout(); + } + } + } + + Component.onCompleted: { + if (CompositorService.isHyprland) { + updateLayout(); + } + } + + function updateLayout() { + if (CompositorService.isHyprland) { + Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => { + if (exitCode !== 0) { + root.currentLayout = "Unknown"; + return; + } + try { + const data = JSON.parse(output); + const mainKeyboard = data.keyboards.find(kb => kb.main === true); + root.hyprlandKeyboard = mainKeyboard.name; + + if (mainKeyboard) { + const layout = mainKeyboard.layout; + const variant = mainKeyboard.variant; + const index = mainKeyboard.active_layout_index; + + if (root.compactMode && layout && index !== undefined) { + const layouts = mainKeyboard.layout.split(","); + const variants = mainKeyboard.variant.split(","); + const index = mainKeyboard.active_layout_index; + + if (layouts[index] && variants[index] !== undefined) { + if (variants[index] === "") { + root.currentLayout = layouts[index]; + } else { + root.currentLayout = layouts[index] + "-" + variants[index]; + } + } else { + root.currentLayout = layouts[index]; + } + } else if (mainKeyboard && mainKeyboard.active_keymap) { + root.currentLayout = mainKeyboard.active_keymap; + } else { + root.currentLayout = "Unknown"; + } + } else { + root.currentLayout = "Unknown"; + } + } catch (e) { + root.currentLayout = "Unknown"; + } + }); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml new file mode 100644 index 0000000..81e6457 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/LauncherButton.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Widgets +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + property var hyprlandOverviewLoader: null + + content: Component { + Item { + implicitWidth: root.widgetThickness - root.horizontalPadding * 2 + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + visible: SettingsData.launcherLogoMode === "apps" + anchors.centerIn: parent + name: "apps" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetIconColor + } + + SystemLogo { + visible: SettingsData.launcherLogoMode === "os" + anchors.centerIn: parent + width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + colorOverride: Theme.effectiveLogoColor + brightnessOverride: SettingsData.launcherLogoBrightness + contrastOverride: SettingsData.launcherLogoContrast + } + + IconImage { + visible: SettingsData.launcherLogoMode === "dank" + anchors.centerIn: parent + width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + smooth: true + mipmap: true + asynchronous: true + source: "file://" + Theme.shellDir + "/assets/danklogo.svg" + layer.enabled: Theme.effectiveLogoColor !== "" + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.effectiveLogoColor + } + } + + IconImage { + visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc) + anchors.centerIn: parent + width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + smooth: true + asynchronous: true + source: { + if (CompositorService.isNiri) { + return "file://" + Theme.shellDir + "/assets/niri.svg"; + } else if (CompositorService.isHyprland) { + return "file://" + Theme.shellDir + "/assets/hyprland.svg"; + } else if (CompositorService.isDwl) { + return "file://" + Theme.shellDir + "/assets/mango.png"; + } else if (CompositorService.isSway) { + return "file://" + Theme.shellDir + "/assets/sway.svg"; + } else if (CompositorService.isScroll) { + return "file://" + Theme.shellDir + "/assets/sway.svg"; + } else if (CompositorService.isMiracle) { + return "file://" + Theme.shellDir + "/assets/miraclewm.svg"; + } else if (CompositorService.isLabwc) { + return "file://" + Theme.shellDir + "/assets/labwc.png"; + } + return ""; + } + layer.enabled: Theme.effectiveLogoColor !== "" + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.effectiveLogoColor + brightness: { + SettingsData.launcherLogoBrightness; + } + contrast: { + SettingsData.launcherLogoContrast; + } + } + } + + IconImage { + visible: SettingsData.launcherLogoMode === "custom" && SettingsData.launcherLogoCustomPath !== "" + anchors.centerIn: parent + width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + smooth: true + asynchronous: true + source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : "" + layer.enabled: Theme.effectiveLogoColor !== "" + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.effectiveLogoColor + brightness: SettingsData.launcherLogoBrightness + contrast: SettingsData.launcherLogoContrast + } + } + } + } + + onRightClicked: { + if (CompositorService.isNiri) { + NiriService.toggleOverview(); + } else if (root.hyprlandOverviewLoader?.item) { + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml new file mode 100644 index 0000000..6007427 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Media.qml @@ -0,0 +1,502 @@ +import QtQuick +import Quickshell.Services.Mpris +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + readonly property MprisPlayer activePlayer: MprisController.activePlayer + readonly property bool playerAvailable: activePlayer !== null + readonly property bool __isChromeBrowser: { + if (!activePlayer?.identity) + return false; + const id = activePlayer.identity.toLowerCase(); + return id.includes("chrome") || id.includes("chromium"); + } + readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser + property bool compactMode: false + property var widgetData: null + readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled + readonly property int maxTextWidth: { + const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize; + switch (size) { + case 0: + return 0; + case 2: + return 180; + case 3: + return 240; + default: + return 120; + } + } + readonly property int currentContentWidth: { + if (isVerticalOrientation) { + return widgetThickness - horizontalPadding * 2; + } + return 0; + } + readonly property int currentContentHeight: { + if (!isVerticalOrientation) { + return widgetThickness - horizontalPadding * 2; + } + const audioVizHeight = 20; + const playButtonHeight = 24; + return audioVizHeight + Theme.spacingXS + playButtonHeight; + } + + property real scrollAccumulatorY: 0 + property real touchpadThreshold: 100 + + onWheel: function (wheelEvent) { + if (SettingsData.audioScrollMode === "nothing") + return; + + if (SettingsData.audioScrollMode === "volume") { + if (!usePlayerVolume) + return; + + wheelEvent.accepted = true; + + const deltaY = wheelEvent.angleDelta.y; + const isMouseWheelY = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; + + const currentVolume = activePlayer.volume * 100; + + let newVolume = currentVolume; + if (isMouseWheelY) { + if (deltaY > 0) { + newVolume = Math.min(100, currentVolume + SettingsData.audioWheelScrollAmount); + } else if (deltaY < 0) { + newVolume = Math.max(0, currentVolume - SettingsData.audioWheelScrollAmount); + } + } else { + scrollAccumulatorY += deltaY; + if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) { + if (scrollAccumulatorY > 0) { + newVolume = Math.min(100, currentVolume + 1); + } else { + newVolume = Math.max(0, currentVolume - 1); + } + scrollAccumulatorY = 0; + } + } + + activePlayer.volume = newVolume / 100; + } else if (SettingsData.audioScrollMode === "song") { + if (!activePlayer) + return; + + wheelEvent.accepted = true; + + const deltaY = wheelEvent.angleDelta.y; + const isMouseWheelY = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; + + if (isMouseWheelY) { + if (deltaY > 0) { + MprisController.previousOrRewind(); + } else { + activePlayer.next(); + } + } else { + scrollAccumulatorY += deltaY; + if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) { + if (scrollAccumulatorY > 0) { + MprisController.previousOrRewind(); + } else { + activePlayer.next(); + } + scrollAccumulatorY = 0; + } + } + } + } + + content: Component { + Item { + 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 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on implicitWidth { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Column { + id: verticalLayout + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + Item { + width: 20 + height: 20 + anchors.horizontalCenter: parent.horizontalCenter + + AudioVisualization { + anchors.fill: parent + visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled + } + + DankIcon { + anchors.fill: parent + name: "music_note" + size: 20 + color: Theme.primary + visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + } + onClicked: { + if (root.popoutTarget && root.popoutTarget.setTriggerPosition) { + const globalPos = parent.mapToItem(null, 0, 0); + const currentScreen = root.parentScreen || Screen; + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width, root.barSpacing, barPosition, root.barConfig); + root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig); + } + root.clicked(); + } + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + anchors.horizontalCenter: parent.horizontalCenter + color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover + visible: root.playerAvailable + opacity: activePlayer ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" + size: 14 + color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary + } + + MouseArea { + anchors.fill: parent + enabled: root.playerAvailable + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: mouse => { + if (!activePlayer) + return; + if (mouse.button === Qt.LeftButton) { + activePlayer.togglePlaying(); + } else if (mouse.button === Qt.MiddleButton) { + MprisController.previousOrRewind(); + } else if (mouse.button === Qt.RightButton) { + activePlayer.next(); + } + } + } + } + } + + Row { + id: mediaRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + Row { + id: mediaInfo + spacing: Theme.spacingXS + + Item { + width: 20 + height: 20 + anchors.verticalCenter: parent.verticalCenter + + AudioVisualization { + anchors.fill: parent + visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled + } + + DankIcon { + anchors.fill: parent + name: "music_note" + size: 20 + color: Theme.primary + visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled + } + } + + Rectangle { + id: textContainer + readonly property string cachedIdentity: activePlayer ? (activePlayer.identity || "") : "" + readonly property string lowerIdentity: cachedIdentity.toLowerCase() + readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari") + + property string displayText: { + if (!activePlayer || !activePlayer.trackTitle) { + return ""; + } + + const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track"); + const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || ""); + return subtitle.length > 0 ? title + " • " + subtitle : title; + } + + anchors.verticalCenter: parent.verticalCenter + width: contentRoot.measuredTextWidth + height: root.widgetThickness + visible: { + const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize; + return size > 0; + } + clip: true + color: "transparent" + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + } + } + + Item { + id: textClip + anchors.fill: parent + clip: true + + 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(); + } + + 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 + } + } + + SequentialAnimation { + id: textChangeAnimation + + 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 + } + } + } + } + } + + MouseArea { + anchors.fill: parent + enabled: root.playerAvailable + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + if (root.popoutTarget && root.popoutTarget.setTriggerPosition) { + const globalPos = mapToItem(null, 0, 0); + const currentScreen = root.parentScreen || Screen; + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width, root.barSpacing, barPosition, root.barConfig); + root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig); + } + root.clicked(); + } + } + } + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + visible: root.playerAvailable + opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_previous" + size: 12 + color: Theme.widgetTextColor + } + + MouseArea { + id: prevArea + anchors.fill: parent + enabled: root.playerAvailable + cursorShape: Qt.PointingHandCursor + onClicked: MprisController.previousOrRewind() + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + anchors.verticalCenter: parent.verticalCenter + color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover + visible: root.playerAvailable + opacity: activePlayer ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" + size: 14 + color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary + } + + MouseArea { + anchors.fill: parent + enabled: root.playerAvailable + cursorShape: Qt.PointingHandCursor + onClicked: { + if (activePlayer) { + activePlayer.togglePlaying(); + } + } + } + } + + Rectangle { + width: 20 + height: 20 + radius: 10 + anchors.verticalCenter: parent.verticalCenter + color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + visible: playerAvailable + opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3 + + DankIcon { + anchors.centerIn: parent + name: "skip_next" + size: 12 + color: Theme.widgetTextColor + } + + MouseArea { + id: nextArea + anchors.fill: parent + enabled: root.playerAvailable + cursorShape: Qt.PointingHandCursor + onClicked: { + if (activePlayer) { + activePlayer.next(); + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml new file mode 100644 index 0000000..b7d8436 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NetworkMonitor.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Modules.Plugins +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +BasePill { + id: root + + function formatNetworkSpeed(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + " B/s" + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s" + } + } + + Component.onCompleted: { + DgopService.addRef(["network"]) + } + Component.onDestruction: { + DgopService.removeRef(["network"]) + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth + implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + Column { + id: contentColumn + anchors.centerIn: parent + spacing: 2 + visible: root.isVerticalOrientation + + DankIcon { + name: "network_check" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + const rate = DgopService.networkRxRate + if (rate < 1024) return rate.toFixed(0) + if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K" + return (rate / (1024 * 1024)).toFixed(0) + "M" + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.info + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + const rate = DgopService.networkTxRate + if (rate < 1024) return rate.toFixed(0) + if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K" + return (rate / (1024 * 1024)).toFixed(0) + "M" + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.error + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: contentRow + anchors.centerIn: parent + spacing: Theme.spacingS + visible: !root.isVerticalOrientation + + DankIcon { + name: "network_check" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "↓" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.info + } + + StyledText { + text: DgopService.networkRxRate > 0 ? root.formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + wrapMode: Text.NoWrap + + StyledTextMetrics { + id: rxBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: "88.8 MB/s" + } + + width: Math.max(rxBaseline.width, paintedWidth) + } + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + StyledText { + text: "↑" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.error + } + + StyledText { + text: DgopService.networkTxRate > 0 ? root.formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + elide: Text.ElideNone + wrapMode: Text.NoWrap + + StyledTextMetrics { + id: txBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: "88.8 MB/s" + } + + width: Math.max(txBaseline.width, paintedWidth) + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml new file mode 100644 index 0000000..08f5e6b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotepadButton.qml @@ -0,0 +1,367 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "") + + function resolveNotepadInstance() { + if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) { + return null; + } + + const targetScreen = focusedScreenName; + if (targetScreen) { + for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { + var slideout = notepadSlideoutVariants.instances[i]; + if (slideout.modelData && slideout.modelData.name === targetScreen) { + return slideout; + } + } + } + + return notepadSlideoutVariants.instances.length > 0 ? notepadSlideoutVariants.instances[0] : null; + } + + readonly property var notepadInstance: resolveNotepadInstance() + readonly property bool isActive: notepadInstance?.isVisible ?? false + property bool isAutoHideBar: false + + readonly property real minTooltipY: { + if (!parentScreen || !(axis?.isVertical ?? false)) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + return barThickness + barSpacing; + } + + return 0; + } + + readonly property var savedTabEntries: { + const result = []; + const tabs = NotepadStorageService.tabs || []; + for (let i = 0; i < tabs.length; i++) { + const tab = tabs[i]; + if (tab && !tab.isTemporary) { + result.push({ + index: i, + tab: tab + }); + } + } + return result.slice(0, 5); + } + + function openTabByIndex(tabIndex) { + if (tabIndex < 0) + return; + if (root.notepadInstance && typeof root.notepadInstance.show === "function") { + root.notepadInstance.show(); + } + Qt.callLater(() => { + NotepadStorageService.switchToTab(tabIndex); + }); + } + + function openNewNote() { + if (root.notepadInstance && typeof root.notepadInstance.show === "function") { + root.notepadInstance.show(); + } + Qt.callLater(() => { + NotepadStorageService.createNewTab(); + }); + } + + function openContextMenu() { + const screen = root.parentScreen || Screen; + const screenX = screen.x || 0; + const screenY = screen.y || 0; + const isVertical = root.axis?.isVertical ?? false; + const edge = root.axis?.edge ?? "top"; + const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS); + + const globalPos = root.mapToGlobal(root.width / 2, root.height / 2); + const relativeX = globalPos.x - screenX; + const relativeY = globalPos.y - screenY; + + let anchorX = relativeX; + let anchorY = relativeY; + + if (isVertical) { + anchorX = edge === "left" ? (root.barThickness + root.barSpacing + gap) : (screen.width - (root.barThickness + root.barSpacing + gap)); + anchorY = relativeY + root.minTooltipY; + } else { + anchorX = relativeX; + anchorY = edge === "bottom" ? (screen.height - (root.barThickness + root.barSpacing + gap)) : (root.barThickness + root.barSpacing + gap); + } + + contextMenuWindow.showAt(anchorX, anchorY, isVertical, edge, screen); + } + + content: Component { + Item { + implicitWidth: notepadIcon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: notepadIcon + + anchors.centerIn: parent + name: "assignment" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: root.isActive ? Theme.primary : Theme.surfaceText + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + } + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton) { + openContextMenu(); + return; + } + const inst = root.notepadInstance; + if (inst) { + inst.toggle(); + } + } + } + + PanelWindow { + id: contextMenuWindow + + WlrLayershell.namespace: "dms:notepad-context-menu" + + property bool isVertical: false + property string edge: "top" + property point anchorPos: Qt.point(0, 0) + + function showAt(x, y, vertical, barEdge, targetScreen) { + if (targetScreen) { + contextMenuWindow.screen = targetScreen; + } + + anchorPos = Qt.point(x, y); + isVertical = vertical ?? false; + edge = barEdge ?? "top"; + + visible = true; + + if (contextMenuWindow.screen) { + TrayMenuManager.registerMenu(contextMenuWindow.screen.name, contextMenuWindow); + } + } + + function closeMenu() { + visible = false; + + if (contextMenuWindow.screen) { + TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name); + } + } + + screen: null + visible: false + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + anchors { + top: true + left: true + right: true + bottom: true + } + + Component.onDestruction: { + if (contextMenuWindow.screen) { + TrayMenuManager.unregisterMenu(contextMenuWindow.screen.name); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + contextMenuWindow.closeMenu(); + } + } + + MouseArea { + anchors.fill: parent + z: 0 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: contextMenuWindow.closeMenu() + } + + Rectangle { + id: menuContainer + z: 1 + + x: { + if (contextMenuWindow.isVertical) { + if (contextMenuWindow.edge === "left") { + return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x); + } + return Math.max(10, contextMenuWindow.anchorPos.x - width); + } + const left = 10; + const right = contextMenuWindow.width - width - 10; + const want = contextMenuWindow.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + y: { + if (contextMenuWindow.isVertical) { + const top = 10; + const bottom = contextMenuWindow.height - height - 10; + const want = contextMenuWindow.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); + } + if (contextMenuWindow.edge === "top") { + return Math.min(contextMenuWindow.height - height - 10, contextMenuWindow.anchorPos.y); + } + return Math.max(10, contextMenuWindow.anchorPos.y - height); + } + + width: Math.min(260, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)) + height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + opacity: contextMenuWindow.visible ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: -1 + } + + Column { + id: menuColumn + width: parent.width - Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + spacing: 1 + + Repeater { + model: root.savedTabEntries + + Rectangle { + required property var modelData + + width: parent.width + height: 30 + radius: Theme.cornerRadius + color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "description" + size: 16 + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.tab?.title || I18n.tr("Saved Note") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MouseArea { + id: tabArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + contextMenuWindow.closeMenu(); + root.openTabByIndex(modelData.index); + } + } + } + } + + Rectangle { + width: parent.width + height: 30 + radius: Theme.cornerRadius + color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "add" + size: 16 + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Open a new note") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } + + MouseArea { + id: newNoteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + contextMenuWindow.closeMenu(); + root.openNewNote(); + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml new file mode 100644 index 0000000..d7bcc1e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml @@ -0,0 +1,40 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Widgets + +BasePill { + id: root + + property bool hasUnread: false + property bool isActive: false + + content: Component { + Item { + implicitWidth: notifIcon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: notifIcon + anchors.centerIn: parent + name: SessionData.doNotDisturb ? "notifications_off" : "notifications" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: SessionData.doNotDisturb ? Theme.primary : (root.isActive ? Theme.primary : Theme.widgetIconColor) + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.error + anchors.right: notifIcon.right + anchors.top: notifIcon.top + visible: root.hasUnread + } + } + } + + onRightClicked: { + SessionData.setDoNotDisturb(!SessionData.doNotDisturb); + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml new file mode 100644 index 0000000..da9caee --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml @@ -0,0 +1,25 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + anchors.centerIn: parent + name: "power_settings_new" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetIconColor + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml new file mode 100644 index 0000000..a09aa55 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/PrivacyIndicator.qml @@ -0,0 +1,247 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + section: "right" + + property bool showMicIcon: SettingsData.privacyShowMicIcon + property bool showCameraIcon: SettingsData.privacyShowCameraIcon + property bool showScreenSharingIcon: SettingsData.privacyShowScreenShareIcon + + readonly property bool hasActivePrivacy: showMicIcon || showCameraIcon || showScreenSharingIcon || PrivacyService.anyPrivacyActive + readonly property int activeCount: (showMicIcon ? 1 : PrivacyService.microphoneActive) + (showCameraIcon ? 1 : PrivacyService.cameraActive) + (showScreenSharingIcon ? 1 : PrivacyService.screensharingActive) + readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0 + readonly property real contentHeight: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0 + + opacity: hasActivePrivacy ? 1 : 0 + + states: [ + State { + name: "hidden_horizontal" + when: !hasActivePrivacy && !isVerticalOrientation + PropertyChanges { + target: root + width: 0 + } + }, + State { + name: "hidden_vertical" + when: !hasActivePrivacy && isVerticalOrientation + PropertyChanges { + target: root + height: 0 + } + } + ] + + transitions: [ + Transition { + NumberAnimation { + properties: "width,height" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + ] + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + content: Component { + Item { + implicitWidth: root.hasActivePrivacy ? root.contentWidth : 0 + implicitHeight: root.hasActivePrivacy ? root.contentHeight : 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + visible: root.isVerticalOrientation && root.hasActivePrivacy + + Item { + width: 18 + height: 18 + visible: root.showMicIcon || PrivacyService.microphoneActive + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: { + const sourceAudio = AudioService.source?.audio; + const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0; + if (muted) + return "mic_off"; + return "mic"; + } + size: Theme.iconSizeSmall + color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText + filled: PrivacyService.microphoneActive + anchors.centerIn: parent + } + } + + Item { + width: 18 + height: 18 + visible: root.showCameraIcon || PrivacyService.cameraActive + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: "camera_video" + size: Theme.iconSizeSmall + color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText + filled: PrivacyService.cameraActive + anchors.centerIn: parent + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.error + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -2 + anchors.topMargin: -1 + visible: PrivacyService.cameraActive + } + } + + Item { + width: 18 + height: 18 + visible: root.showScreenSharingIcon || PrivacyService.screensharingActive + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: "screen_share" + size: Theme.iconSizeSmall + color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText + filled: PrivacyService.screensharingActive + anchors.centerIn: parent + } + } + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + visible: !root.isVerticalOrientation && root.hasActivePrivacy + + Item { + width: 18 + height: 18 + visible: root.showMicIcon || PrivacyService.microphoneActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: { + const sourceAudio = AudioService.source?.audio; + const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0; + if (muted) + return "mic_off"; + return "mic"; + } + size: Theme.iconSizeSmall + color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText + filled: PrivacyService.microphoneActive + anchors.centerIn: parent + } + } + + Item { + width: 18 + height: 18 + visible: root.showCameraIcon || PrivacyService.cameraActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "camera_video" + size: Theme.iconSizeSmall + color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText + filled: PrivacyService.cameraActive + anchors.centerIn: parent + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.error + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -2 + anchors.topMargin: -1 + visible: PrivacyService.cameraActive + } + } + + Item { + width: 18 + height: 18 + visible: root.showScreenSharingIcon || PrivacyService.screensharingActive + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "screen_share" + size: Theme.iconSizeSmall + color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText + filled: PrivacyService.screensharingActive + anchors.centerIn: parent + } + } + } + } + } + + Rectangle { + id: tooltip + width: tooltipText.contentWidth + Theme.spacingM * 2 + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.color: Theme.outlineMedium + border.width: 1 + visible: false + opacity: root.isMouseHovered && hasActivePrivacy ? 1 : 0 + z: 100 + x: (parent.width - width) / 2 + y: -height - Theme.spacingXS + + StyledText { + id: tooltipText + anchors.centerIn: parent + text: PrivacyService.getPrivacySummary() + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + } + + Rectangle { + width: 8 + height: 8 + color: parent.color + border.color: parent.border.color + border.width: parent.border.width + rotation: 45 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.bottom + anchors.topMargin: -4 + } + + Behavior on opacity { + enabled: hasActivePrivacy && root.visible + + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml new file mode 100644 index 0000000..f5d6de5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RamMonitor.qml @@ -0,0 +1,175 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool showPercentage: true + property bool showIcon: true + property var toggleProcessList + property var popoutTarget: null + property var widgetData: null + property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true + property bool showSwap: (widgetData && widgetData.showSwap !== undefined) ? widgetData.showSwap : false + property bool showInGb: (widgetData && widgetData.showInGb !== undefined) ? widgetData.showInGb : false + readonly property real swapUsage: DgopService.totalSwapKB > 0 ? (DgopService.usedSwapKB / DgopService.totalSwapKB) * 100 : 0 + + signal ramClicked + + Component.onCompleted: { + DgopService.addRef(["memory"]); + } + Component.onDestruction: { + DgopService.removeRef(["memory"]); + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : ramContent.implicitWidth + implicitHeight: root.isVerticalOrientation ? ramColumn.implicitHeight : ramContent.implicitHeight + + Column { + id: ramColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: "developer_board" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.memoryUsage > 90) { + return Theme.tempDanger; + } + + if (DgopService.memoryUsage > 75) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) { + return "--"; + } + + if (root.showInGb) { + return (DgopService.usedMemoryMB / 1024).toFixed(1); + } + + return DgopService.memoryUsage.toFixed(0); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + visible: root.showSwap && DgopService.totalSwapKB > 0 + text: root.swapUsage.toFixed(0) + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: ramContent + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + id: ramIcon + name: "developer_board" + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (DgopService.memoryUsage > 90) { + return Theme.tempDanger; + } + + if (DgopService.memoryUsage > 75) { + return Theme.tempWarning; + } + + return Theme.widgetIconColor; + } + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: textBox + anchors.verticalCenter: parent.verticalCenter + + implicitWidth: root.minimumWidth ? Math.max(ramBaseline.width, ramText.paintedWidth) : ramText.paintedWidth + implicitHeight: ramText.implicitHeight + + width: implicitWidth + height: implicitHeight + + StyledTextMetrics { + id: ramBaseline + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + text: { + let baseText = root.showInGb ? "88.8 GB" : "88%"; + if (!root.showSwap) { + return baseText; + } + if (root.swapUsage < 10) { + return baseText + " · 0%"; + } + return baseText + " · 88%"; + } + } + + StyledText { + id: ramText + text: { + if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) { + return root.showInGb ? "-- GB" : "--%"; + } + + let ramText = ""; + if (root.showInGb) { + ramText = (DgopService.usedMemoryMB / 1024).toFixed(1) + " GB"; + } else { + ramText = DgopService.memoryUsage.toFixed(0) + "%"; + } + + if (root.showSwap && DgopService.totalSwapKB > 0) { + return ramText + " · " + root.swapUsage.toFixed(0) + "%"; + } + return ramText; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + wrapMode: Text.NoWrap + } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + DgopService.setSortBy("memory"); + ramClicked(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml new file mode 100644 index 0000000..ae58802 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/RunningApps.qml @@ -0,0 +1,902 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + enableBackgroundHover: false + enableCursor: false + section: "left" + + property var widgetData: null + property var hoveredItem: null + property var topBar: null + property bool isAutoHideBar: false + property Item windowRoot: (Window.window ? Window.window.contentItem : null) + + readonly property real effectiveBarThickness: { + if (barThickness > 0 && barSpacing > 0) { + return barThickness + barSpacing; + } + const innerPadding = barConfig?.innerPadding ?? 4; + const spacing = barConfig?.spacing ?? 4; + return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing; + } + + readonly property var barBounds: { + if (!parentScreen || !barConfig) { + return { + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "wingSize": 0 + }; + } + const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1)); + return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig); + } + + readonly property real barY: barBounds.y + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + return effectiveBarThickness; + } + + return 0; + } + + property int _desktopEntriesUpdateTrigger: 0 + property int _toplevelsUpdateTrigger: 0 + property int _appIdSubstitutionsTrigger: 0 + + readonly property bool _currentWorkspace: widgetData?.runningAppsCurrentWorkspace !== undefined ? widgetData.runningAppsCurrentWorkspace : SettingsData.runningAppsCurrentWorkspace + readonly property bool _currentMonitor: widgetData?.runningAppsCurrentMonitor !== undefined ? widgetData.runningAppsCurrentMonitor : SettingsData.runningAppsCurrentMonitor + readonly property bool _groupByApp: widgetData?.runningAppsGroupByApp !== undefined ? widgetData.runningAppsGroupByApp : SettingsData.runningAppsGroupByApp + + readonly property var sortedToplevels: { + _toplevelsUpdateTrigger; + let toplevels = CompositorService.sortedToplevels; + if (!toplevels || toplevels.length === 0) + return []; + + if (_currentWorkspace) + toplevels = CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []; + if (_currentMonitor) + toplevels = CompositorService.filterCurrentDisplay(toplevels, parentScreen?.name) || []; + return toplevels; + } + + Connections { + target: CompositorService + function onToplevelsChanged() { + _toplevelsUpdateTrigger++; + } + } + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + _desktopEntriesUpdateTrigger++; + } + } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + _appIdSubstitutionsTrigger++; + } + } + readonly property var groupedWindows: { + if (!_groupByApp) { + return []; + } + try { + if (!sortedToplevels || sortedToplevels.length === 0) { + return []; + } + const appGroups = new Map(); + sortedToplevels.forEach((toplevel, index) => { + if (!toplevel) + return; + const appId = toplevel?.appId || "unknown"; + if (!appGroups.has(appId)) { + appGroups.set(appId, { + "appId": appId, + "windows": [] + }); + } + appGroups.get(appId).windows.push({ + "toplevel": toplevel, + "windowId": index, + "windowTitle": toplevel?.title || "(Unnamed)" + }); + }); + return Array.from(appGroups.values()); + } catch (e) { + return []; + } + } + readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0) + readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6 + + readonly property string focusedAppId: { + if (!sortedToplevels || sortedToplevels.length === 0) + return ""; + for (let i = 0; i < sortedToplevels.length; i++) { + if (sortedToplevels[i].activated) + return sortedToplevels[i].appId || ""; + } + return ""; + } + + visible: windowCount > 0 + + property real scrollAccumulator: 0 + property real touchpadThreshold: 500 + + onWheel: function (wheelEvent) { + wheelEvent.accepted = true; + const deltaY = wheelEvent.angleDelta.y; + const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; + + const windows = root.sortedToplevels; + if (windows.length < 2) + return; + + if (isMouseWheel) { + let currentIndex = -1; + for (var i = 0; i < windows.length; i++) { + if (windows[i].activated) { + currentIndex = i; + break; + } + } + + let nextIndex; + if (deltaY < 0) { + nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, windows.length - 1); + } else { + nextIndex = currentIndex === -1 ? windows.length - 1 : Math.max(currentIndex - 1, 0); + } + + const nextWindow = windows[nextIndex]; + if (nextWindow) + nextWindow.activate(); + } else { + scrollAccumulator += deltaY; + + if (Math.abs(scrollAccumulator) >= touchpadThreshold) { + let currentIndex = -1; + for (var i = 0; i < windows.length; i++) { + if (windows[i].activated) { + currentIndex = i; + break; + } + } + + let nextIndex; + if (scrollAccumulator < 0) { + nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, windows.length - 1); + } else { + nextIndex = currentIndex === -1 ? windows.length - 1 : Math.max(currentIndex - 1, 0); + } + + const nextWindow = windows[nextIndex]; + if (nextWindow) + nextWindow.activate(); + + scrollAccumulator = 0; + } + } + } + + content: Component { + Item { + implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 + implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 + + Loader { + id: layoutLoader + anchors.centerIn: parent + sourceComponent: root.isVerticalOrientation ? columnLayout : rowLayout + } + } + } + + Component { + id: rowLayout + Row { + spacing: Theme.spacingXS + + Repeater { + id: windowRepeater + model: ScriptModel { + values: _groupByApp ? groupedWindows : sortedToplevels + objectProp: _groupByApp ? "appId" : "address" + } + + delegate: Item { + id: delegateItem + + property bool isGrouped: root._groupByApp + property var groupData: isGrouped ? modelData : null + property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData + property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false) + property string appId: isGrouped ? modelData.appId : (modelData.appId || "") + readonly property string effectiveAppId: { + root._appIdSubstitutionsTrigger; + return Paths.moddedAppId(appId); + } + property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)" + property var toplevelObject: toplevelData + property int windowCount: isGrouped ? modelData.windows.length : 1 + property string tooltipText: { + root._desktopEntriesUpdateTrigger; + const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null; + const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown"; + + if (isGrouped && windowCount > 1) { + return appName + " (" + windowCount + " windows)"; + } + return appName + (windowTitle ? " • " + windowTitle : ""); + } + readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120) + + width: visualWidth + height: root.barThickness + + Rectangle { + id: visualContent + width: delegateItem.visualWidth + height: root.iconCellSize + anchors.centerIn: parent + radius: Theme.cornerRadius + color: { + if (isFocused) { + return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45); + } + return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; + } + + // App icon + IconImage { + id: iconImg + anchors.left: parent.left + anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + 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: { + root._desktopEntriesUpdateTrigger; + root._appIdSubstitutionsTrigger; + if (!effectiveAppId) + return ""; + const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId); + return Paths.getAppIcon(effectiveAppId, desktopEntry); + } + smooth: true + mipmap: true + asynchronous: true + visible: status === Image.Ready + layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms" + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + } + + DankIcon { + anchors.left: parent.left + anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + name: "sports_esports" + color: Theme.widgetTextColor + visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId) + } + + Text { + anchors.centerIn: parent + visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId) + text: { + root._desktopEntriesUpdateTrigger; + if (!effectiveAppId) + return "?"; + const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId); + const appName = Paths.getAppName(effectiveAppId, desktopEntry); + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.widgetTextColor + } + + Rectangle { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2 + anchors.bottomMargin: -2 + width: 14 + height: 14 + radius: 7 + color: Theme.primary + visible: isGrouped && windowCount > 1 + z: 10 + + StyledText { + anchors.centerIn: parent + text: windowCount > 9 ? "9+" : windowCount + font.pixelSize: 9 + color: Theme.surface + } + } + + StyledText { + anchors.left: iconImg.right + anchors.leftMargin: Theme.spacingXS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + text: windowTitle + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + elide: Text.ElideRight + maximumLineCount: 1 + } + + DankRipple { + id: itemRipple + cornerRadius: Theme.cornerRadius + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: mouse => { + const pos = mapToItem(visualContent, mouse.x, mouse.y); + itemRipple.trigger(pos.x, pos.y); + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (isGrouped && windowCount > 1) { + let currentIndex = -1; + for (var i = 0; i < groupData.windows.length; i++) { + if (groupData.windows[i].toplevel.activated) { + currentIndex = i; + break; + } + } + const nextIndex = (currentIndex + 1) % groupData.windows.length; + groupData.windows[nextIndex].toplevel.activate(); + } else if (toplevelObject) { + toplevelObject.activate(); + } + } else if (mouse.button === Qt.RightButton) { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + + windowContextMenuLoader.active = true; + if (windowContextMenuLoader.item) { + windowContextMenuLoader.item.currentWindow = toplevelObject; + // Pass bar context + windowContextMenuLoader.item.triggerBarConfig = root.barConfig; + windowContextMenuLoader.item.triggerBarPosition = root.axis.edge === "left" ? 2 : (root.axis.edge === "right" ? 3 : (root.axis.edge === "top" ? 0 : 1)); + windowContextMenuLoader.item.triggerBarThickness = root.barThickness; + windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing; + if (root.isVerticalOrientation) { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const relativeY = globalPos.y - screenY; + // Add minTooltipY offset to account for top bar + const adjustedY = relativeY + root.minTooltipY; + const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge); + } else { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const relativeX = globalPos.x - screenX; + const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; + const isBottom = root.axis?.edge === "bottom"; + const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS); + windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge); + } + } + } else if (mouse.button === Qt.MiddleButton) { + if (toplevelObject) { + if (typeof toplevelObject.close === "function") { + toplevelObject.close(); + } + } + } + } + onEntered: { + root.hoveredItem = delegateItem; + tooltipLoader.active = true; + if (tooltipLoader.item) { + if (root.isVerticalOrientation) { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const relativeY = globalPos.y - screenY; + const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + const isLeft = root.axis?.edge === "left"; + const adjustedY = relativeY + root.minTooltipY; + const finalX = screenX + tooltipX; + tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft); + } else { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height); + const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; + const isBottom = root.axis?.edge === "bottom"; + const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); + tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false); + } + } + } + onExited: { + if (root.hoveredItem === delegateItem) { + root.hoveredItem = null; + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + + tooltipLoader.active = false; + } + } + } + } + } + } + } + + Component { + id: columnLayout + Column { + spacing: Theme.spacingXS + + Repeater { + id: windowRepeater + model: ScriptModel { + values: _groupByApp ? groupedWindows : sortedToplevels + objectProp: _groupByApp ? "appId" : "address" + } + + delegate: Item { + id: delegateItem + + property bool isGrouped: root._groupByApp + property var groupData: isGrouped ? modelData : null + property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData + property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false) + property string appId: isGrouped ? modelData.appId : (modelData.appId || "") + readonly property string effectiveAppId: { + root._appIdSubstitutionsTrigger; + return Paths.moddedAppId(appId); + } + property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)" + property var toplevelObject: toplevelData + property int windowCount: isGrouped ? modelData.windows.length : 1 + property string tooltipText: { + root._desktopEntriesUpdateTrigger; + const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null; + const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown"; + + if (isGrouped && windowCount > 1) { + return appName + " (" + windowCount + " windows)"; + } + return appName + (windowTitle ? " • " + windowTitle : ""); + } + readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120) + + width: root.barThickness + height: root.iconCellSize + + Rectangle { + id: visualContent + width: delegateItem.visualWidth + height: root.iconCellSize + anchors.centerIn: parent + radius: Theme.cornerRadius + color: { + if (isFocused) { + return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45); + } + return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; + } + + IconImage { + id: iconImg + anchors.left: parent.left + anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + 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: { + root._desktopEntriesUpdateTrigger; + root._appIdSubstitutionsTrigger; + if (!effectiveAppId) + return ""; + const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId); + return Paths.getAppIcon(effectiveAppId, desktopEntry); + } + smooth: true + mipmap: true + asynchronous: true + visible: status === Image.Ready + layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms" + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + } + + DankIcon { + anchors.left: parent.left + anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)) / 2) : Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + name: "sports_esports" + color: Theme.widgetTextColor + visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId) + } + + Text { + anchors.centerIn: parent + visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId) + text: { + root._desktopEntriesUpdateTrigger; + if (!effectiveAppId) + return "?"; + const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId); + const appName = Paths.getAppName(effectiveAppId, desktopEntry); + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.widgetTextColor + } + + Rectangle { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2 + anchors.bottomMargin: -2 + width: 14 + height: 14 + radius: 7 + color: Theme.primary + visible: isGrouped && windowCount > 1 + z: 10 + + StyledText { + anchors.centerIn: parent + text: windowCount > 9 ? "9+" : windowCount + font.pixelSize: 9 + color: Theme.surface + } + } + + StyledText { + anchors.left: iconImg.right + anchors.leftMargin: Theme.spacingXS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + visible: !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) + text: windowTitle + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + elide: Text.ElideRight + maximumLineCount: 1 + } + + DankRipple { + id: itemRipple + cornerRadius: Theme.cornerRadius + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: mouse => { + const pos = mapToItem(visualContent, mouse.x, mouse.y); + itemRipple.trigger(pos.x, pos.y); + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (isGrouped && windowCount > 1) { + let currentIndex = -1; + for (var i = 0; i < groupData.windows.length; i++) { + if (groupData.windows[i].toplevel.activated) { + currentIndex = i; + break; + } + } + const nextIndex = (currentIndex + 1) % groupData.windows.length; + groupData.windows[nextIndex].toplevel.activate(); + } else if (toplevelObject) { + toplevelObject.activate(); + } + } else if (mouse.button === Qt.RightButton) { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + + windowContextMenuLoader.active = true; + if (windowContextMenuLoader.item) { + windowContextMenuLoader.item.currentWindow = toplevelObject; + // Pass bar context + windowContextMenuLoader.item.triggerBarConfig = root.barConfig; + windowContextMenuLoader.item.triggerBarPosition = root.axis.edge === "left" ? 2 : (root.axis.edge === "right" ? 3 : (root.axis.edge === "top" ? 0 : 1)); + windowContextMenuLoader.item.triggerBarThickness = root.barThickness; + windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing; + if (root.isVerticalOrientation) { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const relativeY = globalPos.y - screenY; + // Add minTooltipY offset to account for top bar + const adjustedY = relativeY + root.minTooltipY; + const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge); + } else { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const relativeX = globalPos.x - screenX; + const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; + const isBottom = root.axis?.edge === "bottom"; + const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS); + windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge); + } + } + } + } + onEntered: { + root.hoveredItem = delegateItem; + tooltipLoader.active = true; + if (tooltipLoader.item) { + if (root.isVerticalOrientation) { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2); + const screenX = root.parentScreen ? root.parentScreen.x : 0; + const screenY = root.parentScreen ? root.parentScreen.y : 0; + const relativeY = globalPos.y - screenY; + const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + const isLeft = root.axis?.edge === "left"; + const adjustedY = relativeY + root.minTooltipY; + const finalX = screenX + tooltipX; + tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft); + } else { + const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height); + const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; + const isBottom = root.axis?.edge === "bottom"; + const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); + tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false); + } + } + } + onExited: { + if (root.hoveredItem === delegateItem) { + root.hoveredItem = null; + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + + tooltipLoader.active = false; + } + } + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + sourceComponent: DankTooltip {} + } + + Loader { + id: windowContextMenuLoader + active: false + sourceComponent: PanelWindow { + id: contextMenuWindow + + WindowBlur { + targetWindow: contextMenuWindow + blurX: contextMenuRect.x + blurY: contextMenuRect.y + blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0 + blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0 + blurRadius: Theme.cornerRadius + } + + property var currentWindow: null + property bool isVisible: false + property point anchorPos: Qt.point(0, 0) + property bool isVertical: false + property string edge: "top" + + // New properties for bar context + property int triggerBarPosition: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) + property real triggerBarThickness: 0 + property real triggerBarSpacing: 0 + property var triggerBarConfig: null + + readonly property real effectiveBarThickness: { + if (triggerBarThickness > 0 && triggerBarSpacing > 0) { + return triggerBarThickness + triggerBarSpacing; + } + return Math.max(26 + (barConfig?.innerPadding ?? 4) * 0.6, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))) + (barConfig?.spacing ?? 4); + } + + property var barBounds: { + if (!contextMenuWindow.screen || !triggerBarConfig) { + return { + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "wingSize": 0 + }; + } + return SettingsData.getBarBounds(contextMenuWindow.screen, effectiveBarThickness, triggerBarPosition, triggerBarConfig); + } + + property real barY: barBounds.y + + function showAt(x, y, vertical, barEdge) { + screen = root.parentScreen; + anchorPos = Qt.point(x, y); + isVertical = vertical ?? false; + edge = barEdge ?? "top"; + isVisible = true; + visible = true; + + if (screen) { + TrayMenuManager.registerMenu(screen.name, contextMenuWindow); + } + } + + function close() { + isVisible = false; + visible = false; + windowContextMenuLoader.active = false; + + if (screen) { + TrayMenuManager.unregisterMenu(screen.name); + } + } + + implicitWidth: 100 + implicitHeight: 40 + visible: false + color: "transparent" + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: true + left: true + right: true + bottom: true + } + + Component.onDestruction: { + if (screen) { + TrayMenuManager.unregisterMenu(screen.name); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + contextMenuWindow.close(); + } + } + + MouseArea { + anchors.fill: parent + onClicked: contextMenuWindow.close() + } + + Rectangle { + id: contextMenuRect + x: { + if (contextMenuWindow.isVertical) { + if (contextMenuWindow.edge === "left") { + return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x); + } else { + return Math.max(10, contextMenuWindow.anchorPos.x - width); + } + } else { + const left = 10; + const right = contextMenuWindow.width - width - 10; + const want = contextMenuWindow.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + } + y: { + if (contextMenuWindow.isVertical) { + const top = Math.max(barY, 10); + const bottom = contextMenuWindow.height - height - 10; + const want = contextMenuWindow.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); + } else { + return contextMenuWindow.anchorPos.y; + } + } + width: 100 + height: 32 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + } + + StyledText { + anchors.centerIn: parent + text: I18n.tr("Close") + font.pixelSize: Theme.fontSizeSmall + color: Theme.widgetTextColor + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (contextMenuWindow.currentWindow) { + contextMenuWindow.currentWindow.close(); + } + contextMenuWindow.close(); + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml new file mode 100644 index 0000000..7fb9362 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml @@ -0,0 +1,1893 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Services.SystemTray +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + enableBackgroundHover: false + 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()) : []; + } + readonly property var allTrayItems: { + if (!hiddenTrayIds.length) { + return SystemTray.items.values; + } + return SystemTray.items.values.filter(item => { + const itemId = item?.id || ""; + return !hiddenTrayIds.includes(itemId.toLowerCase()); + }); + } + function getTrayItemKey(item) { + const id = item?.id || ""; + const tooltipTitle = item?.tooltipTitle || ""; + if (!tooltipTitle || tooltipTitle === id) { + return id; + } + 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"); + Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(globalX), String(globalY)]); + } + + property int _trayOrderTrigger: 0 + + Connections { + target: SessionData + function onTrayItemOrderChanged() { + root._trayOrderTrigger++; + } + } + + function sortByPreferredOrder(items, trigger) { + void trigger; + const savedOrder = SessionData.trayItemOrder || []; + const orderMap = new Map(); + savedOrder.forEach((key, idx) => orderMap.set(key, idx)); + + return [...items].sort((a, b) => { + const keyA = getTrayItemKey(a); + const keyB = getTrayItemKey(b); + const orderA = orderMap.has(keyA) ? orderMap.get(keyA) : 10000 + items.indexOf(a); + const orderB = orderMap.has(keyB) ? orderMap.get(keyB) : 10000 + items.indexOf(b); + return orderA - orderB; + }); + } + + readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger) + readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item)) + readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item))) + readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({ + key: getTrayItemKey(item), + 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) + return; + + const fromKey = mainBarItems[visibleFromIndex]?.key ?? null; + const toKey = mainBarItems[visibleToIndex]?.key ?? null; + if (!fromKey || !toKey) + return; + + const fullOrder = [...allSortedTrayItemKeys]; + const fullFromIndex = fullOrder.indexOf(fromKey); + const fullToIndex = fullOrder.indexOf(toKey); + if (fullFromIndex < 0 || fullToIndex < 0) + return; + + const movedKey = fullOrder.splice(fullFromIndex, 1)[0]; + fullOrder.splice(fullToIndex, 0, movedKey); + SessionData.setTrayItemOrder(fullOrder); + } + + property int draggedIndex: -1 + 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 + + states: [ + State { + name: "hidden_horizontal" + when: allTrayItems.length === 0 && !isVerticalOrientation + PropertyChanges { + target: root + width: 0 + } + }, + State { + name: "hidden_vertical" + when: allTrayItems.length === 0 && isVerticalOrientation + PropertyChanges { + target: root + height: 0 + } + } + ] + + transitions: [ + Transition { + NumberAnimation { + properties: "width,height" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + ] + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6 + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + const estimatedTopBarHeight = barThickness + barSpacing; + return estimatedTopBarHeight; + } + + return 0; + } + + readonly property string autoBarShadowDirection: { + const edge = root.axis?.edge; + switch (edge) { + case "top": + return "top"; + case "bottom": + return "bottom"; + case "left": + return "left"; + case "right": + return "right"; + default: + return "bottom"; + } + } + readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection + + property bool menuOpen: false + property var currentTrayMenu: null + + content: Component { + Item { + implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 + implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 + + Loader { + id: layoutLoader + anchors.centerIn: parent + sourceComponent: root.isVerticalOrientation ? columnComp : rowComp + } + } + } + + Component { + id: rowComp + Row { + spacing: 0 + layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight + + Repeater { + model: ScriptModel { + values: root.displayedMainBarItems + objectProp: "key" + } + + delegate: Item { + id: delegateRoot + property var trayItem: modelData.item + property string itemKey: modelData.key + property string iconSource: root.trayIconSourceFor(trayItem) + + width: root.trayItemSize + height: root.barThickness + 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 { + x: delegateRoot.shiftOffset + Behavior on x { + 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 { + x: 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; + if (root.useOverflowPopup) + 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.x - dragHandler.dragStartPos.x); + if (distance > 5) { + dragHandler.dragging = true; + root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index; + root.dropTargetIndex = root.draggedIndex; + } + } + if (!dragHandler.dragging) + return; + + const axisOffset = mouse.x - dragHandler.dragStartPos.x; + dragHandler.dragAxisOffset = axisOffset; + const itemSize = root.trayItemSize; + const slotOffset = Math.round(axisOffset / itemSize); + 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; + } + } + + 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; + } + if (root.useOverflowPopup) + root.menuOpen = false; + root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); + } + } + } + } + + Item { + width: root.trayItemSize + height: root.barThickness + visible: root.hasHiddenItems + + Rectangle { + id: caretButton + width: root.trayItemSize + height: root.trayItemSize + anchors.centerIn: parent + radius: Theme.cornerRadius + color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: root.toggleIconName() + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + } + + DankRipple { + id: caretRipple + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: caretArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + caretRipple.trigger(mouse.x, mouse.y); + } + onClicked: root.menuOpen = !root.menuOpen + } + } + } + + 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); + } + } + } + } + + Component { + id: columnComp + 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.reverseInlineVertical ? [] : root.displayedMainBarItems + objectProp: "key" + } + delegate: verticalMainTrayItemDelegate + } + + Repeater { + model: ScriptModel { + values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : [] + objectProp: "key" + } + delegate: inlineExpandedTrayItemDelegate + } + + Item { + width: root.barThickness + height: root.trayItemSize + visible: root.hasHiddenItems + + Rectangle { + id: caretButtonVert + width: root.trayItemSize + height: root.trayItemSize + anchors.centerIn: parent + radius: Theme.cornerRadius + color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: root.toggleIconName() + size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetTextColor + } + + DankRipple { + id: caretRippleVert + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: caretAreaVert + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + caretRippleVert.trigger(mouse.x, mouse.y); + } + onClicked: root.menuOpen = !root.menuOpen + } + } + } + + Repeater { + model: ScriptModel { + values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems + objectProp: "key" + } + delegate: inlineExpandedTrayItemDelegate + } + + Repeater { + model: ScriptModel { + values: root.reverseInlineVertical ? root.displayedMainBarItems : [] + objectProp: "key" + } + delegate: verticalMainTrayItemDelegate + } + } + } + + PanelWindow { + id: overflowMenu + + WindowBlur { + targetWindow: overflowMenu + blurX: menuContainer.x + blurY: menuContainer.y + blurWidth: root.menuOpen ? menuContainer.width : 0 + blurHeight: root.menuOpen ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + + visible: root.useOverflowPopup && root.menuOpen + screen: root.parentScreen + WlrLayershell.layer: WlrLayershell.Top + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: { + if (!root.menuOpen) + return WlrKeyboardFocus.None; + if (CompositorService.useHyprlandFocusGrab) + return WlrKeyboardFocus.OnDemand; + return WlrKeyboardFocus.Exclusive; + } + WlrLayershell.namespace: "dms:tray-overflow-menu" + color: "transparent" + + HyprlandFocusGrab { + windows: [overflowMenu] + active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + if (root.useOverflowPopup) + root.menuOpen = false; + } + } + + Component.onDestruction: { + if (root.parentScreen) { + TrayMenuManager.unregisterMenu(root.parentScreen.name); + } + } + + function close() { + root.menuOpen = false; + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(overflowMenu.screen) : (screen?.devicePixelRatio || 1) + property point anchorPos: Qt.point(screen.width / 2, screen.height / 2) + + property var barBounds: { + if (!overflowMenu.screen || !root.barConfig) { + return { + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "wingSize": 0 + }; + } + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + return SettingsData.getBarBounds(overflowMenu.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig); + } + + property real barX: barBounds.x + property real barY: barBounds.y + property real barWidth: barBounds.width + property real barHeight: barBounds.height + + readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)) + readonly property var adjacentBarInfo: parentScreen ? SettingsData.getAdjacentBarInfo(parentScreen, barPosition, root.barConfig) : ({ + "topBar": 0, + "bottomBar": 0, + "leftBar": 0, + "rightBar": 0 + }) + readonly property real effectiveBarSize: root.barThickness + root.barSpacing + + readonly property real maskX: { + const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0; + const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0; + return Math.max(triggeringBarX, adjacentLeftBar); + } + + readonly property real maskY: { + const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0; + const adjacentTopBar = adjacentBarInfo?.topBar ?? 0; + return Math.max(triggeringBarY, adjacentTopBar); + } + + readonly property real maskWidth: { + const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0; + const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0; + const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar); + return Math.max(100, width - maskX - rightExclusion); + } + + readonly property real maskHeight: { + const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0; + const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0; + const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar); + return Math.max(100, height - maskY - bottomExclusion); + } + + mask: Region { + item: Rectangle { + x: overflowMenu.maskX + y: overflowMenu.maskY + width: overflowMenu.maskWidth + height: overflowMenu.maskHeight + } + } + + onVisibleChanged: { + if (visible) { + if (currentTrayMenu) { + currentTrayMenu.showMenu = false; + } + if (root.parentScreen) { + TrayMenuManager.registerMenu(root.parentScreen.name, overflowMenu); + } + PopoutManager.closeAllPopouts(); + ModalManager.closeAllModalsExcept(null); + updatePosition(); + } else if (!visible && root.parentScreen) { + TrayMenuManager.unregisterMenu(root.parentScreen.name); + } + } + + MouseArea { + x: overflowMenu.maskX + y: overflowMenu.maskY + width: overflowMenu.maskWidth + height: overflowMenu.maskHeight + z: -1 + enabled: root.menuOpen + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + const clickX = mouse.x + overflowMenu.maskX; + const clickY = mouse.y + overflowMenu.maskY; + const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height; + + if (!outsideContent) + return; + root.menuOpen = false; + } + } + + FocusScope { + id: overflowFocusScope + anchors.fill: parent + focus: true + + Keys.onEscapePressed: { + root.menuOpen = false; + } + } + + function updatePosition() { + const globalPos = root.mapToGlobal(0, 0); + const screenX = screen.x || 0; + const screenY = screen.y || 0; + const relativeX = globalPos.x - screenX; + const relativeY = globalPos.y - screenY; + + if (root.isVerticalOrientation) { + const edge = root.axis?.edge; + let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance); + const adjustedY = relativeY + root.height / 2 + root.minTooltipY; + anchorPos = Qt.point(targetX, adjustedY); + } else { + let targetY = root.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance; + anchorPos = Qt.point(relativeX + root.width / 2, targetY); + } + } + + Item { + id: menuContainer + objectName: "overflowMenuContainer" + + readonly property real rawWidth: { + const itemCount = root.hiddenBarItems.length; + const cols = Math.min(5, itemCount); + const itemSize = root.trayItemSize + 4; + const spacing = 2; + return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2; + } + readonly property real rawHeight: { + const itemCount = root.hiddenBarItems.length; + const cols = Math.min(5, itemCount); + const rows = Math.ceil(itemCount / cols); + const itemSize = root.trayItemSize + 4; + const spacing = 2; + return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2; + } + + readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr) + readonly property real alignedHeight: Theme.px(rawHeight, overflowMenu.dpr) + + width: alignedWidth + height: alignedHeight + + x: Theme.snap((() => { + if (root.isVerticalOrientation) { + const edge = root.axis?.edge; + if (edge === "left") { + const targetX = overflowMenu.anchorPos.x; + return Math.min(overflowMenu.screen.width - alignedWidth - 10, targetX); + } else { + const targetX = overflowMenu.anchorPos.x - alignedWidth; + return Math.max(10, targetX); + } + } else { + const left = 10; + const right = overflowMenu.width - alignedWidth - 10; + const want = overflowMenu.anchorPos.x - alignedWidth / 2; + return Math.max(left, Math.min(right, want)); + } + })(), overflowMenu.dpr) + + y: Theme.snap((() => { + if (root.isVerticalOrientation) { + const top = Math.max(overflowMenu.barY, 10); + const bottom = overflowMenu.height - alignedHeight - 10; + const want = overflowMenu.anchorPos.y - alignedHeight / 2; + return Math.max(top, Math.min(bottom, want)); + } else { + if (root.isAtBottom) { + const targetY = overflowMenu.anchorPos.y - alignedHeight; + return Math.max(10, targetY); + } else { + const targetY = overflowMenu.anchorPos.y; + return Math.min(overflowMenu.screen.height - alignedHeight - 10, targetY); + } + } + })(), overflowMenu.dpr) + + opacity: root.menuOpen ? 1 : 0 + scale: root.menuOpen ? 1 : 0.85 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + ElevationShadow { + id: bgShadowLayer + anchors.fill: parent + level: Theme.elevationLevel3 + direction: root.effectiveShadowDirection + fallbackOffset: 6 + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + targetRadius: Theme.cornerRadius + sourceRect.antialiasing: true + sourceRect.smooth: true + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled + layer.smooth: true + layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) + layer.textureMirroring: ShaderEffectSource.MirrorVertically + layer.samples: 4 + } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Theme.cornerRadius + border.color: BlurService.borderColor + border.width: BlurService.borderWidth + z: 100 + } + + Grid { + id: menuGrid + anchors.centerIn: parent + columns: Math.min(5, root.hiddenBarItems.length) + spacing: 2 + rowSpacing: 2 + + Repeater { + model: root.hiddenBarItems + + delegate: Rectangle { + property var trayItem: modelData + property string iconSource: root.trayIconSourceFor(trayItem) + + width: root.trayItemSize + 4 + height: root.trayItemSize + 4 + radius: Theme.cornerRadius + color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) + + IconImage { + id: menuIconImg + 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: parent.iconSource + asynchronous: true + smooth: true + mipmap: true + visible: status === Image.Ready + } + + Text { + anchors.centerIn: parent + visible: !menuIconImg.visible + text: { + const itemId = trayItem?.id || ""; + if (!itemId) + return "?"; + return itemId.charAt(0).toUpperCase(); + } + font.pixelSize: 10 + color: Theme.widgetTextColor + } + + MouseArea { + id: itemArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (!trayItem) + return; + if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) { + trayItem.activate(); + root.menuOpen = false; + return; + } + if (!trayItem.hasMenu) { + const gp = itemArea.mapToGlobal(mouse.x, mouse.y); + root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y)); + return; + } + root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); + } + } + } + } + } + } + } + + Component { + id: trayMenuComponent + + Rectangle { + id: menuRoot + + property var trayItem: null + property var anchorItem: null + property var parentScreen: null + property bool isAtBottom: false + property bool isVertical: false + property var axis: null + property bool showMenu: false + property var menuHandle: null + + ListModel { + id: entryStack + } + function topEntry() { + return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null; + } + + function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { + trayItem = item; + anchorItem = anchor; + parentScreen = screen; + isAtBottom = atBottom; + isVertical = vertical; + axis = axisObj; + menuHandle = item?.menu; + + if (parentScreen) { + for (var i = 0; i < Quickshell.screens.length; i++) { + const s = Quickshell.screens[i]; + if (s === parentScreen) { + menuWindow.screen = s; + break; + } + } + } + + showMenu = true; + } + + function close() { + showMenu = false; + } + + Connections { + target: menuWindow + function onVisibleChanged() { + if (menuWindow.visible && parentScreen) { + TrayMenuManager.registerMenu(parentScreen.name, menuRoot); + } else if (!menuWindow.visible && parentScreen) { + TrayMenuManager.unregisterMenu(parentScreen.name); + } + } + } + + Component.onDestruction: { + if (parentScreen) { + TrayMenuManager.unregisterMenu(parentScreen.name); + } + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + menuRoot.close(); + } + } + + function closeWithAction() { + close(); + } + + function showSubMenu(entry) { + if (!entry || !entry.hasChildren) + return; + + entryStack.append({ + handle: entry + }); + + const h = entry.menu || entry; + if (h && typeof h.updateLayout === "function") + h.updateLayout(); + + submenuHydrator.menu = h; + submenuHydrator.open(); + Qt.callLater(() => submenuHydrator.close()); + } + + function goBack() { + if (!entryStack.count) + return; + entryStack.remove(entryStack.count - 1); + } + + width: 0 + height: 0 + color: "transparent" + + PanelWindow { + id: menuWindow + + WindowBlur { + targetWindow: menuWindow + blurX: trayMenuContainer.x + blurY: trayMenuContainer.y + blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0 + blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + + WlrLayershell.namespace: "dms:tray-menu-window" + visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) + WlrLayershell.layer: WlrLayershell.Top + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: { + if (!menuRoot.showMenu) + return WlrKeyboardFocus.None; + if (CompositorService.useHyprlandFocusGrab) + return WlrKeyboardFocus.OnDemand; + return WlrKeyboardFocus.Exclusive; + } + color: "transparent" + + HyprlandFocusGrab { + windows: [menuWindow] + active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(menuWindow.screen) : (screen?.devicePixelRatio || 1) + property point anchorPos: Qt.point(screen.width / 2, screen.height / 2) + + property var barBounds: { + if (!menuWindow.screen || !root.barConfig) { + return { + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "wingSize": 0 + }; + } + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + return SettingsData.getBarBounds(menuWindow.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig); + } + + property real barX: barBounds.x + property real barY: barBounds.y + property real barWidth: barBounds.width + property real barHeight: barBounds.height + + readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)) + readonly property var adjacentBarInfo: menuRoot.parentScreen ? SettingsData.getAdjacentBarInfo(menuRoot.parentScreen, barPosition, root.barConfig) : ({ + "topBar": 0, + "bottomBar": 0, + "leftBar": 0, + "rightBar": 0 + }) + readonly property real effectiveBarSize: root.barThickness + root.barSpacing + + readonly property real maskX: { + const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0; + const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0; + return Math.max(triggeringBarX, adjacentLeftBar); + } + + readonly property real maskY: { + const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0; + const adjacentTopBar = adjacentBarInfo?.topBar ?? 0; + return Math.max(triggeringBarY, adjacentTopBar); + } + + readonly property real maskWidth: { + const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0; + const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0; + const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar); + return Math.max(100, width - maskX - rightExclusion); + } + + readonly property real maskHeight: { + const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0; + const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0; + const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar); + return Math.max(100, height - maskY - bottomExclusion); + } + + mask: Region { + item: Rectangle { + x: menuWindow.maskX + y: menuWindow.maskY + width: menuWindow.maskWidth + height: menuWindow.maskHeight + } + } + + onVisibleChanged: { + if (visible) { + updatePosition(); + if (root.useOverflowPopup) + root.menuOpen = false; + PopoutManager.closeAllPopouts(); + ModalManager.closeAllModalsExcept(null); + } + } + + MouseArea { + x: menuWindow.maskX + y: menuWindow.maskY + width: menuWindow.maskWidth + height: menuWindow.maskHeight + z: -1 + enabled: menuRoot.showMenu + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + const clickX = mouse.x + menuWindow.maskX; + const clickY = mouse.y + menuWindow.maskY; + const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height; + + if (!outsideContent) + return; + menuRoot.close(); + } + } + + FocusScope { + id: menuFocusScope + anchors.fill: parent + focus: true + + Keys.onEscapePressed: { + if (entryStack.count > 0) { + menuRoot.goBack(); + } else { + menuRoot.close(); + } + } + } + + function updatePosition() { + const targetItem = (typeof menuRoot !== "undefined" && menuRoot.anchorItem) ? menuRoot.anchorItem : root; + + const isFromOverflowMenu = targetItem.objectName === "overflowMenuContainer"; + + if (isFromOverflowMenu) { + if (menuRoot.isVertical) { + const edge = menuRoot.axis?.edge; + let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance); + const targetY = targetItem.y + targetItem.height / 2; + anchorPos = Qt.point(targetX, targetY); + } else { + const targetX = targetItem.x + targetItem.width / 2; + let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance; + anchorPos = Qt.point(targetX, targetY); + } + } else { + const globalPos = targetItem.mapToGlobal(0, 0); + const screenX = screen.x || 0; + const screenY = screen.y || 0; + const relativeX = globalPos.x - screenX; + const relativeY = globalPos.y - screenY; + + if (menuRoot.isVertical) { + const edge = menuRoot.axis?.edge; + let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance); + const adjustedY = relativeY + targetItem.height / 2 + root.minTooltipY; + anchorPos = Qt.point(targetX, adjustedY); + } else { + let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance; + anchorPos = Qt.point(relativeX + targetItem.width / 2, targetY); + } + } + } + + Item { + id: trayMenuContainer + + readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2)) + readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2) + + readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr) + readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr) + + width: alignedWidth + height: alignedHeight + + x: Theme.snap((() => { + if (menuRoot.isVertical) { + const edge = menuRoot.axis?.edge; + if (edge === "left") { + const targetX = menuWindow.anchorPos.x; + return Math.min(menuWindow.screen.width - alignedWidth - 10, targetX); + } else { + const targetX = menuWindow.anchorPos.x - alignedWidth; + return Math.max(10, targetX); + } + } else { + const left = 10; + const right = menuWindow.width - alignedWidth - 10; + const want = menuWindow.anchorPos.x - alignedWidth / 2; + return Math.max(left, Math.min(right, want)); + } + })(), menuWindow.dpr) + + y: Theme.snap((() => { + if (menuRoot.isVertical) { + const top = Math.max(menuWindow.barY, 10); + const bottom = menuWindow.height - alignedHeight - 10; + const want = menuWindow.anchorPos.y - alignedHeight / 2; + return Math.max(top, Math.min(bottom, want)); + } else { + if (menuRoot.isAtBottom) { + const targetY = menuWindow.anchorPos.y - alignedHeight; + return Math.max(10, targetY); + } else { + const targetY = menuWindow.anchorPos.y; + return Math.min(menuWindow.screen.height - alignedHeight - 10, targetY); + } + } + })(), menuWindow.dpr) + + opacity: menuRoot.showMenu ? 1 : 0 + scale: menuRoot.showMenu ? 1 : 0.85 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + ElevationShadow { + id: menuBgShadowLayer + anchors.fill: parent + level: Theme.elevationLevel3 + direction: root.effectiveShadowDirection + fallbackOffset: 6 + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + targetRadius: Theme.cornerRadius + sourceRect.antialiasing: true + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled + layer.smooth: true + layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) + layer.textureMirroring: ShaderEffectSource.MirrorVertically + } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Theme.cornerRadius + border.color: BlurService.borderColor + border.width: BlurService.borderWidth + z: 100 + } + + QsMenuAnchor { + id: submenuHydrator + anchor.window: menuWindow + } + + QsMenuOpener { + id: rootOpener + menu: menuRoot.menuHandle + } + + QsMenuOpener { + id: subOpener + menu: { + const e = menuRoot.topEntry(); + return e ? (e.menu || e) : null; + } + } + + Column { + id: menuColumn + + width: parent.width - Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + spacing: 1 + + Rectangle { + visible: entryStack.count === 0 + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: menuRoot.trayItem?.id || "Unknown" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + elide: Text.ElideMiddle + width: parent.width - Theme.spacingS * 2 - 24 + } + + DankIcon { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off" + size: 16 + color: Theme.widgetTextColor + } + + MouseArea { + id: visibilityToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const itemKey = root.getTrayItemKey(menuRoot.trayItem); + if (!itemKey) + return; + if (SessionData.isHiddenTrayId(itemKey)) { + SessionData.showTrayId(itemKey); + } else { + SessionData.hideTrayId(itemKey); + } + menuRoot.closeWithAction(); + } + } + } + + Rectangle { + visible: entryStack.count === 0 + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Rectangle { + visible: entryStack.count > 0 + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankIcon { + name: "arrow_back" + size: 16 + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Back") + font.pixelSize: Theme.fontSizeSmall + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: backArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: menuRoot.goBack() + } + } + + Rectangle { + visible: entryStack.count > 0 + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Repeater { + model: entryStack.count ? (subOpener.children ? subOpener.children : (menuRoot.topEntry()?.children || [])) : rootOpener.children + + Rectangle { + property var menuEntry: modelData + + width: menuColumn.width + height: menuEntry?.isSeparator ? 1 : 28 + radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadius + color: { + if (menuEntry?.isSeparator) + return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2); + return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0); + } + + MouseArea { + id: itemArea + anchors.fill: parent + hoverEnabled: true + enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false) + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!menuEntry || menuEntry.isSeparator) + return; + if (menuEntry.hasChildren) { + menuRoot.showSubMenu(menuEntry); + return; + } + + if (typeof menuEntry.activate === "function") { + menuEntry.activate(); + } else if (typeof menuEntry.triggered === "function") { + menuEntry.triggered(); + } + Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot); + } + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + visible: !menuEntry?.isSeparator + + Rectangle { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0 + radius: menuEntry?.buttonType === 2 ? 8 : 2 + border.width: 1 + border.color: Theme.outline + color: "transparent" + + Rectangle { + anchors.centerIn: parent + width: parent.width - 6 + height: parent.height - 6 + radius: parent.radius - 3 + color: Theme.primary + visible: menuEntry?.checkState === 2 + } + + DankIcon { + anchors.centerIn: parent + name: "check" + size: 10 + color: Theme.primaryText + visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2 + } + } + + Item { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + visible: (menuEntry?.icon ?? "") !== "" + + Image { + anchors.fill: parent + source: menuEntry?.icon || "" + sourceSize.width: 16 + sourceSize.height: 16 + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + StyledText { + text: menuEntry?.text || "" + font.pixelSize: Theme.fontSizeSmall + color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + width: Math.max(150, parent.width - 64) + wrapMode: Text.NoWrap + } + + Item { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "chevron_right" + size: 14 + color: Theme.widgetTextColor + visible: menuEntry?.hasChildren ?? false + } + } + } + } + } + } + } + } + } + } + + function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { + if (!screen) + return; + if (currentTrayMenu) { + currentTrayMenu.showMenu = false; + currentTrayMenu.destroy(); + currentTrayMenu = null; + } + + PopoutManager.closeAllPopouts(); + ModalManager.closeAllModalsExcept(null); + + currentTrayMenu = trayMenuComponent.createObject(null); + if (!currentTrayMenu) + return; + currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj); + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml new file mode 100644 index 0000000..696c76c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml @@ -0,0 +1,187 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + property bool isActive: false + readonly property bool hasUpdates: SystemUpdateService.updateCount > 0 + readonly property bool isChecking: SystemUpdateService.isChecking + readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError + + opacity: shouldHide ? 0 : 1 + + states: [ + State { + name: "hidden_horizontal" + when: root.shouldHide && !isVerticalOrientation + PropertyChanges { + target: root + width: 0 + } + }, + State { + name: "hidden_vertical" + when: root.shouldHide && isVerticalOrientation + PropertyChanges { + target: root + height: 0 + } + } + ] + + transitions: [ + Transition { + NumberAnimation { + properties: "width,height" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + ] + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Ref { + service: SystemUpdateService + } + + content: Component { + Item { + implicitWidth: root.isVerticalOrientation ? root.widgetThickness : updaterIcon.implicitWidth + implicitHeight: root.widgetThickness + + DankIcon { + id: statusIcon + anchors.centerIn: parent + visible: root.isVerticalOrientation + name: { + if (root.isChecking) + return "refresh"; + if (SystemUpdateService.hasError) + return "error"; + if (root.hasUpdates) + return "system_update_alt"; + return "check_circle"; + } + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (SystemUpdateService.hasError) + return Theme.error; + if (root.hasUpdates) + return Theme.primary; + return root.isActive ? Theme.primary : Theme.surfaceText; + } + + RotationAnimation { + id: rotationAnimation + target: statusIcon + property: "rotation" + from: 0 + to: 360 + duration: 1000 + running: root.isChecking + loops: Animation.Infinite + + onRunningChanged: { + if (!running) { + statusIcon.rotation = 0; + } + } + } + } + + Rectangle { + width: 8 + height: 8 + radius: 4 + color: Theme.error + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: (barConfig?.removeWidgetPadding ?? false) ? 0 : 6 + anchors.topMargin: (barConfig?.removeWidgetPadding ?? false) ? 0 : 6 + visible: root.isVerticalOrientation && root.hasUpdates && !root.isChecking + } + + Row { + id: updaterIcon + anchors.centerIn: parent + spacing: Theme.spacingXS + visible: !root.isVerticalOrientation + + DankIcon { + id: statusIconHorizontal + anchors.verticalCenter: parent.verticalCenter + name: { + if (root.isChecking) + return "refresh"; + if (SystemUpdateService.hasError) + return "error"; + if (root.hasUpdates) + return "system_update_alt"; + return "check_circle"; + } + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: { + if (SystemUpdateService.hasError) + return Theme.error; + if (root.hasUpdates) + return Theme.primary; + return root.isActive ? Theme.primary : Theme.surfaceText; + } + + RotationAnimation { + id: rotationAnimationHorizontal + target: statusIconHorizontal + property: "rotation" + from: 0 + to: 360 + duration: 1000 + running: root.isChecking + loops: Animation.Infinite + + onRunningChanged: { + if (!running) { + statusIconHorizontal.rotation = 0; + } + } + } + } + + StyledText { + id: countText + anchors.verticalCenter: parent.verticalCenter + text: SystemUpdateService.updateCount.toString() + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + visible: root.hasUpdates && !root.isChecking + } + } + } + } + + MouseArea { + z: 1 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + root.triggerRipple(this, mouse.x, mouse.y); + if (popoutTarget && popoutTarget.setTriggerPosition) { + const globalPos = root.visualContent.mapToItem(null, 0, 0); + const currentScreen = parentScreen || Screen; + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig); + popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, root.barSpacing, root.barConfig); + } + root.clicked(); + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml new file mode 100644 index 0000000..fec33d0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Vpn.qml @@ -0,0 +1,141 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + Ref { + service: DMSNetworkService + } + + property bool isHovered: clickArea.containsMouse + property bool isAutoHideBar: false + + readonly property real minTooltipY: { + if (!parentScreen || !isVerticalOrientation) { + return 0; + } + + if (isAutoHideBar) { + return 0; + } + + if (parentScreen.y > 0) { + return barThickness + barSpacing; + } + + return 0; + } + + signal toggleVpnPopup + + content: Component { + Item { + implicitWidth: icon.width + implicitHeight: root.widgetThickness - root.horizontalPadding * 2 + + DankIcon { + id: icon + + name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off" + size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: DMSNetworkService.connected ? Theme.primary : Theme.widgetIconColor + opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 + anchors.centerIn: parent + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.InOutQuad + } + } + } + } + } + + Loader { + id: tooltipLoader + active: false + sourceComponent: DankTooltip {} + } + + MouseArea { + id: clickArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + enabled: !DMSNetworkService.isBusy + onPressed: event => { + root.triggerRipple(this, event.x, event.y); + switch (event.button) { + case Qt.RightButton: + DMSNetworkService.toggleVpn(); + return; + case Qt.LeftButton: + root.toggleVpnPopup(); + return; + } + } + onEntered: { + if (!root.parentScreen || (popoutTarget?.shouldBeVisible)) + return; + tooltipLoader.active = true; + if (!tooltipLoader.item) + return; + let tooltipText = ""; + if (!DMSNetworkService.connected) { + tooltipText = "VPN Disconnected"; + } else { + const names = DMSNetworkService.activeNames || []; + if (names.length <= 1) { + const name = names[0] || ""; + const maxLength = 25; + const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name; + tooltipText = "VPN Connected • " + displayName; + } else { + const name = names[0]; + const maxLength = 20; + const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name; + tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1); + } + } + + if (root.isVerticalOrientation) { + const globalPos = mapToGlobal(width / 2, height / 2); + const currentScreen = root.parentScreen || Screen; + const screenX = currentScreen ? currentScreen.x : 0; + const screenY = currentScreen ? currentScreen.y : 0; + const relativeY = globalPos.y - screenY; + const adjustedY = relativeY + root.minTooltipY; + const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); + const isLeft = root.axis?.edge === "left"; + tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft); + } else { + const isBottom = root.axis?.edge === "bottom"; + const globalPos = mapToGlobal(width / 2, 0); + const currentScreen = root.parentScreen || Screen; + + let tooltipY; + if (isBottom) { + const tooltipHeight = Theme.fontSizeSmall * 1.5 + Theme.spacingS * 2; + tooltipY = currentScreen.height - root.barThickness - root.barSpacing - Theme.spacingXS - tooltipHeight; + } else { + tooltipY = root.barThickness + root.barSpacing + Theme.spacingXS; + } + + tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, currentScreen, false, false); + } + } + onExited: { + if (tooltipLoader.item) { + tooltipLoader.item.hide(); + } + tooltipLoader.active = false; + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml new file mode 100644 index 0000000..5dbe53a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/Weather.qml @@ -0,0 +1,80 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins +import qs.Services +import qs.Widgets + +BasePill { + id: root + + visible: SettingsData.weatherEnabled + + Ref { + service: WeatherService + } + + content: Component { + Item { + implicitWidth: { + if (!SettingsData.weatherEnabled) return 0 + if (root.isVerticalOrientation) return root.widgetThickness - root.horizontalPadding * 2 + return Math.min(100 - root.horizontalPadding * 2, weatherRow.implicitWidth) + } + implicitHeight: root.isVerticalOrientation ? weatherColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2) + + Column { + id: weatherColumn + visible: root.isVerticalOrientation + anchors.centerIn: parent + spacing: 1 + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetIconColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (!WeatherService.weather.available) { + return "--"; + } + const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp; + return temp; + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Row { + id: weatherRow + visible: !root.isVerticalOrientation + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.barIconSize(root.barThickness, -6, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + color: Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (!WeatherService.weather.available) { + return "--°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp; + return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C"); + } + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml new file mode 100644 index 0000000..3721e35 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -0,0 +1,1981 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland +import Quickshell.I3 +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property bool isVertical: axis?.isVertical ?? false + property var axis: null + property string screenName: "" + property real widgetHeight: 30 + property real barThickness: 48 + property var barConfig: null + property var blurBarWindow: null + property var hyprlandOverviewLoader: null + property var parentScreen: null + + readonly property real _leftMargin: { + if (isVertical) + return 0; + root.x; + if (!root.parent) + return 0; + const gap = root.mapToItem(null, 0, 0).x; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _rightMargin: { + if (isVertical) + return 0; + root.x; + root.width; + if (!root.parent || !blurBarWindow) + return 0; + const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _topMargin: { + if (!isVertical) + return 0; + root.y; + if (!root.parent) + return 0; + const gap = root.mapToItem(null, 0, 0).y; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _bottomMargin: { + if (!isVertical) + return 0; + root.y; + root.height; + if (!root.parent || !blurBarWindow) + return 0; + const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + + property int _desktopEntriesUpdateTrigger: 0 + readonly property var sortedToplevels: { + return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + } + + readonly property string effectiveScreenName: { + if (!SettingsData.workspaceFollowFocus) + return root.screenName; + + switch (CompositorService.compositor) { + case "niri": + return NiriService.currentOutput || root.screenName; + case "hyprland": + return Hyprland.focusedWorkspace?.monitor?.name || root.screenName; + case "dwl": + return DwlService.activeOutput || root.screenName; + case "sway": + case "scroll": + case "miracle": + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + return focusedWs?.monitor?.name || root.screenName; + default: + return root.screenName; + } + } + + readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable) + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + _desktopEntriesUpdateTrigger++; + } + } + + property var currentWorkspace: { + if (useExtWorkspace) + return getExtWorkspaceActiveWorkspace(); + + switch (CompositorService.compositor) { + case "niri": + return getNiriActiveWorkspace(); + case "hyprland": + return getHyprlandActiveWorkspace(); + case "dwl": + const activeTags = getDwlActiveTags(); + return activeTags.length > 0 ? activeTags[0] : -1; + case "sway": + case "scroll": + case "miracle": + return getSwayActiveWorkspace(); + default: + return 1; + } + } + property var dwlActiveTags: { + if (CompositorService.isDwl) { + return getDwlActiveTags(); + } + return []; + } + property var workspaceList: { + if (useExtWorkspace) { + const baseList = getExtWorkspaceWorkspaces(); + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; + } + + let baseList; + switch (CompositorService.compositor) { + case "niri": + baseList = getNiriWorkspaces(); + break; + case "hyprland": + baseList = getHyprlandWorkspaces(); + break; + case "dwl": + baseList = getDwlTags(); + break; + case "sway": + case "scroll": + case "miracle": + baseList = getSwayWorkspaces(); + break; + default: + return [1]; + } + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; + } + + function getSwayWorkspaces() { + const workspaces = I3.workspaces?.values || []; + if (workspaces.length === 0) + return [ + { + "num": 1 + } + ]; + + function mapWorkspace(ws) { + return { + "num": ws.number, + "name": ws.name, + "focused": ws.focused, + "active": ws.active, + "urgent": ws.urgent, + "monitor": ws.monitor + }; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace); + } + + const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName); + return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [ + { + "num": 1 + } + ]; + } + + function getSwayActiveWorkspace() { + if (!root.screenName || SettingsData.workspaceFollowFocus) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + + const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + + function getHyprlandWorkspaces() { + const workspaces = Hyprland.workspaces?.values || []; + if (workspaces.length === 0) { + return [ + { + id: 1, + name: "1" + } + ]; + } + + let filtered = workspaces.filter(ws => ws.id > -1); + if (filtered.length === 0) { + return [ + { + id: 1, + name: "1" + } + ]; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + filtered = filtered.slice().sort((a, b) => a.id - b.id); + } else { + const monitorWorkspaces = filtered.filter(ws => ws.monitor?.name === root.screenName); + filtered = monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [ + { + id: 1, + name: "1" + } + ]; + } + + if (!SettingsData.showOccupiedWorkspacesOnly) { + return filtered; + } + + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const activeWsId = root.currentWorkspace; + return filtered.filter(ws => { + if (ws.id === activeWsId) + return true; + return hyprlandToplevels.some(tl => tl.workspace?.id === ws.id); + }); + } + + function getHyprlandActiveWorkspace() { + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return Hyprland.focusedWorkspace?.id || 1; + } + + const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName); + return monitor?.activeWorkspace?.id || 1; + } + + function getWorkspaceIcons(ws) { + _desktopEntriesUpdateTrigger; + if (!SettingsData.showWorkspaceApps || !ws) { + return []; + } + + let targetWorkspaceId; + if (CompositorService.isNiri) { + if (!ws || typeof ws !== "object") { + const wsNumber = typeof ws === "number" ? ws : -1; + if (wsNumber <= 0) { + return []; + } + const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName); + if (!workspace) { + return []; + } + targetWorkspaceId = workspace.id; + } else { + if (ws.id === undefined || ws.id === -1 || ws.idx === -1) { + return []; + } + targetWorkspaceId = ws.id; + } + } else if (CompositorService.isHyprland) { + targetWorkspaceId = ws.id !== undefined ? ws.id : ws; + } else if (CompositorService.isDwl) { + if (typeof ws !== "object" || ws.tag === undefined) { + return []; + } + targetWorkspaceId = ws.tag; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + targetWorkspaceId = ws.num !== undefined ? ws.num : ws; + } else { + return []; + } + + const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels; + + const byApp = {}; + let isActiveWs = false; + if (CompositorService.isNiri) { + isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active); + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; + } else if (CompositorService.isDwl) { + const output = DwlService.getOutputState(root.effectiveScreenName); + if (output && output.tags) { + const tag = output.tags.find(t => t.tag === targetWorkspaceId); + isActiveWs = tag ? (tag.state === 1) : false; + } + } else { + isActiveWs = targetWorkspaceId === root.currentWorkspace; + } + + wins.forEach((w, i) => { + if (!w) { + return; + } + + let winWs = null; + if (CompositorService.isNiri) { + winWs = w.workspace_id; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + winWs = w.workspace?.num; + } else { + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w); + winWs = hyprToplevel?.workspace?.id; + } + + if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) { + return; + } + + const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown"); + const moddedId = Paths.moddedAppId(keyBase); + const key = isActiveWs || !SettingsData.groupWorkspaceApps ? `${moddedId}_${i}` : moddedId; + + if (!byApp[key]) { + const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms"; + const isSteamApp = Paths.isSteamApp(moddedId); + const desktopEntry = DesktopEntries.heuristicLookup(moddedId); + const icon = Paths.getAppIcon(moddedId, desktopEntry); + const appName = Paths.getAppName(moddedId, desktopEntry); + byApp[key] = { + "type": "icon", + "icon": icon, + "isQuickshell": isQuickshell, + "isSteamApp": isSteamApp, + "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)), + "count": 1, + "windowId": w.address || w.id, + "fallbackText": appName || "" + }; + } else { + byApp[key].count++; + if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) { + byApp[key].active = true; + } + } + }); + + return Object.values(byApp); + } + + function padWorkspaces(list) { + const padded = list.slice(); + let placeholder; + if (useExtWorkspace) { + placeholder = { + "id": "", + "name": "", + "active": false, + "hidden": true + }; + } else if (CompositorService.isNiri) { + placeholder = { + "id": -1, + "idx": -1, + "name": "" + }; + } else if (CompositorService.isHyprland) { + placeholder = { + "id": -1, + "name": "" + }; + } else if (CompositorService.isDwl) { + placeholder = { + "tag": -1 + }; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + placeholder = { + "num": -1 + }; + } else { + placeholder = -1; + } + while (padded.length < 3) { + padded.push(placeholder); + } + return padded; + } + + function getNiriWorkspaces() { + if (NiriService.allWorkspaces.length === 0) { + return [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + } + + const fallbackWorkspaces = [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + + let workspaces; + if (!root.screenName || SettingsData.workspaceFollowFocus) { + const currentWorkspaces = NiriService.getCurrentOutputWorkspaces(); + workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces; + } else { + const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName); + workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces; + } + + workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx); + + if (!SettingsData.showOccupiedWorkspacesOnly) { + return workspaces; + } + + return workspaces.filter(ws => { + if (ws.is_active) + return true; + return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false; + }); + } + + function getNiriActiveWorkspace() { + if (NiriService.allWorkspaces.length === 0) { + return 1; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return NiriService.getCurrentWorkspaceNumber(); + } + + const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active); + return activeWs ? activeWs.idx : 1; + } + + function getDwlTags() { + if (!DwlService.dwlAvailable) + return []; + + const targetScreen = root.effectiveScreenName; + const output = DwlService.getOutputState(targetScreen); + if (!output || !output.tags || output.tags.length === 0) + return []; + + if (SettingsData.dwlShowAllTags) { + return output.tags.map(tag => ({ + "tag": tag.tag, + "state": tag.state, + "clients": tag.clients, + "focused": tag.focused + })); + } + + const visibleTagIndices = DwlService.getVisibleTags(targetScreen); + return visibleTagIndices.map(tagIndex => { + const tagData = output.tags.find(t => t.tag === tagIndex); + return { + "tag": tagIndex, + "state": tagData?.state ?? 0, + "clients": tagData?.clients ?? 0, + "focused": tagData?.focused ?? false + }; + }); + } + + function getDwlActiveTags() { + if (!DwlService.dwlAvailable) + return []; + + return DwlService.getActiveTags(root.effectiveScreenName); + } + + function getExtWorkspaceWorkspaces() { + const groups = ExtWorkspaceService.groups; + if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) { + return [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName)); + if (!group || !group.workspaces) { + return [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + let visible = group.workspaces.filter(ws => !ws.hidden); + + const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0); + if (hasValidCoordinates) { + visible = visible.sort((a, b) => { + const coordsA = a.coordinates || [0, 0]; + const coordsB = b.coordinates || [0, 0]; + if (coordsA[0] !== coordsB[0]) + return coordsA[0] - coordsB[0]; + return coordsA[1] - coordsB[1]; + }); + } + + visible = visible.map(ws => ({ + id: ws.id, + name: ws.name, + coordinates: ws.coordinates, + state: ws.state, + active: ws.active, + urgent: ws.urgent, + hidden: ws.hidden, + groupID: group.id + })); + + return visible.length > 0 ? visible : [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + function getExtWorkspaceActiveWorkspace() { + if (!ExtWorkspaceService.extWorkspaceAvailable) { + return 1; + } + + const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName); + return activeWs ? (activeWs.id || activeWs.name || "1") : "1"; + } + + readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1 + readonly property real padding: (root.barConfig?.removeWidgetPadding ?? false) ? 0 : Theme.snap((root.barConfig?.widgetPadding ?? 12) * (widgetHeight / 30), dpr) + readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2) + readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight + readonly property real appIconSize: Theme.barIconSize(barThickness, -6 + SettingsData.workspaceAppIconSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + + function getRealWorkspaces() { + return root.workspaceList.filter(ws => { + if (useExtWorkspace) + return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden; + if (CompositorService.isNiri) + return ws && ws.idx !== -1; + if (CompositorService.isHyprland) + return ws && ws.id !== -1; + if (CompositorService.isDwl) + return ws && ws.tag !== -1; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return ws && ws.num !== -1; + return ws !== -1; + }); + } + + function switchToWorkspaceByModelData(data) { + if (!data) + return; + + if (root.useExtWorkspace && (data.id || data.name)) { + ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || ""); + return; + } + + switch (CompositorService.compositor) { + case "niri": + if (data.idx !== undefined) + NiriService.switchToWorkspace(data.idx); + break; + case "hyprland": + if (data.id) + Hyprland.dispatch(`workspace ${data.id}`); + break; + case "dwl": + if (data.tag !== undefined) + DwlService.switchToTag(root.screenName, data.tag); + break; + case "sway": + case "scroll": + case "miracle": + if (data.num) + try { + I3.dispatch(`workspace number ${data.num}`); + } catch (_) {} + break; + } + } + + function findClosestWorkspaceIndex(localX, localY) { + if (workspaceRepeater.count === 0) + return -1; + + let closestIdx = -1; + let closestDist = Infinity; + + for (let i = 0; i < workspaceRepeater.count; i++) { + const item = workspaceRepeater.itemAt(i); + if (!item) + continue; + const center = item.mapToItem(root, item.width / 2, item.height / 2); + const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x); + if (dist < closestDist) { + closestDist = dist; + closestIdx = i; + } + } + return closestIdx; + } + + function switchWorkspace(direction) { + if (useExtWorkspace) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + const nextWorkspace = realWorkspaces[nextIndex]; + ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || ""); + } else if (CompositorService.isNiri) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + const nextWorkspace = realWorkspaces[nextIndex]; + if (!nextWorkspace || nextWorkspace.idx === undefined) { + return; + } + NiriService.switchToWorkspace(nextWorkspace.idx); + } else if (CompositorService.isHyprland) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`); + } else if (CompositorService.isDwl) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + try { + I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`); + } catch (_) {} + } + } + + function getWorkspaceIndexFallback(modelData, index) { + if (root.useExtWorkspace) + return index + 1; + if (CompositorService.isNiri) + return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : ""; + if (CompositorService.isHyprland) + return modelData?.id || ""; + if (CompositorService.isDwl) + return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return modelData?.num || ""; + return modelData - 1; + } + + function getWorkspaceIndex(modelData, index) { + let isPlaceholder; + if (root.useExtWorkspace) { + isPlaceholder = modelData?.hidden === true; + } else if (CompositorService.isNiri) { + isPlaceholder = modelData?.idx === -1; + } else if (CompositorService.isHyprland) { + isPlaceholder = modelData?.id === -1; + } else if (CompositorService.isDwl) { + isPlaceholder = modelData?.tag === -1; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + isPlaceholder = modelData?.num === -1; + } else { + isPlaceholder = modelData === -1; + } + + if (isPlaceholder) + return index + 1; + + let workspaceName = ""; + if (SettingsData.showWorkspaceName) { + workspaceName = modelData?.name ?? ""; + + if (workspaceName && workspaceName !== "") { + if (root.isVertical) { + workspaceName = workspaceName.charAt(0); + } + } else { + workspaceName = ""; + } + } + + if (workspaceName) { + if (SettingsData.showWorkspaceIndex) { + const indexLabel = getWorkspaceIndexFallback(modelData, index); + return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName; + } + return workspaceName; + } + + return getWorkspaceIndexFallback(modelData, index); + } + + readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle + readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 + readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) + + width: shouldShow ? (isVertical ? barThickness : visualWidth) : 0 + height: shouldShow ? (isVertical ? visualHeight : barThickness) : 0 + visible: shouldShow + + Item { + id: visualBackground + width: root.visualWidth + height: root.visualHeight + anchors.centerIn: parent + + Rectangle { + id: outline + anchors.centerIn: parent + width: { + const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return parent.width + borderWidth * 2; + } + height: { + const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return parent.height + borderWidth * 2; + } + radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius + color: "transparent" + border.width: { + if (barConfig?.widgetOutlineEnabled ?? false) { + return barConfig?.widgetOutlineThickness ?? 1; + } + return 0; + } + border.color: { + if (!(barConfig?.widgetOutlineEnabled ?? false)) { + return "transparent"; + } + const colorOption = barConfig?.widgetOutlineColor || "primary"; + const opacity = barConfig?.widgetOutlineOpacity ?? 1.0; + switch (colorOption) { + case "surfaceText": + return Theme.withAlpha(Theme.surfaceText, opacity); + case "secondary": + return Theme.withAlpha(Theme.secondary, opacity); + case "primary": + return Theme.withAlpha(Theme.primary, opacity); + default: + return Theme.withAlpha(Theme.primary, opacity); + } + } + } + + Rectangle { + id: background + anchors.fill: parent + radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius + color: { + if ((barConfig?.noBackground ?? false)) + return "transparent"; + const baseColor = Theme.widgetBaseBackgroundColor; + const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0; + if (Theme.widgetBackgroundHasAlpha) { + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency); + } + return Theme.withAlpha(baseColor, transparency); + } + } + } + + MouseArea { + id: edgeMouseArea + z: -1 + x: -root._leftMargin + y: -root._topMargin + width: root.width + root._leftMargin + root._rightMargin + height: root.height + root._topMargin + root._bottomMargin + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + property real touchpadAccumulator: 0 + property real mouseAccumulator: 0 + property bool scrollInProgress: false + + Timer { + id: scrollCooldown + interval: 100 + onTriggered: parent.scrollInProgress = false + } + + onClicked: mouse => { + const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y); + switch (mouse.button) { + case Qt.RightButton: + if (CompositorService.isNiri) { + NiriService.toggleOverview(); + } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; + } + break; + case Qt.LeftButton: + const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y); + if (idx >= 0) + root.switchToWorkspaceByModelData(root.workspaceList[idx]); + break; + } + } + + onWheel: wheel => { + if (Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y)) { + wheel.accepted = false; + return; + } + + if (scrollInProgress) + return; + + const delta = wheel.angleDelta.y; + const isTouchpad = wheel.pixelDelta && wheel.pixelDelta.y !== 0; + const reverse = SettingsData.reverseScrolling ? -1 : 1; + + if (isTouchpad) { + touchpadAccumulator += delta; + if (Math.abs(touchpadAccumulator) < 500) + return; + const direction = touchpadAccumulator * reverse < 0 ? 1 : -1; + root.switchWorkspace(direction); + scrollInProgress = true; + scrollCooldown.restart(); + touchpadAccumulator = 0; + return; + } + + mouseAccumulator += delta; + if (Math.abs(mouseAccumulator) < 120) + return; + const direction = mouseAccumulator * reverse < 0 ? 1 : -1; + root.switchWorkspace(direction); + scrollInProgress = true; + scrollCooldown.restart(); + mouseAccumulator = 0; + } + } + + property int dragSourceIndex: -1 + property int dragTargetIndex: -1 + property bool suppressShiftAnimation: false + + onWorkspaceListChanged: { + if (dragSourceIndex >= 0) { + dragSourceIndex = -1; + dragTargetIndex = -1; + suppressShiftAnimation = false; + } + } + + Flow { + id: workspaceRow + + x: isVertical ? visualBackground.x : (parent.width - implicitWidth) / 2 + y: isVertical ? (parent.height - implicitHeight) / 2 : visualBackground.y + spacing: Theme.spacingS + flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + id: workspaceRepeater + model: ScriptModel { + values: root.workspaceList + } + + Item { + id: delegateRoot + + property bool isDropTarget: root.dragTargetIndex === index + + z: dragHandler.dragging ? 1000 : 1 + + property real shiftOffset: { + if (root.dragSourceIndex < 0 || index === root.dragSourceIndex) + return 0; + const dragIdx = root.dragSourceIndex; + const dropIdx = root.dragTargetIndex; + if (dropIdx < 0) + return 0; + const shiftAmount = delegateRoot.width + Theme.spacingS; + if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx) + return -shiftAmount; + if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx) + return shiftAmount; + return 0; + } + + transform: Translate { + x: root.isVertical ? 0 : delegateRoot.shiftOffset + y: root.isVertical ? delegateRoot.shiftOffset : 0 + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + Behavior on y { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + property bool isActive: { + if (root.useExtWorkspace) + return (modelData?.id || modelData?.name) === root.currentWorkspace; + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === root.currentWorkspace); + if (CompositorService.isHyprland) + return !!(modelData && modelData.id === root.currentWorkspace); + if (CompositorService.isDwl) + return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return !!(modelData && modelData.num === root.currentWorkspace); + return modelData === root.currentWorkspace; + } + property bool isOccupied: { + if (CompositorService.isHyprland) + return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id); + if (CompositorService.isDwl) + return modelData.clients > 0; + if (CompositorService.isNiri) { + const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName); + return workspace ? (NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false) : false; + } + return false; + } + property bool isPlaceholder: { + if (root.useExtWorkspace) + return !!(modelData && modelData.hidden); + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === -1); + if (CompositorService.isHyprland) + return !!(modelData && modelData.id === -1); + if (CompositorService.isDwl) + return !!(modelData && modelData.tag === -1); + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return !!(modelData && modelData.num === -1); + return modelData === -1; + } + property bool isHovered: mouseArea.containsMouse + + property var loadedWorkspaceData: null + property bool loadedIsUrgent: false + property bool isUrgent: { + if (root.useExtWorkspace) + return modelData?.urgent ?? false; + if (CompositorService.isHyprland) + return modelData?.urgent ?? false; + if (CompositorService.isNiri) + return loadedIsUrgent; + if (CompositorService.isDwl) + return modelData?.state === 2; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return loadedIsUrgent; + return false; + } + readonly property var loadedIconData: { + if (isPlaceholder) + return null; + const name = modelData?.name; + if (!name) + return null; + return SettingsData.getWorkspaceNameIcon(name); + } + readonly property bool loadedHasIcon: loadedIconData !== null + property var loadedIcons: [] + + readonly property int stableIconCount: { + if (!SettingsData.showWorkspaceApps || isPlaceholder) + return 0; + + let targetWorkspaceId; + if (root.useExtWorkspace) { + targetWorkspaceId = modelData?.id || modelData?.name; + } else if (CompositorService.isNiri) { + targetWorkspaceId = modelData?.id; + } else if (CompositorService.isHyprland) { + targetWorkspaceId = modelData?.id; + } else if (CompositorService.isDwl) { + targetWorkspaceId = modelData?.tag; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + targetWorkspaceId = modelData?.num; + } + if (targetWorkspaceId === undefined || targetWorkspaceId === null) + return 0; + + const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels; + const seen = {}; + let groupedCount = 0; + let totalCount = 0; + + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + if (!w) + continue; + + let winWs = null; + if (CompositorService.isNiri) { + winWs = w.workspace_id; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + winWs = w.workspace?.num; + } else if (CompositorService.isHyprland) { + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w); + winWs = hyprToplevel?.workspace?.id; + } + + if (winWs !== targetWorkspaceId) + continue; + totalCount++; + + const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown"; + if (!seen[appKey]) { + seen[appKey] = true; + groupedCount++; + } + } + + return (SettingsData.groupWorkspaceApps && !isActive) ? groupedCount : totalCount; + } + + readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) + readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) + readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" + readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0 + readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0 + + readonly property real iconsExtraWidth: { + if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) { + const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons); + return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); + } + return 0; + } + readonly property real iconsExtraHeight: { + if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) { + const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons); + return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); + } + return 0; + } + + readonly property real visualWidth: { + if (contentImplicitWidth <= 0) + return baseWidth + iconsExtraWidth; + const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS; + return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding); + } + readonly property real visualHeight: { + if (contentImplicitHeight <= 0) + return baseHeight + iconsExtraHeight; + const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS; + return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding); + } + + readonly property color unfocusedColor: { + switch (SettingsData.workspaceUnfocusedColorMode) { + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + default: + return Theme.surfaceTextAlpha; + } + } + + readonly property color activeColor: { + switch (SettingsData.workspaceColorMode) { + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + case "none": + return unfocusedColor; + default: + return Theme.primary; + } + } + + readonly property color occupiedColor: { + switch (SettingsData.workspaceOccupiedColorMode) { + case "sec": + return Theme.secondary; + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + case "schh": + return Theme.surfaceContainerHighest; + default: + return unfocusedColor; + } + } + + readonly property color urgentColor: { + switch (SettingsData.workspaceUrgentColorMode) { + case "primary": + return Theme.primary; + case "secondary": + return Theme.secondary; + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + default: + return Theme.error; + } + } + + readonly property color focusedBorderColor: { + switch (SettingsData.workspaceFocusedBorderColor) { + case "surfaceText": + return Theme.surfaceText; + case "secondary": + return Theme.secondary; + default: + return Theme.primary; + } + } + + function getContrastingIconColor(bgColor) { + const luminance = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b; + return luminance > 0.4 ? Qt.rgba(0.15, 0.15, 0.15, 1) : Qt.rgba(0.8, 0.8, 0.8, 1); + } + + readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor) + readonly property color quickshellIconInactiveColor: getContrastingIconColor(unfocusedColor) + + Item { + id: dragHandler + anchors.fill: parent + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property real dragAxisOffset: 0 + + Connections { + target: root + function onWorkspaceListChanged() { + if (dragHandler.dragging) { + dragHandler.dragging = false; + dragHandler.dragAxisOffset = 0; + mouseArea.mousePressed = false; + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: !isPlaceholder + cursorShape: isPlaceholder ? Qt.ArrowCursor : (dragHandler.dragging ? Qt.ClosedHandCursor : Qt.PointingHandCursor) + enabled: !isPlaceholder + acceptedButtons: Qt.LeftButton | Qt.RightButton + + property bool mousePressed: false + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && CompositorService.isNiri && SettingsData.workspaceDragReorder && !isPlaceholder) { + mousePressed = true; + dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y); + } + } + + onPositionChanged: mouse => { + if (!mousePressed || !CompositorService.isNiri || !SettingsData.workspaceDragReorder || isPlaceholder) + return; + + if (!dragHandler.dragging) { + const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x); + if (distance > 5) { + dragHandler.dragging = true; + root.dragSourceIndex = index; + root.dragTargetIndex = index; + } + } + + if (!dragHandler.dragging) + return; + + const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x); + + const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS; + const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize; + const maxOffsetNegative = -index * itemSize; + const axisOffset = Math.max(maxOffsetNegative, Math.min(maxOffsetPositive, rawAxisOffset)); + dragHandler.dragAxisOffset = axisOffset; + + const slotOffset = Math.round(axisOffset / itemSize); + const newTargetIndex = Math.max(0, Math.min(root.workspaceList.length - 1, index + slotOffset)); + + if (newTargetIndex !== root.dragTargetIndex) { + root.dragTargetIndex = newTargetIndex; + } + } + + onReleased: mouse => { + const wasDragging = dragHandler.dragging; + const didReorder = wasDragging && root.dragTargetIndex >= 0 && root.dragTargetIndex !== root.dragSourceIndex; + + if (didReorder) { + const sourceWs = root.workspaceList[root.dragSourceIndex]; + const targetWs = root.workspaceList[root.dragTargetIndex]; + + if (sourceWs && targetWs && sourceWs.idx !== undefined && targetWs.idx !== undefined) { + root.suppressShiftAnimation = true; + NiriService.moveWorkspaceToIndex(sourceWs.idx, targetWs.idx); + Qt.callLater(() => root.suppressShiftAnimation = false); + } + } + + mousePressed = false; + dragHandler.dragging = false; + dragHandler.dragAxisOffset = 0; + root.dragSourceIndex = -1; + root.dragTargetIndex = -1; + + if (wasDragging || isPlaceholder) + return; + + if (mouse.button === Qt.LeftButton) { + if (root.useExtWorkspace && (modelData?.id || modelData?.name)) { + ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || ""); + } else if (CompositorService.isNiri) { + if (modelData && modelData.idx !== undefined) { + NiriService.switchToWorkspace(modelData.idx); + } + } else if (CompositorService.isHyprland && modelData?.id) { + Hyprland.dispatch(`workspace ${modelData.id}`); + } else if (CompositorService.isDwl && modelData?.tag !== undefined) { + DwlService.switchToTag(root.screenName, modelData.tag); + } else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) { + try { + I3.dispatch(`workspace number ${modelData.num}`); + } catch (_) {} + } + } else if (mouse.button === Qt.RightButton) { + if (CompositorService.isNiri) { + NiriService.toggleOverview(); + } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; + } else if (CompositorService.isDwl && modelData?.tag !== undefined) { + DwlService.toggleTag(root.screenName, modelData.tag); + } + } + } + } + + Timer { + id: dataUpdateTimer + interval: 50 + onTriggered: { + if (isPlaceholder) { + delegateRoot.loadedWorkspaceData = null; + delegateRoot.loadedIcons = []; + delegateRoot.loadedIsUrgent = false; + return; + } + + var wsData = null; + if (root.useExtWorkspace) { + wsData = modelData; + } else if (CompositorService.isNiri) { + wsData = modelData || null; + } else if (CompositorService.isHyprland) { + wsData = modelData; + } else if (CompositorService.isDwl) { + wsData = modelData; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + wsData = modelData; + } + delegateRoot.loadedWorkspaceData = wsData; + if (CompositorService.isNiri) { + const workspaceId = wsData?.id; + delegateRoot.loadedIsUrgent = workspaceId ? NiriService.windows.some(w => w.workspace_id === workspaceId && w.is_urgent) : false; + } else { + delegateRoot.loadedIsUrgent = wsData?.urgent ?? false; + } + + if (SettingsData.showWorkspaceApps) { + if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); + } else if (CompositorService.isNiri) { + delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); + } else { + delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); + } + } else { + delegateRoot.loadedIcons = []; + } + } + } + + function updateAllData() { + dataUpdateTimer.restart(); + } + + width: root.isVertical ? root.widgetHeight : visualWidth + height: root.isVertical ? visualHeight : root.widgetHeight + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + id: focusedBorderRing + x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2 + y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2 + width: { + const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0; + return delegateRoot.visualWidth + borderWidth * 2; + } + height: { + const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0; + return delegateRoot.visualHeight + borderWidth * 2; + } + radius: Theme.cornerRadius + color: "transparent" + border.width: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0 + border.color: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? focusedBorderColor : "transparent" + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + Rectangle { + id: visualContent + width: delegateRoot.visualWidth + height: delegateRoot.visualHeight + x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2 + y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2 + radius: Theme.cornerRadius + color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor + opacity: dragHandler.dragging ? 0.8 : 1.0 + + border.width: dragHandler.dragging ? 2 : (isUrgent ? 2 : (isDropTarget ? 2 : 0)) + border.color: dragHandler.dragging ? Theme.primary : (isUrgent ? urgentColor : (isDropTarget ? Theme.primary : "transparent")) + + transform: Translate { + x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) + y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Loader { + id: appIconsLoader + anchors.fill: parent + active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + sourceComponent: Item { + id: contentRoot + readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0 + readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0 + + Loader { + id: contentRow + anchors.centerIn: parent + sourceComponent: root.isVertical ? columnLayout : rowLayout + } + + Component { + id: rowLayout + Row { + spacing: 4 + visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + + Item { + visible: loadedHasIcon && loadedIconData?.type === "icon" + width: wsIcon.width + height: root.appIconSize + + DankIcon { + id: wsIcon + anchors.verticalCenter: parent.verticalCenter + name: loadedIconData?.value ?? "" + size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + weight: (isActive && !isPlaceholder) ? 500 : 400 + } + } + + Item { + visible: loadedHasIcon && loadedIconData?.type === "text" + width: wsText.implicitWidth + height: root.appIconSize + + StyledText { + id: wsText + anchors.verticalCenter: parent.verticalCenter + text: loadedIconData?.value ?? "" + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + } + + Item { + visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName) + width: wsIndexText.implicitWidth + height: root.appIconSize + + StyledText { + id: wsIndexText + anchors.verticalCenter: parent.verticalCenter + text: loadedHasIcon ? (modelData?.name ?? "") : root.getWorkspaceIndex(modelData, index) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + } + + Repeater { + model: ScriptModel { + values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons) + } + delegate: Item { + width: root.appIconSize + height: root.appIconSize + readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active + readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected + readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary + readonly property real appOpacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + + IconImage { + id: rowAppIcon + anchors.fill: parent + source: modelData.icon || "" + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && rowAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: appGlyphColor + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: appGlyphColor + } + } + + IconImage { + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isQuickshell + layer.enabled: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) + } + } + + IconImage { + id: rowSteamIcon + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && modelData.icon + } + + DankIcon { + anchors.centerIn: parent + size: root.appIconSize + name: "sports_esports" + color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && !modelData.icon + } + + Rectangle { + anchors.fill: parent + visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive + color: "transparent" + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: focusedBorderColor + z: 1 + } + + MouseArea { + id: rowAppMouseArea + anchors.fill: parent + enabled: isActive + cursorShape: Qt.PointingHandCursor + onClicked: { + const winId = modelData.windowId; + if (!winId) + return; + if (CompositorService.isHyprland) { + Hyprland.dispatch(`focuswindow address:${winId}`); + } else if (CompositorService.isNiri) { + NiriService.focusWindow(winId); + } + } + } + + Rectangle { + visible: modelData.count > 1 && !isActive + width: root.appIconSize * 0.67 + height: root.appIconSize * 0.67 + radius: root.appIconSize * 0.33 + color: "black" + border.color: "white" + border.width: 1 + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 2 + + Text { + anchors.centerIn: parent + text: modelData.count + font.pixelSize: root.appIconSize * 0.44 + color: "white" + } + } + } + } + } + } + + Component { + id: columnLayout + Column { + spacing: 4 + visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + + DankIcon { + visible: loadedHasIcon && loadedIconData?.type === "icon" + anchors.horizontalCenter: parent.horizontalCenter + name: loadedIconData?.value ?? "" + size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + weight: (isActive && !isPlaceholder) ? 500 : 400 + } + + StyledText { + visible: loadedHasIcon && loadedIconData?.type === "text" + anchors.horizontalCenter: parent.horizontalCenter + text: loadedIconData?.value ?? "" + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + StyledText { + visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName) + anchors.horizontalCenter: parent.horizontalCenter + text: loadedHasIcon ? (root.isVertical ? (modelData?.name ?? "").charAt(0) : (modelData?.name ?? "")) : root.getWorkspaceIndex(modelData, index) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + Repeater { + model: ScriptModel { + values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons) + } + delegate: Item { + width: root.appIconSize + height: root.appIconSize + readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active + readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected + readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary + readonly property real appOpacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + + IconImage { + id: colAppIcon + anchors.fill: parent + source: modelData.icon || "" + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && colAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: appGlyphColor + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: appGlyphColor + } + } + + IconImage { + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isQuickshell + layer.enabled: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) + } + } + + IconImage { + id: colSteamIcon + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && modelData.icon + } + + DankIcon { + anchors.centerIn: parent + size: root.appIconSize + name: "sports_esports" + color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && !modelData.icon + } + + Rectangle { + anchors.fill: parent + visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive + color: "transparent" + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: focusedBorderColor + z: 1 + } + + MouseArea { + id: colAppMouseArea + anchors.fill: parent + enabled: isActive + cursorShape: Qt.PointingHandCursor + onClicked: { + const winId = modelData.windowId; + if (!winId) + return; + if (CompositorService.isHyprland) { + Hyprland.dispatch(`focuswindow address:${winId}`); + } else if (CompositorService.isNiri) { + NiriService.focusWindow(winId); + } + } + } + + Rectangle { + visible: modelData.count > 1 && !isActive + width: root.appIconSize * 0.67 + height: root.appIconSize * 0.67 + radius: root.appIconSize * 0.33 + color: "black" + border.color: "white" + border.width: 1 + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 2 + + Text { + anchors.centerIn: parent + text: modelData.count + font.pixelSize: root.appIconSize * 0.44 + color: "white" + } + } + } + } + } + } + } + } + } + + Component.onCompleted: updateAllData() + + Connections { + target: CompositorService + function onSortedToplevelsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: NiriService + enabled: CompositorService.isNiri + function onAllWorkspacesChanged() { + delegateRoot.updateAllData(); + } + function onWindowUrgentChanged() { + delegateRoot.updateAllData(); + } + function onWindowsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: SettingsData + function onShowWorkspaceAppsChanged() { + delegateRoot.updateAllData(); + } + function onWorkspaceNameIconsChanged() { + delegateRoot.updateAllData(); + } + function onAppIdSubstitutionsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: DwlService + enabled: CompositorService.isDwl + function onStateChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: Hyprland.workspaces + enabled: CompositorService.isHyprland + function onValuesChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: I3.workspaces + enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + function onValuesChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: ExtWorkspaceService + enabled: root.useExtWorkspace + function onStateChanged() { + delegateRoot.updateAllData(); + } + } + } + } + } + + Component.onCompleted: { + if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) { + DMSService.addSubscription("extworkspace"); + } + _updateBlurRegistration(); + } + + property bool _blurRegistered: false + readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0 + + on_ShouldBlurChanged: _updateBlurRegistration() + + function _updateBlurRegistration() { + if (_shouldBlur && !_blurRegistered) { + blurBarWindow.registerBlurWidget(visualBackground); + _blurRegistered = true; + } else if (!_shouldBlur && _blurRegistered) { + if (blurBarWindow && blurBarWindow.unregisterBlurWidget) + blurBarWindow.unregisterBlurWidget(visualBackground); + _blurRegistered = false; + } + } + + Component.onDestruction: { + if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget) + blurBarWindow.unregisterBlurWidget(visualBackground); + } +} |