Files
DeltaruneQuickshell/Shell/Notifications/NotificationLayer.qml
2026-03-02 19:28:15 +02:00

117 lines
3.9 KiB
QML

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