Files
DeltaruneQuickshell/Shell/Notifications/NotificationLayer.qml
2026-03-01 10:37:07 +02:00

114 lines
3.6 KiB
QML

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);
}
}
}
}
}
}