merge PopupPanel into HoverPanel with popupMode flag, delete PopupPanel

This commit is contained in:
Damocles 2026-04-13 23:26:35 +02:00
parent 885e446bf4
commit 71258b6d80
6 changed files with 121 additions and 169 deletions

View file

@ -3,10 +3,11 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 250 popupMode: true
contentWidth: 250
property var _devices: [] property var _devices: []

View file

@ -3,19 +3,36 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import "." as M import "." as M
// Shared hover/OSD panel PanelWindow slides down from the bar on hover or // Unified bar panel fullscreen transparent window so content can resize
// external trigger. Fullscreen transparent window so content can resize freely // freely without triggering Wayland surface resizes.
// without triggering Wayland surface resize events. Parent module computes //
// showPanel and reads panelHovered. // 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 { PanelWindow {
id: root id: root
required property bool showPanel property bool popupMode: false
required property Item anchorItem
// 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 required property color accentColor
property string panelNamespace: "nova-panel" property string panelNamespace: "nova-panel"
property real contentWidth: 220 property real contentWidth: 220
property bool panelHovered: false
default property alias content: panelContent.children default property alias content: panelContent.children
@ -36,30 +53,46 @@ PanelWindow {
margins.top: 0 margins.top: 0
function _updatePosition() { function _updatePosition() {
const pt = anchorItem.mapToGlobal(anchorItem.width / 2, 0);
const scr = screen; const scr = screen;
const sw = scr?.width ?? 1920; const sw = scr?.width ?? 1920;
panelContent.x = 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));
} }
Timer { function _show() {
id: _hideTimer
interval: 50
onTriggered: {
if (!root.showPanel) {
showAnim.stop();
hideAnim.start();
}
}
}
onShowPanelChanged: {
if (showPanel) {
_hideTimer.stop();
_updatePosition(); _updatePosition();
_winVisible = true; _winVisible = true;
hideAnim.stop(); hideAnim.stop();
showAnim.start(); showAnim.start();
}
function dismiss() {
showAnim.stop();
hideAnim.start();
}
Component.onCompleted: if (popupMode)
_show()
Timer {
id: _hideTimer
interval: 50
onTriggered: if (!root.showPanel)
root.dismiss()
}
onShowPanelChanged: {
if (root.popupMode)
return;
if (showPanel) {
_hideTimer.stop();
_show();
} else { } else {
_hideTimer.restart(); _hideTimer.restart();
} }
@ -68,14 +101,14 @@ PanelWindow {
ParallelAnimation { ParallelAnimation {
id: showAnim id: showAnim
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "opacity" property: "opacity"
to: 1 to: 1
duration: 120 duration: 120
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
} }
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "y" property: "y"
to: 0 to: 0
duration: 150 duration: 150
@ -86,53 +119,81 @@ PanelWindow {
ParallelAnimation { ParallelAnimation {
id: hideAnim id: hideAnim
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "opacity" property: "opacity"
to: 0 to: 0
duration: 150 duration: 150
easing.type: Easing.InCubic easing.type: Easing.InCubic
} }
NumberAnimation { NumberAnimation {
target: panelContent target: panelContainer
property: "y" property: "y"
to: -panelContent.height to: -panelContainer.height
duration: 150 duration: 150
easing.type: Easing.InCubic easing.type: Easing.InCubic
} }
onFinished: root._winVisible = false 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 { M.PopupBackground {
x: panelContent.x x: panelContainer.x
y: panelContent.y y: panelContainer.y
width: panelContent.width width: panelContainer.width
height: panelContent.height height: panelContainer.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) opacity: panelContainer.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor accentColor: root.accentColor
} }
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 { Column {
id: panelContent id: panelContent
width: root.contentWidth width: root.contentWidth
opacity: 0
y: -height
HoverHandler { HoverHandler {
onHoveredChanged: root.panelHovered = hovered 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 { Rectangle {
x: panelContent.x x: panelContainer.x
y: panelContent.y y: panelContainer.y
width: panelContent.width width: panelContainer.width
height: panelContent.height height: panelContainer.height
color: "transparent" color: "transparent"
border.color: root.accentColor border.color: root.accentColor
border.width: 1 border.width: 1
bottomLeftRadius: M.Theme.radius bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius bottomRightRadius: M.Theme.radius
opacity: panelContent.opacity opacity: panelContainer.opacity
} }
} }

View file

@ -3,10 +3,11 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 250 popupMode: true
contentWidth: 250
property var _networks: [] property var _networks: []

View file

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

View file

@ -2,10 +2,11 @@ import QtQuick
import Quickshell import Quickshell
import "." as M import "." as M
M.PopupPanel { M.HoverPanel {
id: menuWindow id: menuWindow
panelWidth: 180 popupMode: true
contentWidth: 180
signal runCommand(var cmd) signal runCommand(var cmd)
@ -56,7 +57,7 @@ M.PopupPanel {
required property var modelData required property var modelData
required property int index required property int index
width: menuWindow.panelWidth width: menuWindow.contentWidth
height: 32 height: 32
Rectangle { Rectangle {

View file

@ -12,7 +12,6 @@ Clock 1.0 Clock.qml
Volume 1.0 Volume.qml Volume 1.0 Volume.qml
Tray 1.0 Tray.qml Tray 1.0 Tray.qml
TrayMenu 1.0 TrayMenu.qml TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml
PopupBackground 1.0 PopupBackground.qml PopupBackground 1.0 PopupBackground.qml
HoverPanel 1.0 HoverPanel.qml HoverPanel 1.0 HoverPanel.qml
PowerMenu 1.0 PowerMenu.qml PowerMenu 1.0 PowerMenu.qml