80 lines
2.9 KiB
QML
80 lines
2.9 KiB
QML
// The banner stack: newest at the top, tucked under the bar on the right.
|
|
//
|
|
// The window is exactly as wide and tall as the stack, so everything outside
|
|
// it stays clickable; the mask trims it further to the cards themselves.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import qs.config
|
|
import qs.services
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
// Whether a notification is allowed to be a banner is decided once, in
|
|
// Notifs.handleNotification: a focus mode's allow-list and the
|
|
// critical-breakthrough switch are both exceptions to Do Not Disturb, and
|
|
// anything they let past is already in `popups`. Re-testing doNotDisturb
|
|
// here would override that three-way decision and leave an allowed app
|
|
// chiming at an empty screen.
|
|
visible: Notifs.popups.length > 0
|
|
color: "transparent"
|
|
|
|
anchors.top: true
|
|
anchors.right: true
|
|
// The gap ALONE, not the bar height plus the gap. exclusiveZone 0 means
|
|
// "reserve nothing, but respect what others reserved", so this surface
|
|
// already begins below the bar's zone -- adding the bar height here counted
|
|
// it twice and left the surface floating 48px under the bar instead of 12.
|
|
margins.top: Theme.barGap * 2
|
|
margins.right: Theme.barSideMargin
|
|
exclusiveZone: 0
|
|
|
|
implicitWidth: 400
|
|
implicitHeight: Math.max(1, stack.implicitHeight)
|
|
|
|
WlrLayershell.namespace: "qs-notifications"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
|
|
// Normally inert so banners never steal a keystroke. An open inline reply
|
|
// is the one case where the surface has to be typeable.
|
|
WlrLayershell.keyboardFocus: root.replyFocused ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
|
|
|
property bool replyFocused: false
|
|
|
|
mask: Region {
|
|
item: stack
|
|
}
|
|
|
|
Column {
|
|
id: stack
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
spacing: Theme.itemSpacing
|
|
|
|
Repeater {
|
|
// Notifs.popups.slice() is a fresh array on every change (a new
|
|
// arrival, a dismissal, a sibling toast timing out). Handing that
|
|
// straight to Repeater would reset the model and rebuild every
|
|
// delegate each time, blowing away whichever toast has a reply
|
|
// field mid-typing. ScriptModel diffs by object identity
|
|
// (Notification instances are unique QObjects), so only genuinely
|
|
// added/removed notifications add/remove delegates — unrelated
|
|
// toasts, and their slide-in animations, are untouched.
|
|
model: ScriptModel {
|
|
values: Notifs.popups.slice(0, Settings.maxVisibleToasts)
|
|
}
|
|
|
|
Toast {
|
|
required property var modelData
|
|
|
|
width: stack.width
|
|
notification: modelData
|
|
onReplyFocusChanged: focused => root.replyFocused = focused
|
|
}
|
|
}
|
|
}
|
|
}
|