import QtQml import QtQml.Models import QtQuick import Quickshell import Quickshell.Widgets import Quickshell.Wayland import qs.Services import QtQuick.Layouts import Quickshell.Services.Notifications Scope { readonly property ShellScreen activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null Instantiator { id: notifsRepeater model: ScriptModel { values: NotificationManager.active ? [] : NotificationManager.groups } delegate: PanelWindow { id: notifWindow required property var modelData required property var index property int activeIx: modelData.length - 1 onModelDataChanged: { notifWindow.activeIx = modelData.length - 1; } property color textColor: { if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low) return "#ff999999"; return "white"; } property color backgroundColor: { if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical) return "#dd900000"; return "black"; } anchors { right: true top: true } readonly property real spaceAbove: { var res = 0; for (let i = 0; i < notifWindow.index; i++) { (_ => {})(notifsRepeater.objectAt(i).modelData); res += notifsRepeater.objectAt(i).height + 8; } return res; } margins { right: 26 + 8 top: 8 + spaceAbove } color: "transparent" implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16 implicitWidth: 400 WrapperMouseArea { enabled: true anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { for (const notif of notifWindow.modelData) notif.dismiss(); } property real angleRem: 0 property real sensitivity: 1 / 120 onWheel: event => { angleRem += event.angleDelta.y; const d = Math.round(angleRem * sensitivity); angleRem -= d / sensitivity; notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1); } Rectangle { color: notifWindow.backgroundColor anchors.fill: parent border { color: Qt.hsla(195/360, 1, 0.45, 1) width: 2 } GridLayout { id: notifLayout width: 400 - 16 anchors.fill: parent anchors.margins: 8 columnSpacing: 8 rowSpacing: 8 columns: notifImage.visible ? 3 : 2 rows: { var res = 1; if (notifBody.visible) res += 1; if (notifActions.visible) res += 1; if (notifTime.visible) res += 1; return res; } Text { id: notifCount visible: notifWindow.modelData?.length > 1 ?? false text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}` font.pointSize: 10 font.family: "Fira Sans" font.bold: true font.features: { "tnum": 1 } color: notifWindow.textColor maximumLineCount: 1 Layout.fillWidth: false Layout.row: 0 Layout.column: notifImage.visible ? 1 : 0 } Text { id: notifSummary text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? "" font.pointSize: 10 font.family: "Fira Sans" font.italic: true color: notifWindow.textColor maximumLineCount: 1 elide: Text.ElideRight Layout.fillWidth: true Layout.row: 0 Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0) Layout.columnSpan: notifCount.visible ? 1 : 2 } Image { id: notifImage visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false onStatusChanged: { if (notifImage.status == Image.Error) notifImage.visible = false; } source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? "" fillMode: Image.PreserveAspectFit asynchronous: true smooth: true mipmap: true Layout.maximumWidth: 50 Layout.column: 0 Layout.row: 0 Layout.fillHeight: true Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) } Text { id: notifBody visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" textFormat: Text.RichText wrapMode: Text.Wrap font.pointSize: 10 font.family: "Fira Sans" color: notifWindow.textColor Layout.fillWidth: true Layout.row: 1 Layout.column: notifImage.visible ? 1 : 0 Layout.columnSpan: notifCount.visible ? 2 : 1 } Text { id: notifTime Connections { target: NotificationManager.clock function onDateChanged() { notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); } } visible: notifTime.text && notifTime.text !== "now" text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) font.pointSize: 8 font.family: "Fira Sans" font.italic: true color: "#555" maximumLineCount: 1 horizontalAlignment: Text.AlignRight Layout.fillWidth: true Layout.row: notifBody.visible ? 2 : 1 Layout.column: notifImage.visible ? 1 : 0 Layout.columnSpan: 2 } RowLayout { id: notifActions visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false spacing: 8 uniformCellSizes: true width: 400 - 16 Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) Layout.column: 0 Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) Repeater { model: ScriptModel { values: notifWindow.modelData?.[notifWindow.activeIx]?.actions } delegate: WrapperMouseArea { id: actionMouseArea required property var modelData height: actionLabelWrapper.implicitHeight Layout.fillWidth: true Layout.horizontalStretchFactor: 1 hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: actionMouseArea.modelData?.invoke() Rectangle { anchors.fill: parent color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent" border { width: 2 color: "#20ffffff" } WrapperItem { id: actionLabelWrapper margin: 8 anchors.centerIn: parent RowLayout { id: actionLabelLayout spacing: 8 IconImage { id: actionIcon visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons onStatusChanged: { if (actionIcon.status == Image.Error) actionIcon.visible = false; } implicitSize: 16 source: { if (!actionIcon.visible) return ""; let icon = actionMouseArea.modelData?.identifier ?? "" if (icon.includes("?path=")) { const split = icon.split("?path=") if (split.length !== 2) return icon const name = split[0] const path = split[1] const fileName = name.substring( name.lastIndexOf("/") + 1) return `file://${path}/${fileName}` } return icon } asynchronous: true smooth: true mipmap: true Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter } Text { id: actionLabel visible: actionMouseArea.modelData?.text ?? false text: actionMouseArea.modelData?.text ?? "" font.pointSize: 10 font.family: "Fira Sans" color: notifWindow.textColor maximumLineCount: 1 elide: Text.ElideRight } } } } } } } } } } } } }