refactor: extract HoverPanel component, replace inline PanelWindows in Volume, Backlight, Mpris

This commit is contained in:
Damocles 2026-04-13 20:34:41 +02:00
parent 50af43a76e
commit 0fd3c78fb0
5 changed files with 686 additions and 840 deletions

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import "." as M import "." as M
M.BarSection { M.BarSection {
@ -12,9 +11,8 @@ M.BarSection {
tooltip: "" tooltip: ""
property int percent: 0 property int percent: 0
property bool _panelHovered: false
property bool _osdActive: false property bool _osdActive: false
readonly property bool _showPanel: root._hovered || _panelHovered || _osdActive readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive
onPercentChanged: if (percent > 0) onPercentChanged: if (percent > 0)
_flashPanel() _flashPanel()
@ -97,97 +95,13 @@ M.BarSection {
onWheel: event => root.adjust(event.angleDelta.y) onWheel: event => root.adjust(event.angleDelta.y)
} }
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
visible: _winVisible
color: "transparent"
property bool _winVisible: false
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-backlight"
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
}
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
}
M.PopupBackground {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor accentColor: root.accentColor
} panelNamespace: "nova-backlight"
contentWidth: 200
Column {
id: panelContent
width: 200
opacity: 0
y: -height
Item { Item {
width: parent.width width: parent.width
@ -261,4 +175,3 @@ M.BarSection {
} }
} }
} }
}

115
modules/HoverPanel.qml Normal file
View file

@ -0,0 +1,115 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Shared hover/OSD panel PanelWindow slides down from the bar on hover or
// external trigger. Parent module computes showPanel and reads panelHovered.
PanelWindow {
id: root
required property bool showPanel
required property real anchorX
required property color accentColor
property string panelNamespace: "nova-panel"
property real contentWidth: 220
property bool animateHeight: false
property bool panelHovered: false
default property alias content: panelContent.children
screen: QsWindow.window?.screen ?? null
visible: _winVisible
color: "transparent"
property bool _winVisible: false
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: root.panelNamespace
anchors.top: true
anchors.left: true
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(anchorX - implicitWidth / 2), (screen?.width ?? 1920) - implicitWidth))
implicitWidth: panelContent.width
implicitHeight: panelContent.height
Behavior on implicitHeight {
enabled: root.animateHeight
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
onShowPanelChanged: {
if (showPanel) {
_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: root._winVisible = false
}
HoverHandler {
onHoveredChanged: root.panelHovered = hovered
}
M.PopupBackground {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor
}
Column {
id: panelContent
width: root.contentWidth
opacity: 0
y: -height
}
}

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "." as M import "." as M
@ -58,9 +57,8 @@ M.BarSection {
required property var bar required property var bar
property bool _panelHovered: false
property bool _pinned: false property bool _pinned: false
readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: { on_AnyHoverChanged: {
@ -97,106 +95,14 @@ M.BarSection {
} }
} }
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
visible: _winVisible
color: "transparent"
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
Behavior on implicitHeight {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
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
}
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
}
M.PopupBackground {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor accentColor: root.accentColor
} panelNamespace: "nova-mpris"
contentWidth: 280
Column { animateHeight: true
id: panelContent
width: 280
opacity: 0
y: -height
topPadding: 4
bottomPadding: 4
spacing: 2
// Album art // Album art
Item { Item {
@ -479,4 +385,3 @@ M.BarSection {
} }
} }
} }
}

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import "." as M import "." as M
@ -40,11 +39,13 @@ M.BarSection {
} }
property bool _expanded: false property bool _expanded: false
property bool _panelHovered: false
property bool _osdActive: false property bool _osdActive: false
readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _expanded || _osdActive readonly property bool _showPanel: _anyHover || _expanded || _osdActive
on_ShowPanelChanged: if (!_showPanel)
_expanded = false
onVolumeChanged: _flashPanel() onVolumeChanged: _flashPanel()
onMutedChanged: _flashPanel() onMutedChanged: _flashPanel()
@ -104,101 +105,13 @@ M.BarSection {
} }
// Unified volume panel hover shows slider, click expands to show devices // Unified volume panel hover shows slider, click expands to show devices
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
visible: _winVisible
color: "transparent"
property bool _winVisible: false
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-volume"
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
// Show/hide logic
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel) {
panel._winVisible = true;
hideAnim.stop();
showAnim.start();
} else {
root._expanded = false;
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
}
// Keep panel open when mouse is over it
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
// Click inside panel doesn't dismiss
}
M.PopupBackground {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor accentColor: root.accentColor
} panelNamespace: "nova-volume"
contentWidth: 220
Column {
id: panelContent
width: 220
opacity: 0
y: -height
// Compact: slider row // Compact: slider row
Item { Item {
@ -509,4 +422,3 @@ M.BarSection {
} }
} }
} }
}

View file

@ -14,6 +14,7 @@ Tray 1.0 Tray.qml
TrayMenu 1.0 TrayMenu.qml TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml PopupPanel 1.0 PopupPanel.qml
PopupBackground 1.0 PopupBackground.qml PopupBackground 1.0 PopupBackground.qml
HoverPanel 1.0 HoverPanel.qml
PowerMenu 1.0 PowerMenu.qml PowerMenu 1.0 PowerMenu.qml
ScreenCorners 1.0 ScreenCorners.qml ScreenCorners 1.0 ScreenCorners.qml
ThemedIcon 1.0 ThemedIcon.qml ThemedIcon 1.0 ThemedIcon.qml