import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import ".." PanelWindow { id: notificationLayer anchors { top: true right: true } margins { top: 24 right: 24 } // Top layer keeps notifications above normal windows while still usually // below fullscreen overlays. Overlay would be too aggressive here. WlrLayershell.layer: WlrLayer.Top WlrLayershell.focusable: false WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.namespace: "deltarune-quickshell-notifications" exclusionMode: ExclusionMode.Ignore aboveWindows: true focusable: false visible: true color: "#00000000" property int stackWidth: 420 property int menuReservedHeight: 182 readonly property int screenHeight: screen ? screen.height : 1080 readonly property bool topbarOpen: ShellStateManager.shellOpen readonly property int stackOffsetY: topbarOpen ? menuReservedHeight + 24 : 0 readonly property int maxStackHeight: Math.max(120, screenHeight - 24 - stackOffsetY - 24) implicitWidth: stackWidth implicitHeight: stackOffsetY + notificationViewport.height // Input handling decision: the layer itself never grabs focus and is only // as large as the stack. This keeps the container effectively click-through // outside notification bounds and avoids any global pointer/keyboard grabs. mask: Region { item: notificationViewport } NotificationModel { id: notificationModel } Flickable { id: notificationViewport x: 0 y: notificationLayer.stackOffsetY width: notificationLayer.stackWidth height: Math.min(notificationColumn.implicitHeight, notificationLayer.maxStackHeight) clip: true boundsBehavior: Flickable.StopAtBounds contentWidth: width contentHeight: notificationColumn.implicitHeight interactive: contentHeight > height flickableDirection: Flickable.VerticalFlick WheelHandler { acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: function (event) { if (!notificationViewport.interactive) return; const step = event.angleDelta.y / 120 * 60; notificationViewport.contentY = Math.max(0, Math.min(notificationViewport.contentHeight - notificationViewport.height, notificationViewport.contentY - step)); event.accepted = true; } } ScrollBar.vertical: ScrollBar { policy: notificationViewport.interactive ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff width: 6 } Column { id: notificationColumn width: notificationViewport.width spacing: 0 Repeater { model: notificationModel.notifications NotificationCard { notificationId: rowNotificationId notifTitle: rowTitle notifBody: rowBody notifUrgency: rowUrgency notifAppName: rowAppName notifAppIcon: rowAppIcon notifImage: rowImage notifTimeoutMs: rowTimeoutMs notifCritical: rowCritical notifHints: rowHints width: notificationLayer.stackWidth onCloseFinished: function (closedNotificationId, reason) { notificationModel.closeById(closedNotificationId, reason); } } } } } }