Files
quickshell/notifications/NotificationPopups.qml

256 lines
8.5 KiB
QML

pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import ".."
// Notification toast overlay.
// Toasts slide in from the right, stack downward, and auto-dismiss.
// Urgency colouring: Low → fgDim stripe, Normal → accent stripe, Critical → critical border + stripe.
PanelWindow {
id: root
anchors {
top: true
right: true
}
exclusionMode: ExclusionMode.Ignore
color: "transparent"
implicitWidth: 356 // 340 toast + 8 left + 8 right margin
implicitHeight: Math.max(1, (clearAllBtn.visible ? clearAllBtn.height + 6 : 0) + notifList.contentHeight + 16)
// ── Notification server ──────────────────────────────────────────────────
NotificationServer {
id: server
onNotification: function (notif) {
notifModel.insert(0, {
"nid": notif.id,
"app": notif.appName,
"icon": notif.appIcon,
"summary": notif.summary !== "" ? notif.summary : notif.appName,
"body": notif.body,
"urgency": notif.urgency
});
}
}
ListModel {
id: notifModel
}
function removeById(nid) {
for (let i = 0; i < notifModel.count; i++) {
if (notifModel.get(i).nid === nid) {
notifModel.remove(i);
return;
}
}
}
// ── Toast list ───────────────────────────────────────────────────────────
// ── Clear all button (visible when 2+ notifications) ────────────────────
Rectangle {
id: clearAllBtn
visible: notifModel.count >= 2
anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 }
width: 340
height: 26
color: clearHover.hovered ? Colors.surface : Colors.bgDark
border.color: Colors.surface
border.width: 1
radius: 6
Text {
anchors.centerIn: parent
text: "Clear all (" + notifModel.count + ")"
color: Colors.fgDim
font.pixelSize: 12
}
HoverHandler { id: clearHover }
TapHandler { onTapped: notifModel.clear() }
}
ListView {
id: notifList
anchors {
top: clearAllBtn.visible ? clearAllBtn.bottom : parent.top
right: parent.right
topMargin: clearAllBtn.visible ? 6 : 8
rightMargin: 8
}
width: 340
height: contentHeight
model: notifModel
spacing: 6
interactive: false
add: Transition {
NumberAnimation {
property: "x"
from: 356
to: 0
duration: 220
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 180
}
}
remove: Transition {
NumberAnimation {
property: "opacity"
to: 0
duration: 130
easing.type: Easing.InCubic
}
NumberAnimation {
property: "x"
to: 356
duration: 180
easing.type: Easing.InCubic
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: 220
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 220
}
}
delegate: Item {
id: toast
required property int index
required property int nid
required property string app
required property string icon
required property string summary
required property string body
required property int urgency
width: 340
height: toastBg.height
// ── Background card ──────────────────────────────────────────────
Rectangle {
id: toastBg
width: parent.width
height: cardContent.implicitHeight + 20
color: Colors.bgDark
border.color: toast.urgency === 2 ? Colors.critical : Colors.surface
border.width: 1
radius: 8
// Urgency stripe on left edge
Rectangle {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
topMargin: 1
bottomMargin: 1
leftMargin: 1
}
width: 3
radius: 8
color: toast.urgency === 0 ? Colors.fgDim : toast.urgency === 2 ? Colors.critical : Colors.accent
}
ColumnLayout {
id: cardContent
anchors {
left: parent.left
right: parent.right
top: parent.top
leftMargin: 14
rightMargin: 10
topMargin: 10
}
spacing: 4
// ── Header row: icon + summary + app name + close button ─
RowLayout {
Layout.fillWidth: true
spacing: 6
// App icon (shown when available)
IconImage {
visible: toast.icon !== ""
source: toast.icon
width: 16
height: 16
mipmap: true
}
Text {
text: toast.summary
color: toast.urgency === 2 ? Colors.critical : toast.urgency === 0 ? Colors.fgDim : Colors.accent
font.pixelSize: 13
font.weight: Font.Medium
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: toast.app
color: Colors.fgDim
font.pixelSize: 11
elide: Text.ElideRight
Layout.maximumWidth: 90
}
Rectangle {
width: 18
height: 18
radius: 4
color: closeHover.hovered ? Colors.surface : "transparent"
HoverHandler {
id: closeHover
}
TapHandler {
onTapped: root.removeById(toast.nid)
}
Text {
anchors.centerIn: parent
text: "✕"
color: Colors.fgDim
font.pixelSize: 11
}
}
}
// ── Body text ────────────────────────────────────────────
Text {
visible: toast.body !== ""
text: toast.body
color: Colors.fg
font.pixelSize: 12
Layout.fillWidth: true
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
bottomPadding: 2
}
}
}
}
}
}