diff --git a/modules/Bar.qml b/modules/Bar.qml index f200738..e1efa6f 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -171,6 +171,7 @@ PanelWindow { } M.Volume { visible: M.Modules.volume.enable + bar: bar } } diff --git a/modules/BluetoothMenu.qml b/modules/BluetoothMenu.qml index 6d4d1e4..0c2cfdb 100644 --- a/modules/BluetoothMenu.qml +++ b/modules/BluetoothMenu.qml @@ -3,10 +3,11 @@ import Quickshell import Quickshell.Io import "." as M -M.PopupPanel { +M.HoverPanel { id: menuWindow - panelWidth: 250 + popupMode: true + contentWidth: 250 property var _devices: [] diff --git a/modules/Cpu.qml b/modules/Cpu.qml index d49a9fc..815f2cd 100644 --- a/modules/Cpu.qml +++ b/modules/Cpu.qml @@ -222,7 +222,7 @@ M.BarSection { anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: parent._f.toFixed(2) - color: parent._barColor + color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily width: 32 diff --git a/modules/HoverPanel.qml b/modules/HoverPanel.qml index 03e5194..377a75d 100644 --- a/modules/HoverPanel.qml +++ b/modules/HoverPanel.qml @@ -3,18 +3,36 @@ 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. +// Unified bar panel — fullscreen transparent window so content can resize +// freely without triggering Wayland surface resizes. +// +// Hover mode (popupMode: false, default): +// Parent drives visibility via showPanel. Panel auto-closes when showPanel +// drops, with a 50ms debounce. Reports panelHovered back to parent. +// Pass anchorItem for lazy position computation on each show. +// +// Popup mode (popupMode: true): +// Shows immediately on creation. Stays open until click-outside or +// dismiss() call. Emits dismissed() when closed — caller's LazyLoader +// sets active: false. Pass anchorX (screen-relative centre x). PanelWindow { id: root - required property bool showPanel - required property Item anchorItem + property bool popupMode: false + + // Hover mode + property bool showPanel: true + property Item anchorItem: null + property bool panelHovered: false + + // Popup mode + property real anchorX: -1 + signal dismissed + + // Shared 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 @@ -29,49 +47,52 @@ PanelWindow { anchors.top: true anchors.left: true + anchors.right: true + anchors.bottom: true margins.top: 0 - implicitWidth: panelContent.width - implicitHeight: panelContent.height - - Behavior on implicitHeight { - enabled: root.animateHeight - NumberAnimation { - duration: 100 - easing.type: Easing.OutCubic - } - } - function _updatePosition() { - const pt = anchorItem.mapToGlobal(anchorItem.width / 2, 0); const scr = screen; const sw = scr?.width ?? 1920; - margins.left = Math.max(0, Math.min(Math.round(pt.x - (scr?.x ?? 0) - contentWidth / 2), sw - contentWidth)); + let cx; + if (root.anchorItem) { + const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0); + cx = pt.x - (scr?.x ?? 0); + } else { + cx = root.anchorX; + } + panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth)); } + function _show() { + _updatePosition(); + _winVisible = true; + hideAnim.stop(); + showAnim.start(); + } + + function dismiss() { + showAnim.stop(); + hideAnim.start(); + } + + Component.onCompleted: if (popupMode) + _show() + Timer { id: _hideTimer interval: 50 - onTriggered: { - if (!root.showPanel) { - console.log("[hp:" + panelNamespace + "] hideTimer fired, starting hideAnim"); - showAnim.stop(); - hideAnim.start(); - } - } + onTriggered: if (!root.showPanel) + root.dismiss() } - on_WinVisibleChanged: console.log("[hp:" + panelNamespace + "] _winVisible →", _winVisible) - onShowPanelChanged: { - console.log("[hp:" + panelNamespace + "] showPanel →", showPanel); + if (root.popupMode) + return; if (showPanel) { _hideTimer.stop(); - _updatePosition(); - _winVisible = true; - hideAnim.stop(); - showAnim.start(); + _show(); } else { _hideTimer.restart(); } @@ -80,14 +101,14 @@ PanelWindow { ParallelAnimation { id: showAnim NumberAnimation { - target: panelContent + target: panelContainer property: "opacity" to: 1 duration: 120 easing.type: Easing.OutCubic } NumberAnimation { - target: panelContent + target: panelContainer property: "y" to: 0 duration: 150 @@ -98,57 +119,81 @@ PanelWindow { ParallelAnimation { id: hideAnim NumberAnimation { - target: panelContent + target: panelContainer property: "opacity" to: 0 duration: 150 easing.type: Easing.InCubic } NumberAnimation { - target: panelContent + target: panelContainer property: "y" - to: -panelContent.height + to: -panelContainer.height duration: 150 easing.type: Easing.InCubic } - onStarted: console.log("[hp:" + panelNamespace + "] hideAnim started") - onFinished: root._winVisible = false - } - - HoverHandler { - onHoveredChanged: { - console.log("[hp:" + panelNamespace + "] hovered →", hovered); - root.panelHovered = hovered; + onFinished: { + root._winVisible = false; + if (root.popupMode) + root.dismissed(); } } + // Popup mode: click-outside dismiss (declared first = lowest z = below content) + MouseArea { + anchors.fill: parent + visible: root.popupMode + enabled: root.popupMode + onClicked: root.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) + x: panelContainer.x + y: panelContainer.y + width: panelContainer.width + height: panelContainer.height + opacity: panelContainer.opacity * Math.max(M.Theme.barOpacity, 0.85) accentColor: root.accentColor } - Column { - id: panelContent - width: root.contentWidth - opacity: 0 + Item { + id: panelContainer + x: 0 y: -height + width: root.contentWidth + height: panelContent.height + opacity: 0 + + // Popup mode: eat clicks on panel background so outer dismiss doesn't fire + MouseArea { + anchors.fill: parent + visible: root.popupMode + enabled: root.popupMode + } + + Column { + id: panelContent + width: root.contentWidth + + HoverHandler { + enabled: !root.popupMode + onHoveredChanged: if (!root.popupMode) + root.panelHovered = hovered + } + } } - // Border overlay — drawn on top of content so full-bleed items (e.g. album art) don't cover it + // Border overlay — on top of content so full-bleed items don't cover it Rectangle { - x: panelContent.x - y: panelContent.y - width: panelContent.width - height: panelContent.height + x: panelContainer.x + y: panelContainer.y + width: panelContainer.width + height: panelContainer.height color: "transparent" border.color: root.accentColor border.width: 1 bottomLeftRadius: M.Theme.radius bottomRightRadius: M.Theme.radius - opacity: panelContent.opacity + opacity: panelContainer.opacity } } diff --git a/modules/Mpris.qml b/modules/Mpris.qml index abf2a88..252a3cc 100644 --- a/modules/Mpris.qml +++ b/modules/Mpris.qml @@ -103,7 +103,6 @@ M.BarSection { accentColor: root.accentColor panelNamespace: "nova-mpris" contentWidth: 280 - animateHeight: true // Album art Item { diff --git a/modules/NetworkMenu.qml b/modules/NetworkMenu.qml index bc6a6d2..4f8e128 100644 --- a/modules/NetworkMenu.qml +++ b/modules/NetworkMenu.qml @@ -3,10 +3,11 @@ import Quickshell import Quickshell.Io import "." as M -M.PopupPanel { +M.HoverPanel { id: menuWindow - panelWidth: 250 + popupMode: true + contentWidth: 250 property var _networks: [] diff --git a/modules/PopupPanel.qml b/modules/PopupPanel.qml deleted file mode 100644 index ed8b5e6..0000000 --- a/modules/PopupPanel.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import "." as M - -// Shared flyout popup window — slides down from the bar, dismisses on -// click outside. Created on demand via Loader; animates in on creation, -// animates out then emits dismissed() for the Loader to deactivate. -PanelWindow { - id: root - - default property alias content: contentCol.children - required property var screen - required property real anchorX - property real panelWidth: 220 - property color accentColor: M.Theme.base05 - - signal dismissed - - visible: true - color: "transparent" - - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusiveZone: 0 - WlrLayershell.namespace: "nova-popup" - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - - Component.onCompleted: showAnim.start() - - function dismiss() { - showAnim.stop(); - hideAnim.start(); - } - - // Click outside → dismiss - MouseArea { - anchors.fill: parent - onClicked: root.dismiss() - } - - Item { - id: panel - - x: Math.max(0, Math.min(Math.round(root.anchorX - contentCol.width / 2), root.width - contentCol.width)) - y: 0 - width: contentCol.width - height: contentCol.height - opacity: 0 - - // Eat clicks inside the panel - MouseArea { - anchors.fill: parent - } - - M.PopupBackground { - anchors.fill: parent - accentColor: root.accentColor - } - - Column { - id: contentCol - width: root.panelWidth - topPadding: 4 - bottomPadding: 4 - spacing: 2 - } - } - - ParallelAnimation { - id: showAnim - NumberAnimation { - target: panel - property: "opacity" - from: 0 - to: 1 - duration: 150 - easing.type: Easing.OutCubic - } - NumberAnimation { - target: panel - property: "y" - from: -panel.height - to: 0 - duration: 200 - easing.type: Easing.OutCubic - } - } - - ParallelAnimation { - id: hideAnim - NumberAnimation { - target: panel - property: "opacity" - to: 0 - duration: 150 - easing.type: Easing.InCubic - } - NumberAnimation { - target: panel - property: "y" - to: -panel.height - duration: 150 - easing.type: Easing.InCubic - } - onFinished: root.dismissed() - } -} diff --git a/modules/PowerMenu.qml b/modules/PowerMenu.qml index 53197ac..b93a5f6 100644 --- a/modules/PowerMenu.qml +++ b/modules/PowerMenu.qml @@ -2,10 +2,11 @@ import QtQuick import Quickshell import "." as M -M.PopupPanel { +M.HoverPanel { id: menuWindow - panelWidth: 180 + popupMode: true + contentWidth: 180 signal runCommand(var cmd) @@ -56,7 +57,7 @@ M.PopupPanel { required property var modelData required property int index - width: menuWindow.panelWidth + width: menuWindow.contentWidth height: 32 Rectangle { diff --git a/modules/Volume.qml b/modules/Volume.qml index 9631646..1b55d60 100644 --- a/modules/Volume.qml +++ b/modules/Volume.qml @@ -38,18 +38,9 @@ M.BarSection { return streams; } - property bool _expanded: false property bool _osdActive: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered - readonly property bool _showPanel: _anyHover || _expanded || _osdActive - - on_ShowPanelChanged: { - console.log("[vol] showPanel →", _showPanel, "| expanded:", _expanded, "| anyHover:", _anyHover); - if (!_showPanel) - _expanded = false; - } - - on_ExpandedChanged: console.log("[vol] expanded →", _expanded) + readonly property bool _showPanel: _anyHover || _osdActive onVolumeChanged: _flashPanel() onMutedChanged: _flashPanel() @@ -65,6 +56,8 @@ M.BarSection { onTriggered: root._osdActive = false } + required property var bar + M.BarIcon { icon: root._volumeIcon minIcon: "\uF028" @@ -98,7 +91,7 @@ M.BarSection { } } - // Unified volume panel — hover shows slider, click expands to show devices + // OSD panel — hover shows slider only, fixed height, no resize M.HoverPanel { id: hoverPanel showPanel: root._showPanel @@ -107,14 +100,12 @@ M.BarSection { accentColor: root.accentColor panelNamespace: "nova-volume" contentWidth: 220 - animateHeight: true - // Compact: slider row + // Slider row Item { width: parent.width height: 36 - // Mute toggle Text { id: muteIcon anchors.left: parent.left @@ -132,7 +123,6 @@ M.BarSection { } } - // Slider Item { id: slider anchors.left: muteIcon.right @@ -147,13 +137,11 @@ M.BarSection { color: M.Theme.base02 radius: 3 } - Rectangle { width: parent.width * Math.min(1, Math.max(0, root.volume)) height: parent.height color: root._volumeColor radius: 3 - Behavior on width { NumberAnimation { duration: 80 @@ -191,7 +179,7 @@ M.BarSection { } } - // Sink name — click to expand/collapse device list + // Sink name row — click chevron to open mixer popup Item { width: parent.width height: 22 @@ -214,7 +202,7 @@ M.BarSection { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: root._expanded ? "\uF077" : "\uF078" + text: "\uF078" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 3 font.family: M.Theme.iconFontFamily @@ -222,17 +210,24 @@ M.BarSection { TapHandler { cursorShape: Qt.PointingHandCursor - onTapped: root._expanded = true + onTapped: mixerLoader.active = true } } + } - // Expanded: output device list - Column { - id: deviceList - width: parent.width - visible: root._expanded + // Mixer popup — separate window, no resize issues + LazyLoader { + id: mixerLoader + active: false - // Separator + M.PopupPanel { + accentColor: root.accentColor + screen: root.bar.screen + anchorX: root.mapToGlobal(root.width / 2, 0).x - (root.bar.screen?.x ?? 0) + panelWidth: 220 + onDismissed: mixerLoader.active = false + + // Output devices Rectangle { width: parent.width - 16 height: 1 @@ -240,7 +235,6 @@ M.BarSection { color: M.Theme.base03 } - // Header Text { width: parent.width height: 24 @@ -258,7 +252,7 @@ M.BarSection { delegate: Item { required property var modelData - width: deviceList.width + width: 220 height: 28 readonly property bool _active: modelData === root.sink @@ -293,38 +287,31 @@ M.BarSection { TapHandler { onTapped: { Pipewire.preferredDefaultAudioSink = modelData; - root._expanded = false; + mixerLoader.active = false; } } } } - // Streams header (only if there are streams) - Item { + // Streams section + Rectangle { + visible: root._streamList.length > 0 + width: parent.width - 16 + height: visible ? 1 : 0 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { visible: root._streamList.length > 0 width: parent.width - height: visible ? streamSep.height + streamHeader.height : 0 - - Rectangle { - id: streamSep - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Text { - id: streamHeader - anchors.top: streamSep.bottom - width: parent.width - height: 24 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Applications" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - } + height: visible ? 24 : 0 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Applications" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily } Repeater { @@ -334,7 +321,7 @@ M.BarSection { id: streamEntry required property var modelData - width: deviceList.width + width: 220 height: 32 readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" @@ -422,12 +409,6 @@ M.BarSection { } } } - - // Bottom padding - Item { - width: 1 - height: 4 - } } } } diff --git a/modules/qmldir b/modules/qmldir index c79fa03..076eec8 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -12,7 +12,6 @@ Clock 1.0 Clock.qml Volume 1.0 Volume.qml Tray 1.0 Tray.qml TrayMenu 1.0 TrayMenu.qml -PopupPanel 1.0 PopupPanel.qml PopupBackground 1.0 PopupBackground.qml HoverPanel 1.0 HoverPanel.qml PowerMenu 1.0 PowerMenu.qml