mpris: inline hover panel replaces separate MprisMenu

This commit is contained in:
Damocles 2026-04-12 18:38:04 +02:00
parent cde8aa99aa
commit 90ec41fd05
3 changed files with 316 additions and 246 deletions

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "." as M import "." as M
@ -8,27 +9,40 @@ M.BarSection {
spacing: M.Theme.moduleSpacing spacing: M.Theme.moduleSpacing
opacity: M.Modules.mpris.enable && player !== null ? 1 : 0 opacity: M.Modules.mpris.enable && player !== null ? 1 : 0
visible: opacity > 0 visible: opacity > 0
tooltip: { 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;
}
readonly property MprisPlayer player: Mpris.players.values[0] ?? null readonly property MprisPlayer player: Mpris.players.values[0] ?? null
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing 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 { M.BarIcon {
icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB") icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB")
color: M.Theme.base0E color: M.Theme.base0E
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root.player?.trackTitle || root.player?.identity || "" label: root.player?.trackTitle || root.player?.identity || ""
@ -36,23 +50,299 @@ M.BarSection {
elide: Text.ElideRight elide: Text.ElideRight
width: Math.min(implicitWidth, 200) width: Math.min(implicitWidth, 200)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root._pinned = !root._pinned
}
} }
required property var bar PanelWindow {
id: panel
TapHandler { screen: QsWindow.window?.screen ?? null
cursorShape: Qt.PointingHandCursor visible: _winVisible
onTapped: menuLoader.active = !menuLoader.active color: "transparent"
}
Loader { property bool _winVisible: false
id: menuLoader
active: false WlrLayershell.layer: WlrLayer.Overlay
sourceComponent: M.MprisMenu { WlrLayershell.exclusiveZone: 0
player: root.player WlrLayershell.namespace: "nova-mpris"
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchors.top: true
onDismissed: menuLoader.active = false 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
}
} }
} }
} }

View file

@ -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
}
}
}

View file

@ -20,7 +20,6 @@ Osd 1.0 Osd.qml
ThemedIcon 1.0 ThemedIcon.qml ThemedIcon 1.0 ThemedIcon.qml
Battery 1.0 Battery.qml Battery 1.0 Battery.qml
Mpris 1.0 Mpris.qml Mpris 1.0 Mpris.qml
MprisMenu 1.0 MprisMenu.qml
Network 1.0 Network.qml Network 1.0 Network.qml
NetworkMenu 1.0 NetworkMenu.qml NetworkMenu 1.0 NetworkMenu.qml
Bluetooth 1.0 Bluetooth.qml Bluetooth 1.0 Bluetooth.qml