update quickshell conf

This commit is contained in:
Johannes Knopp
2026-04-14 18:13:39 +02:00
parent 9248f9b33f
commit 6efa5d599c
10 changed files with 663 additions and 475 deletions

View File

@ -17,68 +17,121 @@ PanelWindow {
color: Theme.bg color: Theme.bg
readonly property string screenName: modelData.name readonly property string screenName: modelData.name
property string activePopup: ""
// Layout — three sections anchored independently for true centering readonly property int bw: Theme.borderWidth // 2
readonly property int pad: Theme.enclosureMargin // 3
// ── Bar bottom border ─────────────────────────────────────────────
Rectangle {
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
height: root.bw
color: Theme.border
}
// ── Bar content ───────────────────────────────────────────────────
Item { Item {
anchors.fill: parent anchors {
anchors.leftMargin: 8 fill: parent
anchors.rightMargin: 8 leftMargin: 8
anchors.topMargin: 2 rightMargin: 8
anchors.bottomMargin: 2 topMargin: root.bw
bottomMargin: root.bw
// Left
Enclosure {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
child: Workspaces {
screenName: root.screenName
}
} }
// Center — truly centered regardless of left/right content width Workspaces {
Enclosure { anchors { left: parent.left; verticalCenter: parent.verticalCenter }
anchors.centerIn: parent screenName: root.screenName
child: MusicPlayer {
id: musicChip
onClicked: musicControls.visible = !musicControls.visible
}
} }
// Right — status chips MusicPlayer {
Enclosure { id: musicChip
anchors.right: parent.right anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
anchors.verticalCenter: parent.verticalCenter onClicked: root.activePopup = root.activePopup === "controls" ? "" : "controls"
child: Row { }
spacing: 8
SysTray { Row {
anchors.verticalCenter: parent.verticalCenter id: rightRow
barWindow: root anchors { right: parent.right; verticalCenter: parent.verticalCenter }
} spacing: 8
NetworkStatus { SysTray {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} barWindow: root
}
VolumeControl { NetworkStatus {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
VolumeControl {
Clock { anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter onClickedLeft: root.activePopup = root.activePopup === "mixer" ? "" : "mixer"
} }
Clock {
id: clockDisp
anchors.verticalCenter: parent.verticalCenter
onClicked: root.activePopup = root.activePopup === "calendar" ? "" : "calendar"
} }
} }
} }
// Popup declared after the layout so musicChip is already initialised. // ── Music controls popup ──────────────────────────────────────────
// Centered horizontally within the bar window, placed just below it. PopoutWindow {
MusicPlayerControls { popupName: "controls"
id: musicControls activePopup: root.activePopup
visible: false
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
anchor.rect.x: Math.round((root.width - implicitWidth) / 2)
implicitWidth: Math.max(musicChip.implicitWidth + 2 * root.pad,
ctrlContent.implicitWidth + 2 * root.bw)
implicitHeight: ctrlContent.implicitHeight + root.bw + Theme.radius
MusicPlayerControls {
id: ctrlContent
anchors { left: parent.left; right: parent.right }
player: musicChip.player
}
}
// ── Calendar popup ────────────────────────────────────────────────
PopoutWindow {
popupName: "calendar"
activePopup: root.activePopup
anchor.window: root anchor.window: root
anchor.rect.x: Math.round((root.width - musicControls.width) / 2) anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
anchor.rect.y: root.height readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
player: musicChip.player calContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: calContent.implicitHeight + root.bw + Theme.radius
CalendarContent {
id: calContent
anchors { left: parent.left; right: parent.right }
now: clockDisp.now
}
}
// ── Volume mixer popup ────────────────────────────────────────────
PopoutWindow {
popupName: "mixer"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
mixContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: mixContent.implicitHeight + root.bw + Theme.radius
VolumeMixerContent {
id: mixContent
anchors { left: parent.left; right: parent.right }
}
} }
} }

View File

@ -0,0 +1,139 @@
import QtQuick
import "../components"
// Calendar grid a plain Item so it can live inside an expanding section border.
Item {
id: root
property var now: new Date()
property int viewYear: now.getFullYear()
property int viewMonth: now.getMonth() // 0-based
readonly property var monthNames: [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate()
// First weekday of month: Mon=0 … Sun=6
readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7
readonly property int todayYear: now.getFullYear()
readonly property int todayMonth: now.getMonth()
readonly property int todayDay: now.getDate()
// All widths derived from this constant — never from parent.width —
// to avoid Column polish loops when the popup is wider than the grid.
readonly property int cellW: 28
readonly property int cellGap: 2
readonly property int gridW: 7 * cellW + 6 * cellGap // 208
implicitWidth: gridW + 24
implicitHeight: calLayout.implicitHeight + 16
Column {
id: calLayout
// Centre the fixed-width grid inside the (potentially wider) popup.
// No left+right anchors → implicitWidth stays at gridW, no resize loop.
anchors { top: parent.top; topMargin: 8; horizontalCenter: parent.horizontalCenter }
spacing: 6
// Month navigation — all child widths are explicit constants
Row {
spacing: 0
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 0) { root.viewMonth = 11; root.viewYear-- }
else root.viewMonth--
}
}
}
Text {
text: root.monthNames[root.viewMonth] + " " + root.viewYear
color: Theme.text
font.pixelSize: 13
font.bold: true
width: root.gridW - 40 // fixed constant, not parent.width
horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 11) { root.viewMonth = 0; root.viewYear++ }
else root.viewMonth++
}
}
}
}
// Weekday headers
Row {
spacing: root.cellGap
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
text: modelData
color: Theme.textDim
font.pixelSize: 10
width: root.cellW
horizontalAlignment: Text.AlignHCenter
}
}
}
// Day grid
Grid {
columns: 7
spacing: root.cellGap
Repeater {
model: root.totalCells
delegate: Item {
width: root.cellW
height: 22
readonly property int dayNum: index - root.startOffset + 1
readonly property bool valid:
index >= root.startOffset &&
index < root.startOffset + root.daysInMonth
readonly property bool isToday:
valid &&
root.viewYear === root.todayYear &&
root.viewMonth === root.todayMonth &&
dayNum === root.todayDay
Rectangle {
anchors.fill: parent
radius: 3
color: isToday ? Theme.accent : "transparent"
visible: isToday
}
Text {
anchors.centerIn: parent
text: parent.valid ? String(parent.dayNum) : ""
color: parent.isToday ? Theme.text : Theme.textDim
font.pixelSize: 11
font.bold: parent.isToday
}
}
}
}
}
}

View File

@ -1,10 +1,12 @@
import Quickshell
import QtQuick import QtQuick
import "../components" import "../components"
Item { Item {
id: root id: root
implicitWidth: timeLabel.implicitWidth + 10
signal clicked
implicitWidth: timeLabel.implicitWidth + 16
implicitHeight: 24 implicitHeight: 24
property var now: new Date() property var now: new Date()
@ -19,10 +21,13 @@ Item {
readonly property string display: { readonly property string display: {
const d = now const d = now
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
const h = String(d.getHours()).padStart(2, "0") const dd = String(d.getDate()).padStart(2, "0")
const m = String(d.getMinutes()).padStart(2, "0") const mm = String(d.getMonth() + 1).padStart(2, "0")
const s = String(d.getSeconds()).padStart(2, "0") const yyyy = d.getFullYear()
return days[d.getDay()] + " " + h + ":" + m + ":" + s const h = String(d.getHours()).padStart(2, "0")
const m = String(d.getMinutes()).padStart(2, "0")
const s = String(d.getSeconds()).padStart(2, "0")
return days[d.getDay()] + " " + dd + "." + mm + "." + yyyy + " " + h + ":" + m + ":" + s
} }
Text { Text {
@ -37,160 +42,6 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: calPopup.visible = !calPopup.visible onClicked: root.clicked()
}
// --------------- Calendar popup ---------------
PopupWindow {
id: calPopup
visible: false
anchor.item: root
anchor.edges: Edges.Bottom
anchor.gravity: Edges.Bottom
// calendar state
property int viewYear: root.now.getFullYear()
property int viewMonth: root.now.getMonth() // 0-based
readonly property var monthNames: [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate()
// first weekday of month: Mon=0 … Sun=6
readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7
readonly property int todayYear: root.now.getFullYear()
readonly property int todayMonth: root.now.getMonth()
readonly property int todayDay: root.now.getDate()
implicitWidth: calBg.implicitWidth
implicitHeight: calBg.implicitHeight
Rectangle {
id: calBg
anchors.fill: parent
color: Theme.bg
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitWidth: calLayout.implicitWidth + 24
implicitHeight: calLayout.implicitHeight + 24
Column {
id: calLayout
anchors { fill: parent; margins: 12 }
spacing: 6
// Month navigation
Row {
width: parent.width
spacing: 0
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (calPopup.viewMonth === 0) {
calPopup.viewMonth = 11
calPopup.viewYear--
} else {
calPopup.viewMonth--
}
}
}
}
Text {
text: calPopup.monthNames[calPopup.viewMonth] + " " + calPopup.viewYear
color: Theme.text
font.pixelSize: 13
font.bold: true
// fill remaining space between arrows
width: parent.width - 40
horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (calPopup.viewMonth === 11) {
calPopup.viewMonth = 0
calPopup.viewYear++
} else {
calPopup.viewMonth++
}
}
}
}
}
// Weekday headers
Row {
spacing: 2
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
text: modelData
color: Theme.textDim
font.pixelSize: 10
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
}
// Day grid
Grid {
columns: 7
spacing: 2
Repeater {
model: calPopup.totalCells
delegate: Item {
width: 28
height: 22
readonly property int dayNum: index - calPopup.startOffset + 1
readonly property bool valid:
index >= calPopup.startOffset &&
index < calPopup.startOffset + calPopup.daysInMonth
readonly property bool isToday:
valid &&
calPopup.viewYear === calPopup.todayYear &&
calPopup.viewMonth === calPopup.todayMonth &&
dayNum === calPopup.todayDay
Rectangle {
anchors.fill: parent
radius: 3
color: isToday ? Theme.accent : "transparent"
visible: isToday
}
Text {
anchors.centerIn: parent
text: parent.valid ? String(parent.dayNum) : ""
color: parent.isToday ? Theme.text : Theme.textDim
font.pixelSize: 11
font.bold: parent.isToday
}
}
}
}
}
}
} }
} }

View File

@ -1,4 +1,3 @@
import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import QtQuick import QtQuick
import "../components" import "../components"

View File

@ -1,138 +1,124 @@
import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import "../components" import "../components"
PopupWindow { // Plain Item lives inside the expanding center section border in Bar.qml.
Item {
id: root id: root
required property var player required property var player
implicitWidth: 280 implicitWidth: 280
implicitHeight: bg.implicitHeight implicitHeight: layout.implicitHeight + 28
// Tickle positionChanged every frame while playing so the player.position // Keep the progress binding live while playing, pause while seeking.
// binding re-evaluates. Paused while the user is dragging the seek bar.
FrameAnimation { FrameAnimation {
running: (root.player?.isPlaying ?? false) && !progressArea.pressed running: (root.player?.isPlaying ?? false) && !progressArea.pressed
onTriggered: root.player.positionChanged() onTriggered: root.player.positionChanged()
} }
Rectangle { ColumnLayout {
id: bg id: layout
anchors.fill: parent anchors { fill: parent; margins: 14 }
color: Theme.bg spacing: 8
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitHeight: layout.implicitHeight + 28
ColumnLayout { // Track info
id: layout Text {
anchors { fill: parent; margins: 14 } text: root.player?.trackTitle ?? ""
spacing: 8 color: Theme.text
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
// Track info Text {
Text { text: root.player?.trackArtist ?? ""
text: root.player?.trackTitle ?? "" color: Theme.textDim
color: Theme.text font.pixelSize: 11
font.pixelSize: 13 Layout.fillWidth: true
font.bold: true elide: Text.ElideRight
Layout.fillWidth: true visible: (root.player?.trackArtist ?? "") !== ""
elide: Text.ElideRight Layout.topMargin: -4
} }
Text { // Progress bar
text: root.player?.trackArtist ?? "" Item {
color: Theme.textDim visible: root.player?.positionSupported && root.player?.lengthSupported
font.pixelSize: 11 && (root.player?.length ?? 0) > 0
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight height: 12
visible: (root.player?.trackArtist ?? "") !== ""
Layout.topMargin: -4
}
// Progress bar Rectangle {
Item { anchors.verticalCenter: parent.verticalCenter
visible: root.player?.positionSupported && root.player?.lengthSupported width: parent.width
&& (root.player?.length ?? 0) > 0 height: 3
Layout.fillWidth: true radius: 2
height: 12 // visual track is centred inside; extra height = easier to hit color: Theme.progressTrack
// Track
Rectangle { Rectangle {
anchors.verticalCenter: parent.verticalCenter width: root.player?.length > 0
width: parent.width ? parent.width * (root.player.position / root.player.length)
height: 3 : 0
radius: 2 height: parent.height
color: Theme.progressTrack radius: parent.radius
color: Theme.accent
// Filled portion
Rectangle {
width: root.player?.length > 0
? parent.width * (root.player.position / root.player.length)
: 0
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
id: progressArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) {
if (!(root.player?.canSeek)) return
const ratio = Math.max(0, Math.min(1, mx / width))
root.player.position = ratio * (root.player?.length ?? 0)
}
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
} }
} }
// Media controls MouseArea {
RowLayout { id: progressArea
Layout.fillWidth: true anchors.fill: parent
spacing: 0 hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Item { Layout.fillWidth: true } function seek(mx) {
if (!(root.player?.canSeek)) return
ControlButton { const ratio = Math.max(0, Math.min(1, mx / width))
icon: "⏮" root.player.position = ratio * (root.player?.length ?? 0)
iconSize: 18
enabled: root.player?.canGoPrevious ?? false
onActivated: root.player.previous()
} }
Item { width: 8 } onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
ControlButton {
icon: root.player?.isPlaying ? "⏸" : "▶"
iconSize: 22
enabled: true
onActivated: root.player?.togglePlaying()
}
Item { width: 8 }
ControlButton {
icon: "⏭"
iconSize: 18
enabled: root.player?.canGoNext ?? false
onActivated: root.player.next()
}
Item { Layout.fillWidth: true }
} }
} }
// Media controls
RowLayout {
Layout.fillWidth: true
spacing: 0
Item { Layout.fillWidth: true }
ControlButton {
icon: "⏮"
iconSize: 18
enabled: root.player?.canGoPrevious ?? false
onActivated: root.player.previous()
}
Item { width: 8 }
ControlButton {
icon: root.player?.isPlaying ? "⏸" : "▶"
iconSize: 22
enabled: true
onActivated: root.player?.togglePlaying()
}
Item { width: 8 }
ControlButton {
icon: "⏭"
iconSize: 18
enabled: root.player?.canGoNext ?? false
onActivated: root.player.next()
}
Item { Layout.fillWidth: true }
}
} }
// Inline helper — a button that highlights on hover
component ControlButton: Rectangle { component ControlButton: Rectangle {
id: btn id: btn
required property string icon required property string icon
@ -144,7 +130,6 @@ PopupWindow {
implicitHeight: label.implicitHeight + 10 implicitHeight: label.implicitHeight + 10
radius: Theme.radius radius: Theme.radius
color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent' color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent'
Behavior on color { ColorAnimation { duration: 80 } } Behavior on color { ColorAnimation { duration: 80 } }
Text { Text {

View File

@ -0,0 +1,162 @@
import Quickshell
import QtQuick
import QtQuick.Shapes
import "../components"
PopupWindow {
id: self
property string popupName: ""
property string activePopup: ""
visible: false
color: "transparent"
default property alias content: contentSlot.data
readonly property int bw: Theme.borderWidth
readonly property real r: Theme.radius
readonly property bool _open: activePopup === popupName
on_OpenChanged: {
if (_open) {
hideTimer.stop();
self.visible = true;
fade.opacity = 1.0;
} else {
fade.opacity = 0.0;
hideTimer.start();
}
}
Timer {
id: hideTimer
interval: 200
onTriggered: self.visible = false
}
Item {
id: fade
anchors.fill: parent
opacity: 0.0
Behavior on opacity {
NumberAnimation {
duration: 180
easing.type: Easing.InOutCubic
}
}
// Background + border in one Shape so fill matches stroke exactly
Shape {
id: pbs
anchors.fill: parent
ShapePath {
strokeWidth: self.bw
strokeColor: Theme.border
fillColor: Theme.bg
// Start flush at top-left, hidden under bar
startX: 0
startY: self.r
// Left side down to bottom-left corner
PathLine {
x: 0
y: pbs.height - self.r
}
// Bottom-left corner — Counterclockwise for outward curve
PathArc {
x: self.r
y: pbs.height
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Bottom edge
PathLine {
x: pbs.width - self.r
y: pbs.height
}
// Bottom-right corner — Counterclockwise for outward curve
PathArc {
x: pbs.width
y: pbs.height - self.r
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Right side up, flush to top, hidden under bar
PathLine {
x: pbs.width
y: self.r
}
}
}
// // Top-left concave corner
// Shape {
// x: 0
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: 0
// startY: 0
// PathLine {
// x: self.r
// y: 0
// }
// PathArc {
// x: 0
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Clockwise
// }
// }
// }
//
// // Top-right concave corner
// Shape {
// x: parent.width - self.r
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: self.r
// startY: 0
// PathLine {
// x: 0
// y: 0
// }
// PathArc {
// x: self.r
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Counterclockwise
// }
// }
// }
Item {
id: contentSlot
anchors {
top: parent.top
topMargin: self.r + self.bw
left: parent.left
leftMargin: self.bw
right: parent.right
rightMargin: self.bw
}
}
}
}

View File

@ -1,4 +1,3 @@
import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@ -9,13 +8,12 @@ Item {
implicitWidth: chip.implicitWidth + 10 implicitWidth: chip.implicitWidth + 10
implicitHeight: 24 implicitHeight: 24
// Track all PipeWire nodes so their properties are populated. signal clickedLeft
// Without this, nodes in Pipewire.nodes are "unbound" and have null audio/type.
PwObjectTracker { PwObjectTracker {
objects: Pipewire.nodes.values objects: Pipewire.nodes.values
} }
// ── helpers ────────────────────────────────────────────────────────
function safeVolume(node) { function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0 if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume const v = node.audio.volume
@ -26,13 +24,12 @@ Item {
node.audio.volume = Math.max(0, Math.min(1, v)) node.audio.volume = Math.max(0, Math.min(1, v))
} }
function volIcon(vol, muted) { function volIcon(vol, muted) {
if (muted) return "\uf6a9" // fa-volume-mute if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028" // fa-volume-up if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027" // fa-volume-down if (vol > 0.2) return "\uf027"
return "\uf026" // fa-volume-off return "\uf026"
} }
// ── bar chip (default sink) ─────────────────────────────────────────
readonly property var defaultSink: Pipewire.defaultAudioSink readonly property var defaultSink: Pipewire.defaultAudioSink
readonly property real defaultVolume: safeVolume(defaultSink) readonly property real defaultVolume: safeVolume(defaultSink)
readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false
@ -66,7 +63,7 @@ Item {
if (root.defaultSink?.ready && root.defaultSink?.audio) if (root.defaultSink?.ready && root.defaultSink?.audio)
root.defaultSink.audio.muted = !root.defaultSink.audio.muted root.defaultSink.audio.muted = !root.defaultSink.audio.muted
} else { } else {
mixerPopup.visible = !mixerPopup.visible root.clickedLeft()
} }
} }
onWheel: wheel => { onWheel: wheel => {
@ -74,149 +71,4 @@ Item {
root.setVolume(root.defaultSink, root.defaultVolume + wheel.angleDelta.y / 120 * 0.05) root.setVolume(root.defaultSink, root.defaultVolume + wheel.angleDelta.y / 120 * 0.05)
} }
} }
// ── mixer popup ─────────────────────────────────────────────────────
PopupWindow {
id: mixerPopup
visible: false
anchor.item: root
anchor.edges: Edges.Bottom
anchor.gravity: Edges.Bottom
implicitWidth: mixerBg.implicitWidth
implicitHeight: mixerBg.implicitHeight
Rectangle {
id: mixerBg
anchors.fill: parent
color: Theme.bg
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitWidth: 300
implicitHeight: mixerCol.implicitHeight + 24
ColumnLayout {
id: mixerCol
anchors { fill: parent; margins: 12 }
spacing: 4
// ── Output devices ──────────────────────────────────────
Text {
text: "Output Devices"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isSink && !modelData.isStream && modelData.ready
}
}
// ── Application streams ────────────────────────────────
Text {
text: "Applications"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
// exclude monitor sources; include only audio output streams
visible: modelData.isStream && !modelData.isSink && modelData.ready
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
}
}
}
}
}
// ── per-node row component ──────────────────────────────────────────
component NodeRow: RowLayout {
id: row
required property var node
spacing: 8
implicitHeight: 28
// mute toggle
Text {
id: nodeIcon
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.node.ready && row.node.audio)
row.node.audio.muted = !row.node.audio.muted
}
}
}
// name
Text {
text: row.node.description || row.node.nickname || row.node.name || "?"
font.pixelSize: 11
color: Theme.textDim
Layout.preferredWidth: 90
elide: Text.ElideRight
}
// horizontal slider
Item {
Layout.fillWidth: true
implicitHeight: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: parent.width * Math.min(1, root.safeVolume(row.node))
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) {
root.setVolume(row.node, mx / width)
}
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
// percentage
Text {
text: Math.round(root.safeVolume(row.node) * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
}
} }

View File

@ -0,0 +1,143 @@
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../components"
// Volume mixer a plain Item so it can live inside an expanding section border.
Item {
id: root
implicitWidth: 300
implicitHeight: mixerCol.implicitHeight + 24
PwObjectTracker {
objects: Pipewire.nodes.values
}
function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume
return (v !== undefined && !isNaN(v)) ? v : 0
}
function setVolume(node, v) {
if (!node || !node.ready || !node.audio) return
node.audio.volume = Math.max(0, Math.min(1, v))
}
function volIcon(vol, muted) {
if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027"
return "\uf026"
}
ColumnLayout {
id: mixerCol
anchors { fill: parent; margins: 12 }
spacing: 4
Text {
text: "Output Devices"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isSink && !modelData.isStream && modelData.ready
}
}
Text {
text: "Applications"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isStream && !modelData.isSink && modelData.ready
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
}
}
}
component NodeRow: RowLayout {
id: row
required property var node
spacing: 8
implicitHeight: 28
Text {
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.node.ready && row.node.audio)
row.node.audio.muted = !row.node.audio.muted
}
}
}
Text {
text: row.node.description || row.node.nickname || row.node.name || "?"
font.pixelSize: 11
color: Theme.textDim
Layout.preferredWidth: 90
elide: Text.ElideRight
}
Item {
Layout.fillWidth: true
implicitHeight: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: parent.width * Math.min(1, root.safeVolume(row.node))
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) { root.setVolume(row.node, mx / width) }
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
Text {
text: Math.round(root.safeVolume(row.node) * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
}
}

View File

@ -1,9 +1,12 @@
Bar 1.0 Bar.qml Bar 1.0 Bar.qml
PopoutWindow 1.0 PopoutWindow.qml
CalendarContent 1.0 CalendarContent.qml
Clock 1.0 Clock.qml Clock 1.0 Clock.qml
MusicPlayer 1.0 MusicPlayer.qml MusicPlayer 1.0 MusicPlayer.qml
MusicPlayerControls 1.0 MusicPlayerControls.qml MusicPlayerControls 1.0 MusicPlayerControls.qml
NetworkStatus 1.0 NetworkStatus.qml NetworkStatus 1.0 NetworkStatus.qml
SysTray 1.0 SysTray.qml SysTray 1.0 SysTray.qml
VolumeControl 1.0 VolumeControl.qml VolumeControl 1.0 VolumeControl.qml
VolumeMixerContent 1.0 VolumeMixerContent.qml
Workspaces 1.0 Workspaces.qml Workspaces 1.0 Workspaces.qml
WorkspaceButton 1.0 WorkspaceButton.qml WorkspaceButton 1.0 WorkspaceButton.qml

View File

@ -3,7 +3,8 @@ import QtQuick
QtObject { QtObject {
// Core palette // Core palette
readonly property color bg: '#000000' readonly property color bg: '#D9000000' // bar background (semi-transparent)
readonly property color bgPopup: '#000000' // popup background (fully opaque)
readonly property color accent: '#9B1A1A' readonly property color accent: '#9B1A1A'
readonly property color border: '#FFFFFF' readonly property color border: '#FFFFFF'
readonly property color text: '#FFFFFF' readonly property color text: '#FFFFFF'
@ -12,7 +13,7 @@ QtObject {
readonly property color progressTrack: '#333333' readonly property color progressTrack: '#333333'
// Shape / sizing // Shape / sizing
readonly property int radius: 4 readonly property int radius: 16
readonly property int borderWidth: 2 readonly property int borderWidth: 2
readonly property int enclosureMargin: 3 readonly property int enclosureMargin: 3
} }