From 90ec41fd05265b794a0d38e02f3a49b8f84a0f84 Mon Sep 17 00:00:00 2001 From: Damocles Date: Sun, 12 Apr 2026 18:38:04 +0200 Subject: [PATCH] mpris: inline hover panel replaces separate MprisMenu --- modules/Mpris.qml | 342 ++++++++++++++++++++++++++++++++++++++---- modules/MprisMenu.qml | 219 --------------------------- modules/qmldir | 1 - 3 files changed, 316 insertions(+), 246 deletions(-) delete mode 100644 modules/MprisMenu.qml diff --git a/modules/Mpris.qml b/modules/Mpris.qml index 28e5f5a..7d1b299 100644 --- a/modules/Mpris.qml +++ b/modules/Mpris.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Wayland import Quickshell.Services.Mpris import "." as M @@ -8,27 +9,40 @@ M.BarSection { spacing: M.Theme.moduleSpacing opacity: M.Modules.mpris.enable && player !== null ? 1 : 0 visible: opacity > 0 - tooltip: { - const p = root.player; - if (!p) - return ""; - const parts = []; - if (p.trackTitle) - parts.push(p.trackTitle); - if (p.trackArtists?.length) - parts.push(Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : p.trackArtists); - if (p.trackAlbum) - parts.push(p.trackAlbum); - return parts.join("\n") || p.identity; - } + tooltip: "" readonly property MprisPlayer player: Mpris.players.values[0] ?? null readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing + required property var bar + + property bool _panelHovered: false + property bool _pinned: false + readonly property bool _anyHover: root._hovered || _panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false + } + M.BarIcon { icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB") color: M.Theme.base0E anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root._pinned = !root._pinned + } } M.BarLabel { label: root.player?.trackTitle || root.player?.identity || "" @@ -36,23 +50,299 @@ M.BarSection { elide: Text.ElideRight width: Math.min(implicitWidth, 200) anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root._pinned = !root._pinned + } } - required property var bar + PanelWindow { + id: panel - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: menuLoader.active = !menuLoader.active - } + screen: QsWindow.window?.screen ?? null + visible: _winVisible + color: "transparent" - Loader { - id: menuLoader - active: false - sourceComponent: M.MprisMenu { - player: root.player - screen: root.bar.screen - anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - onDismissed: menuLoader.active = false + property bool _winVisible: false + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusiveZone: 0 + WlrLayershell.namespace: "nova-mpris" + + anchors.top: true + anchors.left: true + + margins.top: 0 + margins.left: Math.max(0, Math.min( + Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), + (panel.screen?.width ?? 1920) - implicitWidth + )) + + implicitWidth: panelContent.width + implicitHeight: panelContent.height + + Connections { + target: root + function on_ShowPanelChanged() { + if (root._showPanel) { + panel._winVisible = true; + hideAnim.stop(); + showAnim.start(); + } else { + showAnim.stop(); + hideAnim.start(); + } + } + } + + ParallelAnimation { + id: showAnim + NumberAnimation { target: panelContent; property: "opacity"; to: 1; duration: 120; easing.type: Easing.OutCubic } + NumberAnimation { target: panelContent; property: "y"; to: 0; duration: 150; easing.type: Easing.OutCubic } + } + + ParallelAnimation { + id: hideAnim + NumberAnimation { target: panelContent; property: "opacity"; to: 0; duration: 150; easing.type: Easing.InCubic } + NumberAnimation { target: panelContent; property: "y"; to: -panelContent.height; duration: 150; easing.type: Easing.InCubic } + onFinished: panel._winVisible = false + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: root._panelHovered = containsMouse + } + + Rectangle { + x: panelContent.x + y: panelContent.y + width: panelContent.width + height: panelContent.height + color: M.Theme.base01 + opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + } + + Column { + id: panelContent + width: 280 + opacity: 0 + y: -height + topPadding: 4 + bottomPadding: 4 + spacing: 2 + + // Album art + Item { + width: parent.width + height: _artImg.status === Image.Ready ? 140 : 60 + clip: true + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + } + + Image { + id: _artImg + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + visible: status === Image.Ready + asynchronous: true + + property string _lastGoodSource: "" + property string _lastTrack: "" + readonly property string _artUrl: root.player?.trackArtUrl ?? "" + readonly property string _track: root.player?.trackTitle ?? "" + on_ArtUrlChanged: if (_artUrl) _lastGoodSource = _artUrl + on_TrackChanged: if (_track !== _lastTrack) { _lastTrack = _track; _lastGoodSource = _artUrl || "" } + source: _lastGoodSource + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 40 + visible: _artImg.visible + gradient: Gradient { + GradientStop { position: 0; color: "transparent" } + GradientStop { position: 1; color: M.Theme.base01 } + } + } + + Text { + anchors.centerIn: parent + text: "\uF001" + color: M.Theme.base04 + font.pixelSize: 28 + font.family: M.Theme.iconFontFamily + visible: _artImg.status !== Image.Ready + } + } + + // Track info + Item { + width: parent.width + height: titleCol.implicitHeight + 8 + + Column { + id: titleCol + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 2 + + Text { + width: parent.width + text: root.player?.trackTitle || "No track" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + 1 + font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + text: { + const p = root.player; + if (!p) return ""; + const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); + return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); + } + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + visible: text !== "" + } + } + } + + // Progress + Item { + width: parent.width + height: 20 + + readonly property real pos: root.player?.position ?? 0 + readonly property real dur: root.player?.length ?? 0 + readonly property real frac: dur > 0 ? pos / dur : 0 + + function _fmtTime(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.pos) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.dur) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 80 + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) + height: parent.height + color: M.Theme.base0E + radius: 2 + } + } + } + + // Transport controls + Item { + width: parent.width + height: 36 + + Row { + anchors.centerIn: parent + spacing: 24 + + Text { + text: "\uF048" + color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoPrevious ?? false + onClicked: root.player.previous() + } + } + + Text { + text: root.playing ? "\uF04C" : "\uF04B" + color: M.Theme.base0E + font.pixelSize: M.Theme.fontSize + 8 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.player?.togglePlaying() + } + } + + Text { + text: "\uF051" + color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoNext ?? false + onClicked: root.player.next() + } + } + } + } + + // Player name + Text { + width: parent.width + height: 20 + horizontalAlignment: Text.AlignHCenter + text: root.player?.identity ?? "" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } } } } diff --git a/modules/MprisMenu.qml b/modules/MprisMenu.qml deleted file mode 100644 index 719436d..0000000 --- a/modules/MprisMenu.qml +++ /dev/null @@ -1,219 +0,0 @@ -import QtQuick -import Quickshell.Services.Mpris -import "." as M - -M.PopupPanel { - id: menuWindow - - panelWidth: 280 - - required property MprisPlayer player - - function _fmtTime(ms) { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - return m + ":" + String(s % 60).padStart(2, "0"); - } - - Item { - width: menuWindow.panelWidth - height: _artImg.status === Image.Ready ? 140 : 60 - clip: true - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - } - - Image { - id: _artImg - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - visible: status === Image.Ready - asynchronous: true - - property string _lastGoodSource: "" - property string _lastTrack: "" - readonly property string _artUrl: menuWindow.player?.trackArtUrl ?? "" - readonly property string _track: menuWindow.player?.trackTitle ?? "" - on_ArtUrlChanged: if (_artUrl) _lastGoodSource = _artUrl - on_TrackChanged: if (_track !== _lastTrack) { _lastTrack = _track; _lastGoodSource = _artUrl || "" } - source: _lastGoodSource - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - visible: _artImg.visible - gradient: Gradient { - GradientStop { position: 0; color: "transparent" } - GradientStop { position: 1; color: M.Theme.base01 } - } - } - - Text { - anchors.centerIn: parent - text: "\uF001" - color: M.Theme.base04 - font.pixelSize: 28 - font.family: M.Theme.iconFontFamily - visible: _artImg.status !== Image.Ready - } - } - - Item { - width: menuWindow.panelWidth - height: titleCol.implicitHeight + 8 - - Column { - id: titleCol - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 2 - - Text { - width: parent.width - text: menuWindow.player?.trackTitle || "No track" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize + 1 - font.family: M.Theme.fontFamily - font.bold: true - elide: Text.ElideRight - } - - Text { - width: parent.width - text: { - const p = menuWindow.player; - if (!p) return ""; - const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); - return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); - } - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - visible: text !== "" - } - } - } - - Item { - width: menuWindow.panelWidth - height: 20 - - readonly property real pos: menuWindow.player?.position ?? 0 - readonly property real dur: menuWindow.player?.length ?? 0 - readonly property real frac: dur > 0 ? pos / dur : 0 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: menuWindow._fmtTime(parent.pos) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: menuWindow._fmtTime(parent.dur) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 80 - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) - height: parent.height - color: M.Theme.base0E - radius: 2 - } - } - } - - Item { - width: menuWindow.panelWidth - height: 36 - - Row { - anchors.centerIn: parent - spacing: 24 - - Text { - text: "\uF048" - color: menuWindow.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: menuWindow.player?.canGoPrevious ?? false - onClicked: menuWindow.player.previous() - } - } - - Text { - text: menuWindow.player?.playbackState === MprisPlaybackState.Playing ? "\uF04C" : "\uF04B" - color: M.Theme.base0E - font.pixelSize: M.Theme.fontSize + 8 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: menuWindow.player?.togglePlaying() - } - } - - Text { - text: "\uF051" - color: menuWindow.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: menuWindow.player?.canGoNext ?? false - onClicked: menuWindow.player.next() - } - } - } - } - - Item { - width: menuWindow.panelWidth - height: 20 - - Text { - anchors.centerIn: parent - text: menuWindow.player?.identity ?? "" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } -} diff --git a/modules/qmldir b/modules/qmldir index 49b15cd..e936add 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -20,7 +20,6 @@ Osd 1.0 Osd.qml ThemedIcon 1.0 ThemedIcon.qml Battery 1.0 Battery.qml Mpris 1.0 Mpris.qml -MprisMenu 1.0 MprisMenu.qml Network 1.0 Network.qml NetworkMenu 1.0 NetworkMenu.qml Bluetooth 1.0 Bluetooth.qml