import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import ".." PanelWindow { id: notificationLayer anchors { top: true right: true } margins { top: theme.panelMarginTop right: theme.panelMarginRight } // 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" NotificationTheme { id: theme } property int stackWidth: theme.stackWidth property int menuReservedHeight: theme.topbarReservedHeight readonly property bool stackPreviewEnabled: Boolean(ShellStateManager.global("notifications.stackPreviewEnabled", true)) readonly property int screenHeight: screen ? screen.height : 1080 readonly property bool topbarOpen: ShellStateManager.shellOpen readonly property int stackOffsetY: topbarOpen ? menuReservedHeight + theme.panelMarginTop : 0 readonly property int maxStackHeight: Math.max(120, screenHeight - theme.panelMarginTop - stackOffsetY - theme.panelMarginTop) 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.groupedNotifications NotificationGroup { theme: theme groupData: modelData expanded: notificationModel.groupIsExpanded(modelData.groupKey) peekEnabled: notificationLayer.stackPreviewEnabled width: notificationLayer.stackWidth onExpandRequested: function (groupKey, shouldExpand) { notificationModel.setGroupExpanded(groupKey, shouldExpand); } onCloseFinished: function (closedNotificationId, reason) { notificationModel.closeById(closedNotificationId, reason); } } } } } }