From 36807f0ad0b93f2c21ddfe1daf0b6173f5a4d3e0 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 09:12:41 +0200 Subject: [PATCH 1/3] remove hover panel debug logging --- modules/HoverPanel.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/HoverPanel.qml b/modules/HoverPanel.qml index efe889b..e930f67 100644 --- a/modules/HoverPanel.qml +++ b/modules/HoverPanel.qml @@ -132,7 +132,6 @@ PanelWindow { } function dismiss() { - console.log("HoverPanel.dismiss ns=" + panelNamespace + " popup=" + popupMode + " grace=" + _grace + " showPanel=" + showPanel + " pinned=" + _pinned); _pinned = false; showAnim.stop(); hideAnim.start(); @@ -214,7 +213,6 @@ PanelWindow { onTapped: { const p = point.position; const pad = 8; - console.log("HoverPanel tap ns=" + root.panelNamespace + " at=(" + p.x.toFixed(0) + "," + p.y.toFixed(0) + ") container=(" + panelContainer.x + "," + panelContainer.y + " " + panelContainer.width + "x" + panelContainer.height + ")"); if (p.x < panelContainer.x - pad || p.x > panelContainer.x + panelContainer.width + pad || p.y < panelContainer.y - pad || p.y > panelContainer.y + panelContainer.height + pad) root.dismiss(); } From 13372e8055d7132690127ad3e9127b665641280c Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 09:22:15 +0200 Subject: [PATCH 2/3] feat(temperature): add hover panel with history sparkline and gauge --- modules/SystemStats.qml | 3 + modules/Temperature.qml | 244 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 244 insertions(+), 3 deletions(-) diff --git a/modules/SystemStats.qml b/modules/SystemStats.qml index 3582a79..9ee7adf 100644 --- a/modules/SystemStats.qml +++ b/modules/SystemStats.qml @@ -32,6 +32,7 @@ QtObject { // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 + property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min // ── Memory ─────────────────────────────────────────────────────────── property int memPercent: 0 @@ -83,6 +84,8 @@ QtObject { } } else if (ev.type === "temp") { root.tempCelsius = ev.celsius; + const th = root.tempHistory.concat([ev.celsius]); + root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; } else if (ev.type === "mem") { root.memPercent = ev.percent; root.memUsedGb = ev.used_gb; diff --git a/modules/Temperature.qml b/modules/Temperature.qml index d6f9ade..0106632 100644 --- a/modules/Temperature.qml +++ b/modules/Temperature.qml @@ -1,27 +1,265 @@ import QtQuick +import Quickshell import "." as M M.BarSection { id: root spacing: Math.max(1, M.Theme.moduleSpacing - 2) - tooltip: "Temperature: " + M.SystemStats.tempCelsius + "\u00B0C" + tooltip: "" - property color _stateColor: M.SystemStats.tempCelsius > (M.Modules.temperature.hot || 80) ? M.Theme.base09 : M.SystemStats.tempCelsius > (M.Modules.temperature.warm || 60) ? M.Theme.base0A : root.accentColor + readonly property int _warm: M.Modules.temperature.warm || 80 + readonly property int _hot: M.Modules.temperature.hot || 90 + readonly property int _temp: M.SystemStats.tempCelsius + + property color _stateColor: _temp > _hot ? M.Theme.base08 : _temp > _warm ? M.Theme.base0A : root.accentColor Behavior on _stateColor { ColorAnimation { duration: 300 } } + property bool _pinned: false + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false + } + + // Returns a color interpolated green→yellow→red for a given celsius value + function _tempColor(celsius) { + const t = Math.max(0, Math.min(100, celsius)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; + const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; + return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); + } + M.BarIcon { icon: "\uF2C9" color: root._stateColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root._pinned = !root._pinned + } } M.BarLabel { - label: M.SystemStats.tempCelsius + "\u00B0C" + label: root._temp + "\u00B0C" minText: "100\u00B0C" color: root._stateColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root._pinned = !root._pinned + } + } + + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + screen: QsWindow.window?.screen ?? null + anchorItem: root + accentColor: root.accentColor + panelNamespace: "nova-temperature" + panelTitle: "Temperature" + contentWidth: 220 + + // Header — current temp + Item { + width: parent.width + height: 28 + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._temp + "\u00B0C" + color: root._stateColor + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: true + width: _tempSizer.implicitWidth + horizontalAlignment: Text.AlignRight + + Text { + id: _tempSizer + visible: false + text: "100\u00B0C" + font: parent.font + } + } + } + + // Gauge bar (0–100°C), with warm/hot threshold markers + Item { + width: parent.width + height: 16 + + Item { + id: _gaugeBar + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * Math.min(1, root._temp / 100) + height: parent.height + color: root._stateColor + radius: 3 + Behavior on width { + enabled: root._showPanel + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + + // Warm threshold marker + Rectangle { + x: parent.width * (root._warm / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base0A + opacity: 0.6 + } + + // Hot threshold marker + Rectangle { + x: parent.width * (root._hot / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base08 + opacity: 0.6 + } + } + } + + // History sparkline (~10 min @ 4s per sample) + Canvas { + id: _sparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 40 + + property var _hist: M.SystemStats.tempHistory + property color _col: root._stateColor + + on_HistChanged: if (root._showPanel) + requestPaint() + on_ColChanged: if (root._showPanel) + requestPaint() + + Connections { + target: root + function on_ShowPanelChanged() { + if (root._showPanel) + _sparkline.requestPaint(); + } + } + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = _hist; + if (!d.length) + return; + + const maxSamples = 150; + const bw = width / maxSamples; + + // Background tint + ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); + ctx.fillRect(0, 0, width, height); + + // Warm threshold line + const warmY = height - height * (root._warm / 100); + ctx.strokeStyle = M.Theme.base0A.toString(); + ctx.globalAlpha = 0.3; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.moveTo(0, warmY); + ctx.lineTo(width, warmY); + ctx.stroke(); + + // Hot threshold line + const hotY = height - height * (root._hot / 100); + ctx.strokeStyle = M.Theme.base08.toString(); + ctx.beginPath(); + ctx.moveTo(0, hotY); + ctx.lineTo(width, hotY); + ctx.stroke(); + + ctx.setLineDash([]); + ctx.globalAlpha = 1.0; + + // Bars + const offset = maxSamples - d.length; + for (let i = 0; i < d.length; i++) { + const barH = Math.max(1, height * d[i] / 100); + const barColor = d[i] > root._hot ? M.Theme.base08 : d[i] > root._warm ? M.Theme.base0A : _col; + ctx.fillStyle = barColor.toString(); + ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); + } + } + } + + // Threshold labels + Item { + width: parent.width + height: 16 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "warm " + root._warm + "\u00B0 hot " + root._hot + "\u00B0" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 0.5 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "10 min" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + } + } + + Item { + width: 1 + height: 4 + } } } From 69e3711aec0fe8b1ac6da7c03e82d3679bf58106 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 09:31:34 +0200 Subject: [PATCH 3/3] notifcenter: group notifications by app with per-group dismiss --- modules/NotifCenter.qml | 516 +++++++++++++++++++++++++--------------- 1 file changed, 320 insertions(+), 196 deletions(-) diff --git a/modules/NotifCenter.qml b/modules/NotifCenter.qml index f89a2e7..7c3f2f8 100644 --- a/modules/NotifCenter.qml +++ b/modules/NotifCenter.qml @@ -68,25 +68,73 @@ M.HoverPanel { property var _pendingDismissIds: [] - function _cascadeDismiss() { - const list = M.NotifService.list; - if (list.length === 0) - return; - - _pendingDismissIds = list.map(n => n.id); - - const visibles = []; - for (let i = 0; i < list.length; i++) { - const d = notifList.itemAtIndex(i); - if (d && d.modelData?.state !== "dismissing") - visibles.push(d); + // Group notifications by appName, sorted by max urgency desc then most recent time desc + readonly property var _groups: { + const map = {}; + for (const n of M.NotifService.list) { + const key = n.appName || ""; + if (!map[key]) + map[key] = { + appName: key, + appIcon: n.appIcon, + notifs: [], + maxUrgency: 0, + maxTime: 0 + }; + map[key].notifs.push(n); + if (n.urgency > map[key].maxUrgency) + map[key].maxUrgency = n.urgency; + if (n.time > map[key].maxTime) + map[key].maxTime = n.time; } + return Object.values(map).sort((a, b) => { + if (b.maxUrgency !== a.maxUrgency) + return b.maxUrgency - a.maxUrgency; + return b.maxTime - a.maxTime; + }); + } + // Flat model: group header followed by its notifications + readonly property var _flatModel: { + const arr = []; + for (const g of _groups) { + arr.push({ + type: "header", + appName: g.appName, + appIcon: g.appIcon, + count: g.notifs.length + }); + for (const n of g.notifs) + arr.push({ + type: "notif", + data: n + }); + } + return arr; + } + + // Collect visible (non-dismissing) notif delegates, optionally filtered by appName + function _getVisibleNotifDelegates(appName) { + const result = []; + for (let i = 0; i < _flatModel.length; i++) { + const item = _flatModel[i]; + if (item.type !== "notif") + continue; + if (appName !== undefined && item.data.appName !== appName) + continue; + const d = notifList.itemAtIndex(i); + if (d && d._type === "notif" && d._notif?.state !== "dismissing") + result.push(d); + } + return result; + } + + function _startCascade(visibles, ids) { + _pendingDismissIds = ids; if (visibles.length === 0) { _finishCascade(); return; } - for (let i = 0; i < visibles.length; i++) { _cascadeTimer.createObject(menuWindow, { _target: visibles[i], @@ -96,6 +144,20 @@ M.HoverPanel { } } + function _cascadeDismiss() { + if (M.NotifService.list.length === 0) + return; + const ids = M.NotifService.list.map(n => n.id); + _startCascade(_getVisibleNotifDelegates(), ids); + } + + function _cascadeGroupDismiss(appName) { + const ids = M.NotifService.list.filter(n => n.appName === appName).map(n => n.id); + if (ids.length === 0) + return; + _startCascade(_getVisibleNotifDelegates(appName), ids); + } + function _finishCascade() { const ids = _pendingDismissIds; _pendingDismissIds = []; @@ -113,10 +175,8 @@ M.HoverPanel { onTriggered: { if (_target && _target.dismissVisualOnly) _target.dismissVisualOnly(); - if (_isLast) { - // Wait for the last animation to finish, then bulk dismiss + if (_isLast) _bulkTimer.createObject(menuWindow, {}); - } destroy(); } } @@ -148,33 +208,36 @@ M.HoverPanel { height: Math.min(contentHeight, 60 * (M.Modules.notifications.maxVisible || 10)) clip: true boundsBehavior: Flickable.StopAtBounds - model: M.NotifService.list + model: menuWindow._flatModel delegate: Item { - id: notifItem + id: notifDelegate required property var modelData required property int index + readonly property string _type: modelData.type + readonly property var _notif: _type === "notif" ? modelData.data : null + width: menuWindow.contentWidth height: _targetHeight * _heightScale - opacity: 0 clip: true + opacity: 0 - readonly property real _targetHeight: notifContent.height + 12 + readonly property real _targetHeight: _type === "header" ? 28 : (notifContent.implicitHeight + 12) property real _heightScale: 1 property bool _skipDismiss: false function dismiss() { - if (notifItem.modelData.state === "dismissing") + if (_type !== "notif" || _notif.state === "dismissing") return; - notifItem.modelData.beginDismiss(); + _notif.beginDismiss(); _dismissAnim.start(); } function dismissVisualOnly() { - if (notifItem.modelData.state === "dismissing") + if (_type !== "notif" || _notif.state === "dismissing") return; - notifItem.modelData.beginDismiss(); + _notif.beginDismiss(); _skipDismiss = true; _dismissAnim.start(); } @@ -183,194 +246,264 @@ M.HoverPanel { NumberAnimation { id: fadeIn - target: notifItem + target: notifDelegate property: "opacity" to: 1 duration: 150 easing.type: Easing.OutCubic } - Rectangle { + // ---- Group header ---- + Item { + visible: notifDelegate._type === "header" anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: notifArea.containsMouse ? M.Theme.base02 : "transparent" - radius: M.Theme.radius - Rectangle { - visible: (notifItem.modelData.hints?.value ?? -1) >= 0 - anchors.top: parent.top - anchors.bottom: parent.bottom + Image { + id: _headerIcon anchors.left: parent.left - width: parent.width * Math.min(1, Math.max(0, (notifItem.modelData.hints?.value ?? 0) / 100)) - color: M.Theme.base02 - radius: parent.radius - - Behavior on width { - NumberAnimation { - duration: 200 - } - } - } - } - - // Urgency accent - Rectangle { - anchors.left: parent.left - anchors.leftMargin: 4 - anchors.top: parent.top - anchors.topMargin: 4 - anchors.bottom: parent.bottom - anchors.bottomMargin: 4 - width: 2 - radius: 1 - color: { - const u = notifItem.modelData.urgency; - return u === NotificationUrgency.Critical ? M.Theme.base08 : u === NotificationUrgency.Low ? M.Theme.base04 : M.Theme.base0D; - } - } - - Image { - id: ncIcon - anchors.left: parent.left - anchors.leftMargin: 14 - anchors.top: parent.top - anchors.topMargin: 6 - width: 24 - height: 24 - source: { - const img = notifItem.modelData.image; - if (img) - return img; - const ic = notifItem.modelData.appIcon; - if (!ic) - return ""; - return ic.startsWith("/") ? ic : Quickshell.iconPath(ic, "dialog-information"); - } - visible: status === Image.Ready - fillMode: Image.PreserveAspectFit - sourceSize: Qt.size(24, 24) - asynchronous: true - } - - Column { - id: notifContent - anchors.left: ncIcon.visible ? ncIcon.right : parent.left - anchors.right: dismissBtn.left - anchors.top: parent.top - anchors.leftMargin: ncIcon.visible ? 6 : 14 - anchors.topMargin: 6 - spacing: 1 - - // App + time - Row { - width: parent.width - Text { - text: notifItem.modelData.appName || "Notification" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - ageText.width - 4 - } - Text { - id: ageText - text: { - const diff = Math.floor((Date.now() - notifItem.modelData.time) / 60000); - if (diff < 1) - return "now"; - if (diff < 60) - return diff + "m"; - if (diff < 1440) - return Math.floor(diff / 60) + "h"; - return Math.floor(diff / 1440) + "d"; - } - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + width: M.Theme.fontSize + 2 + height: M.Theme.fontSize + 2 + source: { + if (notifDelegate._type !== "header") + return ""; + const ic = notifDelegate.modelData.appIcon; + if (!ic) + return ""; + return ic.startsWith("/") ? ic : Quickshell.iconPath(ic, "dialog-information"); } + visible: status === Image.Ready + fillMode: Image.PreserveAspectFit + sourceSize: Qt.size(M.Theme.fontSize + 2, M.Theme.fontSize + 2) + asynchronous: true } Text { - width: parent.width - text: notifItem.modelData.summary || "" + anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left + anchors.leftMargin: _headerIcon.visible ? 6 : 10 + anchors.right: _groupDismissBtn.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : "" color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize + font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily font.bold: true elide: Text.ElideRight } Text { - width: parent.width - text: notifItem.modelData.body || "" - color: M.Theme.base04 + id: _groupDismissBtn + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.verticalCenter: parent.verticalCenter + text: "\uF1F8" + color: _groupDismissArea.containsMouse ? M.Theme.base08 : M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text !== "" - } + font.family: M.Theme.iconFontFamily - // Actions - Row { - spacing: 4 - visible: notifItem.modelData.actions && notifItem.modelData.actions.length > 0 - - Repeater { - model: notifItem.modelData.actions || [] - delegate: Rectangle { - required property var modelData - width: actText.implicitWidth + 10 - height: actText.implicitHeight + 4 - radius: M.Theme.radius - color: actArea.containsMouse ? M.Theme.base02 : "transparent" - border.color: M.Theme.base03 - border.width: 1 - - Text { - id: actText - anchors.centerIn: parent - text: parent.modelData.text - color: M.Theme.base0D - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - MouseArea { - id: actArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - parent.modelData.invoke(); - M.NotifService.dismiss(notifItem.modelData.id); - } - } + MouseArea { + id: _groupDismissArea + anchors.fill: parent + anchors.margins: -4 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (notifDelegate._type === "header") + menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName); } } } } - // Dismiss button - Text { - id: dismissBtn - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.top: parent.top - anchors.topMargin: 8 - text: "\uF00D" - color: dismissArea.containsMouse ? M.Theme.base08 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.iconFontFamily + // ---- Individual notification ---- + Item { + id: _notifRow + visible: notifDelegate._type === "notif" + anchors.fill: parent - MouseArea { - id: dismissArea + // Hover + progress bar background + Rectangle { anchors.fill: parent - anchors.margins: -4 + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: notifArea.containsMouse ? M.Theme.base02 : "transparent" + radius: M.Theme.radius + + Rectangle { + visible: (notifDelegate._notif?.hints?.value ?? -1) >= 0 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.width * Math.min(1, Math.max(0, (notifDelegate._notif?.hints?.value ?? 0) / 100)) + color: M.Theme.base02 + radius: parent.radius + + Behavior on width { + NumberAnimation { + duration: 200 + } + } + } + } + + // Urgency accent bar + Rectangle { + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.top: parent.top + anchors.topMargin: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: 4 + width: 2 + radius: 1 + color: { + const u = notifDelegate._notif?.urgency ?? NotificationUrgency.Normal; + return u === NotificationUrgency.Critical ? M.Theme.base08 : u === NotificationUrgency.Low ? M.Theme.base04 : M.Theme.base0D; + } + } + + // App icon / image + Image { + id: ncIcon + anchors.left: parent.left + anchors.leftMargin: 14 + anchors.top: parent.top + anchors.topMargin: 6 + width: 24 + height: 24 + source: { + if (notifDelegate._type !== "notif") + return ""; + const img = notifDelegate._notif?.image; + if (img) + return img; + const ic = notifDelegate._notif?.appIcon; + if (!ic) + return ""; + return ic.startsWith("/") ? ic : Quickshell.iconPath(ic, "dialog-information"); + } + visible: status === Image.Ready + fillMode: Image.PreserveAspectFit + sourceSize: Qt.size(24, 24) + asynchronous: true + } + + Column { + id: notifContent + anchors.left: ncIcon.visible ? ncIcon.right : parent.left + anchors.right: dismissBtn.left + anchors.top: parent.top + anchors.leftMargin: ncIcon.visible ? 6 : 14 + anchors.topMargin: 6 + spacing: 1 + + // Summary + time on the same row + Row { + width: parent.width + + Text { + text: notifDelegate._notif?.summary ?? "" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight + width: parent.width - _notifTime.implicitWidth - 4 + } + + Text { + id: _notifTime + text: notifDelegate._notif?.timeStr ?? "" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Text { + width: parent.width + text: notifDelegate._notif?.body ?? "" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + visible: text !== "" + } + + // Actions + Row { + spacing: 4 + visible: notifDelegate._notif?.actions && notifDelegate._notif.actions.length > 0 + + Repeater { + model: notifDelegate._notif?.actions ?? [] + delegate: Rectangle { + required property var modelData + width: actText.implicitWidth + 10 + height: actText.implicitHeight + 4 + radius: M.Theme.radius + color: actArea.containsMouse ? M.Theme.base02 : "transparent" + border.color: M.Theme.base03 + border.width: 1 + + Text { + id: actText + anchors.centerIn: parent + text: parent.modelData.text + color: M.Theme.base0D + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + MouseArea { + id: actArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + parent.modelData.invoke(); + M.NotifService.dismiss(notifDelegate._notif.id); + } + } + } + } + } + } + + // Per-notification dismiss button + Text { + id: dismissBtn + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.top: parent.top + anchors.topMargin: 8 + text: "\uF00D" + color: dismissArea.containsMouse ? M.Theme.base08 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.iconFontFamily + + MouseArea { + id: dismissArea + anchors.fill: parent + anchors.margins: -4 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: notifDelegate.dismiss() + } + } + + // Right-click to dismiss + MouseArea { + id: notifArea + anchors.fill: parent + z: -1 hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: _dismissAnim.start() + acceptedButtons: Qt.RightButton + onClicked: notifDelegate.dismiss() } } @@ -378,14 +511,14 @@ M.HoverPanel { id: _dismissAnim ParallelAnimation { NumberAnimation { - target: notifItem + target: notifDelegate property: "x" to: menuWindow.contentWidth duration: 200 easing.type: Easing.InCubic } NumberAnimation { - target: notifItem + target: notifDelegate property: "opacity" to: 0 duration: 200 @@ -393,7 +526,7 @@ M.HoverPanel { } } NumberAnimation { - target: notifItem + target: notifDelegate property: "_heightScale" to: 0 duration: 150 @@ -401,20 +534,11 @@ M.HoverPanel { } ScriptAction { script: { - if (!notifItem._skipDismiss) - M.NotifService.dismiss(notifItem.modelData.id); + if (notifDelegate._notif && !notifDelegate._skipDismiss) + M.NotifService.dismiss(notifDelegate._notif.id); } } } - - MouseArea { - id: notifArea - anchors.fill: parent - z: -1 - hoverEnabled: true - acceptedButtons: Qt.RightButton - onClicked: _dismissAnim.start() - } } }