From 6efa5d599cf45904807c9ed4e8bfc3b594c1592d Mon Sep 17 00:00:00 2001 From: Johannes Knopp Date: Tue, 14 Apr 2026 18:13:39 +0200 Subject: [PATCH] update quickshell conf --- roles/quickshell/files/bar/Bar.qml | 153 +++++++++----- .../quickshell/files/bar/CalendarContent.qml | 139 ++++++++++++ roles/quickshell/files/bar/Clock.qml | 173 ++------------- roles/quickshell/files/bar/MusicPlayer.qml | 1 - .../files/bar/MusicPlayerControls.qml | 197 ++++++++---------- roles/quickshell/files/bar/PopoutWindow.qml | 162 ++++++++++++++ roles/quickshell/files/bar/VolumeControl.qml | 162 +------------- .../files/bar/VolumeMixerContent.qml | 143 +++++++++++++ roles/quickshell/files/bar/qmldir | 3 + roles/quickshell/files/components/Theme.qml | 5 +- 10 files changed, 663 insertions(+), 475 deletions(-) create mode 100644 roles/quickshell/files/bar/CalendarContent.qml create mode 100644 roles/quickshell/files/bar/PopoutWindow.qml create mode 100644 roles/quickshell/files/bar/VolumeMixerContent.qml diff --git a/roles/quickshell/files/bar/Bar.qml b/roles/quickshell/files/bar/Bar.qml index 1ab5ff9..9f8823c 100644 --- a/roles/quickshell/files/bar/Bar.qml +++ b/roles/quickshell/files/bar/Bar.qml @@ -17,68 +17,121 @@ PanelWindow { color: Theme.bg 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 { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - anchors.topMargin: 2 - anchors.bottomMargin: 2 - - // Left - Enclosure { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - child: Workspaces { - screenName: root.screenName - } + anchors { + fill: parent + leftMargin: 8 + rightMargin: 8 + topMargin: root.bw + bottomMargin: root.bw } - // Center — truly centered regardless of left/right content width - Enclosure { - anchors.centerIn: parent - child: MusicPlayer { - id: musicChip - onClicked: musicControls.visible = !musicControls.visible - } + Workspaces { + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + screenName: root.screenName } - // Right — status chips - Enclosure { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - child: Row { - spacing: 8 + MusicPlayer { + id: musicChip + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + onClicked: root.activePopup = root.activePopup === "controls" ? "" : "controls" + } - SysTray { - anchors.verticalCenter: parent.verticalCenter - barWindow: root - } + Row { + id: rightRow + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 8 - NetworkStatus { - anchors.verticalCenter: parent.verticalCenter - } - - VolumeControl { - anchors.verticalCenter: parent.verticalCenter - } - - Clock { - anchors.verticalCenter: parent.verticalCenter - } + SysTray { + anchors.verticalCenter: parent.verticalCenter + barWindow: root + } + NetworkStatus { + anchors.verticalCenter: parent.verticalCenter + } + VolumeControl { + 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. - // Centered horizontally within the bar window, placed just below it. - MusicPlayerControls { - id: musicControls - visible: false + // ── Music controls popup ────────────────────────────────────────── + PopoutWindow { + popupName: "controls" + activePopup: root.activePopup + + 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.rect.x: Math.round((root.width - musicControls.width) / 2) - anchor.rect.y: root.height - player: musicChip.player + anchor.rect.y: root.implicitHeight - root.bw - Theme.radius + readonly property real pw: Math.max(rightRow.width + 2 * root.pad, + 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 } + } } } diff --git a/roles/quickshell/files/bar/CalendarContent.qml b/roles/quickshell/files/bar/CalendarContent.qml new file mode 100644 index 0000000..3deedcc --- /dev/null +++ b/roles/quickshell/files/bar/CalendarContent.qml @@ -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 + } + } + } + } + } +} diff --git a/roles/quickshell/files/bar/Clock.qml b/roles/quickshell/files/bar/Clock.qml index 66522da..24e40f4 100644 --- a/roles/quickshell/files/bar/Clock.qml +++ b/roles/quickshell/files/bar/Clock.qml @@ -1,10 +1,12 @@ -import Quickshell import QtQuick import "../components" Item { id: root - implicitWidth: timeLabel.implicitWidth + 10 + + signal clicked + + implicitWidth: timeLabel.implicitWidth + 16 implicitHeight: 24 property var now: new Date() @@ -19,10 +21,13 @@ Item { readonly property string display: { const d = now const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - 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()] + " " + h + ":" + m + ":" + s + const dd = String(d.getDate()).padStart(2, "0") + const mm = String(d.getMonth() + 1).padStart(2, "0") + const yyyy = d.getFullYear() + 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 { @@ -37,160 +42,6 @@ Item { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: calPopup.visible = !calPopup.visible - } - - // --------------- 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 - } - } - } - } - } - } + onClicked: root.clicked() } } diff --git a/roles/quickshell/files/bar/MusicPlayer.qml b/roles/quickshell/files/bar/MusicPlayer.qml index f8781df..318c8b8 100644 --- a/roles/quickshell/files/bar/MusicPlayer.qml +++ b/roles/quickshell/files/bar/MusicPlayer.qml @@ -1,4 +1,3 @@ -import Quickshell import Quickshell.Services.Mpris import QtQuick import "../components" diff --git a/roles/quickshell/files/bar/MusicPlayerControls.qml b/roles/quickshell/files/bar/MusicPlayerControls.qml index f6e44b9..808a5ee 100644 --- a/roles/quickshell/files/bar/MusicPlayerControls.qml +++ b/roles/quickshell/files/bar/MusicPlayerControls.qml @@ -1,138 +1,124 @@ -import Quickshell import Quickshell.Services.Mpris import QtQuick import QtQuick.Layouts import "../components" -PopupWindow { +// Plain Item – lives inside the expanding center section border in Bar.qml. +Item { id: root required property var player - implicitWidth: 280 - implicitHeight: bg.implicitHeight + implicitWidth: 280 + implicitHeight: layout.implicitHeight + 28 - // Tickle positionChanged every frame while playing so the player.position - // binding re-evaluates. Paused while the user is dragging the seek bar. + // Keep the progress binding live while playing, pause while seeking. FrameAnimation { running: (root.player?.isPlaying ?? false) && !progressArea.pressed onTriggered: root.player.positionChanged() } - Rectangle { - id: bg - anchors.fill: parent - color: Theme.bg - border.color: Theme.border - border.width: Theme.borderWidth - radius: Theme.radius - implicitHeight: layout.implicitHeight + 28 + ColumnLayout { + id: layout + anchors { fill: parent; margins: 14 } + spacing: 8 - ColumnLayout { - id: layout - anchors { fill: parent; margins: 14 } - spacing: 8 + // Track info + Text { + text: root.player?.trackTitle ?? "" + color: Theme.text + font.pixelSize: 13 + font.bold: true + Layout.fillWidth: true + elide: Text.ElideRight + } - // Track info - Text { - text: root.player?.trackTitle ?? "" - color: Theme.text - font.pixelSize: 13 - font.bold: true - Layout.fillWidth: true - elide: Text.ElideRight - } + Text { + text: root.player?.trackArtist ?? "" + color: Theme.textDim + font.pixelSize: 11 + Layout.fillWidth: true + elide: Text.ElideRight + visible: (root.player?.trackArtist ?? "") !== "" + Layout.topMargin: -4 + } - Text { - text: root.player?.trackArtist ?? "" - color: Theme.textDim - font.pixelSize: 11 - Layout.fillWidth: true - elide: Text.ElideRight - visible: (root.player?.trackArtist ?? "") !== "" - Layout.topMargin: -4 - } + // Progress bar + Item { + visible: root.player?.positionSupported && root.player?.lengthSupported + && (root.player?.length ?? 0) > 0 + Layout.fillWidth: true + height: 12 - // Progress bar - Item { - visible: root.player?.positionSupported && root.player?.lengthSupported - && (root.player?.length ?? 0) > 0 - Layout.fillWidth: true - height: 12 // visual track is centred inside; extra height = easier to hit + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 3 + radius: 2 + color: Theme.progressTrack - // Track Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: 3 - radius: 2 - color: Theme.progressTrack - - // 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) + width: root.player?.length > 0 + ? parent.width * (root.player.position / root.player.length) + : 0 + height: parent.height + radius: parent.radius + color: Theme.accent } } - // Media controls - RowLayout { - Layout.fillWidth: true - spacing: 0 + MouseArea { + id: progressArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor - Item { Layout.fillWidth: true } - - ControlButton { - icon: "⏮" - iconSize: 18 - enabled: root.player?.canGoPrevious ?? false - onActivated: root.player.previous() + 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) } - 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 } + onClicked: seek(mouseX) + onPositionChanged: if (pressed) seek(mouseX) } } + + // 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 { id: btn required property string icon @@ -144,7 +130,6 @@ PopupWindow { implicitHeight: label.implicitHeight + 10 radius: Theme.radius color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent' - Behavior on color { ColorAnimation { duration: 80 } } Text { diff --git a/roles/quickshell/files/bar/PopoutWindow.qml b/roles/quickshell/files/bar/PopoutWindow.qml new file mode 100644 index 0000000..7750fb3 --- /dev/null +++ b/roles/quickshell/files/bar/PopoutWindow.qml @@ -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 + } + } + } +} diff --git a/roles/quickshell/files/bar/VolumeControl.qml b/roles/quickshell/files/bar/VolumeControl.qml index b80bce5..47e8089 100644 --- a/roles/quickshell/files/bar/VolumeControl.qml +++ b/roles/quickshell/files/bar/VolumeControl.qml @@ -1,4 +1,3 @@ -import Quickshell import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts @@ -9,13 +8,12 @@ Item { implicitWidth: chip.implicitWidth + 10 implicitHeight: 24 - // Track all PipeWire nodes so their properties are populated. - // Without this, nodes in Pipewire.nodes are "unbound" and have null audio/type. + signal clickedLeft + PwObjectTracker { objects: Pipewire.nodes.values } - // ── helpers ──────────────────────────────────────────────────────── function safeVolume(node) { if (!node || !node.ready || !node.audio) return 0 const v = node.audio.volume @@ -26,13 +24,12 @@ Item { node.audio.volume = Math.max(0, Math.min(1, v)) } function volIcon(vol, muted) { - if (muted) return "\uf6a9" // fa-volume-mute - if (vol > 0.6) return "\uf028" // fa-volume-up - if (vol > 0.2) return "\uf027" // fa-volume-down - return "\uf026" // fa-volume-off + if (muted) return "\uf6a9" + if (vol > 0.6) return "\uf028" + if (vol > 0.2) return "\uf027" + return "\uf026" } - // ── bar chip (default sink) ───────────────────────────────────────── readonly property var defaultSink: Pipewire.defaultAudioSink readonly property real defaultVolume: safeVolume(defaultSink) readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false @@ -66,7 +63,7 @@ Item { if (root.defaultSink?.ready && root.defaultSink?.audio) root.defaultSink.audio.muted = !root.defaultSink.audio.muted } else { - mixerPopup.visible = !mixerPopup.visible + root.clickedLeft() } } onWheel: wheel => { @@ -74,149 +71,4 @@ Item { 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 - } - } } diff --git a/roles/quickshell/files/bar/VolumeMixerContent.qml b/roles/quickshell/files/bar/VolumeMixerContent.qml new file mode 100644 index 0000000..c289802 --- /dev/null +++ b/roles/quickshell/files/bar/VolumeMixerContent.qml @@ -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 + } + } +} diff --git a/roles/quickshell/files/bar/qmldir b/roles/quickshell/files/bar/qmldir index 37727eb..b284364 100644 --- a/roles/quickshell/files/bar/qmldir +++ b/roles/quickshell/files/bar/qmldir @@ -1,9 +1,12 @@ Bar 1.0 Bar.qml +PopoutWindow 1.0 PopoutWindow.qml +CalendarContent 1.0 CalendarContent.qml Clock 1.0 Clock.qml MusicPlayer 1.0 MusicPlayer.qml MusicPlayerControls 1.0 MusicPlayerControls.qml NetworkStatus 1.0 NetworkStatus.qml SysTray 1.0 SysTray.qml VolumeControl 1.0 VolumeControl.qml +VolumeMixerContent 1.0 VolumeMixerContent.qml Workspaces 1.0 Workspaces.qml WorkspaceButton 1.0 WorkspaceButton.qml diff --git a/roles/quickshell/files/components/Theme.qml b/roles/quickshell/files/components/Theme.qml index c9faea2..8d5f472 100644 --- a/roles/quickshell/files/components/Theme.qml +++ b/roles/quickshell/files/components/Theme.qml @@ -3,7 +3,8 @@ import QtQuick QtObject { // 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 border: '#FFFFFF' readonly property color text: '#FFFFFF' @@ -12,7 +13,7 @@ QtObject { readonly property color progressTrack: '#333333' // Shape / sizing - readonly property int radius: 4 + readonly property int radius: 16 readonly property int borderWidth: 2 readonly property int enclosureMargin: 3 }