// Popup.qml - a reusable popup component for Quickshell. import Quickshell import Quickshell.Wayland import QtQuick PanelWindow { id: root required property var screen required property string popupName property int rightMargin: 10 property int topMargin: 1 property int autoHideDelay: Theme.popupAutoHideDelay default property alias content: contentItem.data // ── Visibility ──────────────────────────────────────────── property bool logicalOpen: GlobalStates.popups.active === popupName property bool _keepVisible: false visible: logicalOpen || _keepVisible onLogicalOpenChanged: { if (logicalOpen) { closeTimer.stop() _keepVisible = false openY.start() openOpacity.start() } else { hideTimer.stop() _keepVisible = true closeY.start() closeOpacity.start() closeTimer.restart() } } // Keeps the Overlay window alive for the duration of the close animation Timer { id: closeTimer interval: 280 onTriggered: root._keepVisible = false } WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-popup-" + popupName WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { top: true; left: true; right: true; bottom: true } color: Qt.rgba(0, 0, 0, 0) Timer { id: hideTimer interval: root.autoHideDelay repeat: false onTriggered: GlobalStates.popups.close() } MouseArea { anchors.fill: parent onClicked: GlobalStates.popups.close() } // ── Slide animations ────────────────────────────────────── NumberAnimation { id: openY; target: box; property: "y"; to: root.topMargin; duration: 250; easing.type: Easing.OutCubic } NumberAnimation { id: closeY; target: box; property: "y"; to: root.topMargin - 200; duration: 200; easing.type: Easing.InCubic } NumberAnimation { id: openOpacity; target: box; property: "opacity"; to: Theme.popupOpacity; duration: 200 } NumberAnimation { id: closeOpacity;target: box; property: "opacity"; to: 0.0; duration: 200 } // ── Popup box ───────────────────────────────────────────── Rectangle { id: box x: { var ax = GlobalStates.popups.anchorX var centered = ax > 0 ? ax - width / 2 : parent.width - width - root.rightMargin return Math.max(4, Math.min(parent.width - width - 4, centered)) } // Initial state: closed (above the bar, transparent) y: root.topMargin - 40 opacity: 0.0 width: contentItem.implicitWidth + 10 height: contentItem.implicitHeight + 10 color: Theme.popupBackground radius: Theme.radius * 2 border.color: Theme.popupBorderColor border.width: Theme.popupBorderWidth // Absorb clicks so the outer close-on-click doesn't fire inside the box MouseArea { anchors.fill: parent } // HoverHandler doesn't lose hover when a child MouseArea grabs a press, // so clicking buttons won't accidentally start the hide timer. HoverHandler { onHoveredChanged: hovered ? hideTimer.stop() : hideTimer.start() } Item { id: contentItem x: 5; y: 5 implicitWidth: childrenRect.width implicitHeight: childrenRect.height } } }