// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick // One balloon: icon at the left, app, summary and body, an X, and the click // targets. The body is markup, which is why the daemon advertises body-markup. Rectangle { id: b // Neither property is `required` on purpose. A required property makes // QML create the delegate in its own context, where `modelData` and // `index` are not defined, so the Repeater's `notification: modelData` // silently arrives undefined and every binding on it fails. property var notification property double now: 0 // Driven by the shell's tick. The balloon drops itself once its expiry // passes, or while a suppression says so: the drawer and snooze withhold // everything, DND withholds low and normal but still shows critical. The // drawer lists a notification DND chose not to pop, because a list the // user opened is not an interruption. visible: { if (Notify.drawerOpen) return false; if (Notify.snoozeUntil > b.now) return false; if (Status.dnd && b.notification.urgency !== "critical") return false; if (b.notification.expires !== 0 && b.now >= b.notification.expires) return false; return true; } width: parent ? parent.width : 340 implicitHeight: texts.implicitHeight + 20 radius: 10 color: Qt.alpha(Theme.base, 0.82) border.width: 1 border.color: Qt.alpha(Theme.text, 0.12) // The background click target is declared first so the X, declared later, // sits above it and wins its corner. MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: mouse => { if (mouse.button === Qt.RightButton) { Notify.closeAll(); return; } const acts = b.notification.actions || []; const inert = b.notification.expires !== 0 && Date.now() >= b.notification.expires; if (acts.length > 0 && !inert) Notify.actions(b.notification.id, acts); else Notify.close(b.notification.id); } } Image { id: icon visible: b.notification.icon !== "" && b.notification.icon !== undefined anchors { left: parent.left; top: parent.top; margins: 10 } width: 32 height: 32 source: visible ? "file://" + b.notification.icon : "" sourceSize { width: 64; height: 64 } } Text { id: close anchors { right: parent.right; top: parent.top; margins: 6 } width: 20 height: 20 text: "\uf00d" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font { family: Theme.iconFamily; pixelSize: 11 } color: closeArea.containsMouse ? Theme.red : Theme.subtext MouseArea { id: closeArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: Notify.close(b.notification.id) } } Column { id: texts anchors { left: icon.visible ? icon.right : parent.left leftMargin: 10 right: parent.right rightMargin: 10 top: parent.top topMargin: 10 } spacing: 2 Text { width: parent.width text: b.notification.app || "" elide: Text.ElideRight font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } color: Theme.subtext } Text { width: parent.width text: b.notification.summary || "" elide: Text.ElideRight font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.text } Text { width: parent.width visible: text !== "" text: b.notification.body || "" textFormat: Text.RichText wrapMode: Text.WordWrap maximumLineCount: 3 elide: Text.ElideRight font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.subtext } } }