From b59eb012ba7a04c67b9c79fac5cba32739f62a09 Mon Sep 17 00:00:00 2001 From: Hannes Date: Tue, 1 Sep 2026 23:59:36 +0200 Subject: [PATCH] feat: refactor: add helpers for summary/body placement in NotificationBody --- config/Config.qml | 2 +- modules/notification/NotificationBody.qml | 94 +++-- modules/notification/NotificationHistory.qml | 333 +++++++++--------- .../notificationServer/NotificationServer.qml | 2 +- 4 files changed, 237 insertions(+), 194 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index 342be08..a9d7f3e 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -81,5 +81,5 @@ QtObject { return maxWidth; } - property string debug: "Notification" + property string debug: "" } \ No newline at end of file diff --git a/modules/notification/NotificationBody.qml b/modules/notification/NotificationBody.qml index 6e4762b..2486125 100644 --- a/modules/notification/NotificationBody.qml +++ b/modules/notification/NotificationBody.qml @@ -16,6 +16,14 @@ Rectangle { // When used in history overview we don't want auto-timeout or click-to-close property bool isHistory: false + // Helpers to handle summary/body placement + // - "Message:" title is always static (fixed bug where appName replaced it when image existed) + // - If notification only has summary and no body, show summary on the right side + property bool hasBody: notification.body !== undefined && notification.body !== null && String(notification.body).trim() !== "" + property bool hasSummary: notification.summary !== undefined && notification.summary !== null && String(notification.summary).trim() !== "" + property bool showSummaryOnLeft: hasSummary && hasBody + property bool showSummaryOnRight: hasSummary && !hasBody + property real duration: isHistory ? 0 : ( notification.expireTimeout > 0 ? notification.expireTimeout*1000 : urgency ? urgency * 2000 : @@ -26,12 +34,13 @@ Rectangle { implicitWidth: screen.width/7 // implicitHeight: screen.height/7 - implicitHeight: contentRowRectangle.height+(actionRowRectangle.visible?actionRowRectangle.height:0)+(inlineReplyRectangle.visible?inlineReplyRectangle.height:0)+(progressBar.visible?progressBar.height:0) + implicitHeight: contentRowRectangle.height+(actionRowRectangle.visible?actionRowRectangle.height:0)+(inlineReplyRectangle.visible?inlineReplyRectangle.height:0)+(progressBar.visible?progressBar +.height:0) color: textBGColor border.width: Config.border_width border.color: textColor - radius: isHistory ? Config.radius : 0 + radius: isHistory ? Config.small_radius : 0 clip: isHistory property color textColor: urgency != 3 ? Colors.primary : Colors.tertiary @@ -73,7 +82,6 @@ Rectangle { height: Math.max(iconRectangle.height, contentRectangle.height)+contentRow.anchors.margins*2 width: parent.width color: "transparent" - radius: isHistory ? Config.radius : 0 border.width: Config.border_width border.color: textColor @@ -83,26 +91,26 @@ Rectangle { anchors.right: parent.right anchors.margins: Config.spacing - width: closeIconText.implicitHeight + Config.spacing - height: closeIconText.implicitHeight + Config.spacing + width: closeIconText.implicitHeight + height: closeIconText.implicitHeight hoverEnabled: true cursorShape: Qt.PointingHandCursor Rectangle { anchors.fill: parent radius: Config.radius - color: parent.containsMouse ? Colors.errorContainer : "transparent" + color: parent.containsMouse ? "red" : "transparent" border.width: Config.border_width - border.color: parent.containsMouse ? Colors.error : textColor + border.color: parent.containsMouse ? "white" : textColor Text { id: closeIconText anchors.centerIn: parent - text: "✕"; // or "✕" + text: ""; // or "✕" font.family: Config.font - font.pixelSize: Config.screen_height_to_font_small(screen.height) - color: parent.parent.containsMouse ? Colors.error_fg : textColor + font.pixelSize: Config.screen_height_to_font_medium(screen.height) + color: parent.parent.containsMouse ? "white" : textColor } } onClicked: function(mouse){ @@ -154,10 +162,10 @@ Rectangle { Text { id: summaryText - // height: implicitHeight + font.pixelSize + Config.spacing - text: root.notification.summary - visible: root.notification.summary ? true : false + // Only show summary under icon when both summary and body exist. + // If only summary exists (no body), it will be shown on the right instead. + visible: root.showSummaryOnLeft width: iconColumn.width font.family: Config.font font.pixelSize: Config.screen_height_to_font_small(screen.height) @@ -173,37 +181,54 @@ Rectangle { Rectangle{ id: contentRectangle width: root.width - iconColumn.width - contentRow.spacing - contentRow.anchors.margins*2 - height: iconRectangle.height + height: Math.max(iconRectangle.height, contentBodyColumn.implicitHeight) color: "transparent" Column { id: contentBodyColumn width: parent.width - height: Math.max(childrenRect.height, iconRectangle.height) spacing: Config.spacing - Text { - id: bodyTitleText - text: { - if (notification.image) return notification.appName.charAt(0).toUpperCase() + notification.appName.slice(1) - return "Message:" + Item { + id: titleTextItem + width: parent.width-closingMouseArea.width + height: closingMouseArea.height + Text { + id: bodyTitleText + anchors.centerIn: parent + text: (!notification.appName || !notification.appName.split(".").pop() + ? "Message" + : notification.appName.split(".").pop().charAt(0).toUpperCase() + + notification.appName.split(".").pop().slice(1)) + font.family: Config.font + font.bold: true + font.pixelSize: Config.screen_height_to_font_medium(screen.height) + color: textColor } - font.family: Config.font - font.bold: true - font.pixelSize: closingMouseArea.height //Config.screen_height_to_font_small(screen.height) - color: textColor } + Rectangle { + width: parent.width + height: Math.max(iconRectangle.height - titleTextItem.height - contentBodyColumn.spacing, 64) + color: textBGColor + radius: Config.screen_big_radius_fun(screen.height) + border.width: Config.border_width + border.color: textColor + Item{ + anchors.fill: parent + anchors.margins: Config.spacing + Text { + id: bodyText + // If only summary exists (no body), show summary on the right instead of left + text: root.hasBody ? root.notification.body : (root.showSummaryOnRight ? root.notification.summary : "") + visible: root.hasBody || root.showSummaryOnRight - Text { - id: bodyText - text: root.notification.body - visible: root.notification.body ? true : false - - width: contentBodyColumn.width - font.family: Config.font - font.pixelSize: Config.screen_height_to_font_tiny(screen.height) - color: textColor - wrapMode: Text.Wrap + width: parent.width - 2*parent.anchors.margins + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(screen.height) + color: textColor + wrapMode: Text.Wrap + } + } } } } @@ -396,7 +421,6 @@ Rectangle { to: 0.0 duration: root.duration paused: (mouseArea.containsMouse || closingMouseArea.containsMouse || inlineReplyButtonMouseArea.containsMouse || root._hoveredActionCount > 0 || inlineReplyTextField.hovered || inlineReplyTextField.activeFocus) && !isHistory - onFinished: root.closeNotification(false) } } diff --git a/modules/notification/NotificationHistory.qml b/modules/notification/NotificationHistory.qml index 0d2e1f7..3c1dd0d 100644 --- a/modules/notification/NotificationHistory.qml +++ b/modules/notification/NotificationHistory.qml @@ -10,7 +10,7 @@ import "./notificationServer" PanelWindow { id: root implicitWidth: screen.width/5 - implicitHeight: screen.height - Config.barHeight(screen.height)*2 + implicitHeight: screen.height - Config.barHeight(screen.height) color: "transparent" visible: true focusable: true @@ -101,14 +101,14 @@ PanelWindow { // text: NotificationS.trackedNotifications.values[0] // } - Component.onCompleted: { - for (var notification in NotificationS.trackedNotifications.values) { - console.log("[NotificationHistory]: " + notification.appName + " Notifications") - } - for (var i =0; i < NotificationS.notificationNum; i++) { - console.log("[NotificationHistory]: " + NotificationS.trackedNotifications.values[i].appName + " Notifications") - } - } + // Component.onCompleted: { + // for (var notification in NotificationS.trackedNotifications.values) { + // console.log("[NotificationHistory]: " + notification.appName + " Notifications") + // } + // for (var i =0; i < NotificationS.notificationNum; i++) { + // console.log("[NotificationHistory]: " + NotificationS.trackedNotifications.values[i].appName + " Notifications") + // } + // } Column{ id: bgColumn @@ -119,7 +119,7 @@ PanelWindow { Rectangle { id: topBar width: parent.width - height: bigFontSize*1.25 + height: bigFontSize*2.5 color: textBGColor radius: Config.screen_big_radius_fun(screen.height) border.width: Config.border_width @@ -135,20 +135,28 @@ PanelWindow { Text{ text: "Notifications:" font.family: Config.font - font.pixelSize: bigFontSize + font.pixelSize: topBar.height*0.75 color: textColor } } + // Row{ + // id: backgroundCenter + // anchors.centerIn: parent + // spacing: Config.screen_small_bar_spacing_fun(screen.height) + // } + Row{ - id: backgroundCenter - anchors.centerIn: parent - spacing: Config.screen_small_bar_spacing_fun(screen.height) + id: backgroundRight + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: Config.screen_spacing_fun(screen.height) + spacing: Config.screen_spacing_fun(screen.height) Text{ text: NotificationS.soundEnabled ? "󰂚" : "󰂛" font.family: Config.font - font.pixelSize: bigFontSize + font.pixelSize: topBar.height*0.75 color: textColor } @@ -156,7 +164,7 @@ PanelWindow { id: notificationVolumeSwitch implicitWidth: notificationVolumeSwitch.height*2 - implicitHeight: bigFontSize + implicitHeight: topBar.height*0.75 anchors.verticalCenter: parent.verticalCenter checked: NotificationS.soundEnabled @@ -199,19 +207,12 @@ PanelWindow { console.log("NotificationS.soundEnabled:", NotificationS.soundEnabled) } } - } - - Row{ - id: backgroundRight - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: Config.screen_spacing_fun(screen.height) - spacing: Config.screen_spacing_fun(screen.height) MouseArea { id: closeArea - width: bigFontSize - height: bigFontSize + width: closeArea.height + height: topBar.height*0.75 + anchors.verticalCenter: parent.verticalCenter hoverEnabled: true cursorShape: Qt.PointingHandCursor Rectangle{ @@ -221,15 +222,16 @@ PanelWindow { border.width: Config.border_width border.color: closeArea.containsMouse ? "white" : textColor Text{ - text: "" + text: "" anchors.centerIn: parent font.family: Config.font - font.pixelSize: bigFontSize + font.pixelSize: closeArea.height - Config.spacing color: closeArea.containsMouse ? "white" : textColor } } onClicked: { - closeNHistory() + root.clearAll() + // closeNHistory() } } } @@ -268,161 +270,178 @@ PanelWindow { width: flick.width spacing: Config.spacing - // ── Grouped view ────────────────────────────── - // Uses root.grouped (sorted by appName). Each group - // shows a header with appName + count + expand/collapse - // + clear-group button. The notifications themselves - // are rendered via NotificationBody.qml with - // isHistory:true (no auto-timeout, no click-to-close). + // ── Grouped view (only groups with >1 notification are grouped) ── + // Uses root.grouped (sorted by appName). Each app group with + // more than one notification is rendered as a collapsible + // card with a header (appName + count + expand/collapse + + // clear-group). Singletons are rendered directly as a + // NotificationBody without a header. Repeater { id: groupedRepeater model: root.grouped - delegate: Rectangle { - id: groupDelegate + delegate: Item { + id: delegateRoot required property var modelData required property int index property var group: modelData - // collapsed by default (expandedGroups[appName] === true means expanded) - // to default to expanded use: root.expandedGroups[group.appName] !== false + property bool isGrouped: group.notes.length > 1 property bool isExpanded: root.expandedGroups[group.appName] === true width: contentColumn.width - implicitHeight: groupColumn.implicitHeight + Config.spacing * 2 - color: Qt.alpha(Colors.surfaceContainerHigh, 0.45) - radius: Config.screen_big_radius_fun(root.screen.height) - border.width: Config.border_width - border.color: root.textColor - clip: true + // height adapts to which component is visible + implicitHeight: isGrouped ? groupedCard.implicitHeight : singleBody.implicitHeight + height: implicitHeight - Column { - id: groupColumn - anchors.fill: parent - anchors.margins: Config.spacing - spacing: Config.spacing + // ── Singleton: plain NotificationBody ── + NotificationBody { + id: singleBody + visible: !delegateRoot.isGrouped + width: parent.width + screen: root.screen + notification: delegateRoot.group.notes[0] + isHistory: true + onDismissed: function(notification, dismiss) { + if (notification && notification.tracked) notification.dismiss() + else if (notification && notification.close) notification.close() + } + } - // Header row - Rectangle { - id: groupHeader - width: parent.width - height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5 - color: root.bGColor - radius: Config.radius - border.width: Config.border_width - border.color: root.textColor + // ── Grouped card (count > 1) ── + Rectangle { + id: groupedCard + visible: delegateRoot.isGrouped + width: parent.width + implicitHeight: groupColumn.implicitHeight + Config.spacing * 2 + color: Qt.alpha(Colors.surfaceContainerHigh, 0.45) + radius: Config.screen_big_radius_fun(root.screen.height) + border.width: Config.border_width + border.color: root.textColor + clip: true - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.toggleGroup(group.appName) - } + Column { + id: groupColumn + anchors.fill: parent + anchors.margins: Config.spacing + spacing: Config.spacing - Row { - id: headerLeft - anchors.left: parent.left - anchors.leftMargin: Config.spacing - anchors.verticalCenter: parent.verticalCenter - spacing: Config.spacing - - Text { - id: expandIcon - text: groupDelegate.isExpanded ? "󰅀" : "󰅂" - font.family: Config.font - font.pixelSize: root.fontSize - color: root.textColor - anchors.verticalCenter: parent.verticalCenter - } - Text { - id: appNameText - text: group.appName - font.family: Config.font - font.pixelSize: root.fontSize - font.bold: true - color: root.textColor - elide: Text.ElideRight - maximumLineCount: 1 - } - Text { - id: countText - text: "(" + group.notes.length + ")" - font.family: Config.font - font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) - color: Qt.alpha(root.textColor, 0.8) - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - id: headerRight - anchors.right: parent.right - anchors.rightMargin: Config.spacing - anchors.verticalCenter: parent.verticalCenter - spacing: Config.spacing + Rectangle { + id: groupHeader + width: parent.width + height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5 + color: root.bGColor + radius: Config.radius + border.width: Config.border_width + border.color: root.textColor MouseArea { - id: clearGroupArea - width: clearGroupIcon.implicitWidth + Config.spacing - height: clearGroupIcon.implicitHeight + Config.spacing * 0.6 - hoverEnabled: true + anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: function(mouse) { - mouse.accepted = true - root.clearGroup(group.appName) + onClicked: root.toggleGroup(group.appName) + } + + Row { + id: headerLeft + anchors.left: parent.left + anchors.leftMargin: Config.spacing + anchors.verticalCenter: parent.verticalCenter + spacing: Config.spacing + + Text { + id: expandIcon + text: delegateRoot.isExpanded ? "󰅀" : "󰅂" + font.family: Config.font + font.pixelSize: root.fontSize + color: root.textColor + anchors.verticalCenter: parent.verticalCenter } - Rectangle { - anchors.fill: parent - radius: Config.radius - color: clearGroupArea.containsMouse ? Colors.errorContainer : "transparent" - border.width: Config.border_width - border.color: clearGroupArea.containsMouse ? Colors.error : root.textColor - Text { - id: clearGroupIcon - anchors.centerIn: parent - text: "󰆴" - font.family: Config.font - font.pixelSize: root.fontSize - color: clearGroupArea.containsMouse ? Colors.error_fg : root.textColor + Text { + id: appNameText + text: group.appName + font.family: Config.font + font.pixelSize: root.fontSize + font.bold: true + color: root.textColor + elide: Text.ElideRight + maximumLineCount: 1 + } + Text { + id: countText + text: "(" + group.notes.length + ")" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: Qt.alpha(root.textColor, 0.8) + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + id: headerRight + anchors.right: parent.right + anchors.rightMargin: Config.spacing + anchors.verticalCenter: parent.verticalCenter + spacing: Config.spacing + + MouseArea { + id: clearGroupArea + width: bigFontSize + Config.spacing + height: bigFontSize + Config.spacing + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + mouse.accepted = true + root.clearGroup(group.appName) + } + Rectangle{ + anchors.fill: parent + radius: Config.screen_big_radius_fun(screen.height) + color: clearGroupArea.containsMouse ? "red" : textBGColor + border.width: Config.border_width + border.color: clearGroupArea.containsMouse ? "white" : textColor + Text{ + id: clearGroupIcon + anchors.centerIn: parent + text: "" + font.family: Config.font + font.pixelSize: bigFontSize + color: clearGroupArea.containsMouse ? "white" : textColor + } } } } } - } - // Expanded list of NotificationBody items - Column { - id: notificationsColumn - width: parent.width - spacing: Config.spacing - visible: groupDelegate.isExpanded - Repeater { - model: group.notes - delegate: NotificationBody { - required property var modelData - required property int index - width: notificationsColumn.width - screen: root.screen - notification: modelData - isHistory: true - onDismissed: function(notification, dismiss) { - if (notification && notification.tracked) { - notification.dismiss() - } else if (notification && notification.close) { - notification.close() + Column { + id: notificationsColumn + width: parent.width + spacing: Config.spacing + visible: delegateRoot.isExpanded + Repeater { + model: group.notes + delegate: NotificationBody { + required property var modelData + required property int index + width: notificationsColumn.width + screen: root.screen + notification: modelData + isHistory: true + onDismissed: function(notification, dismiss) { + if (notification && notification.tracked) notification.dismiss() + else if (notification && notification.close) notification.close() } } } } - } - // Hint when collapsed - Text { - visible: !groupDelegate.isExpanded && group.notes.length > 0 - width: parent.width - text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand" - font.family: Config.font - font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) - color: Qt.alpha(root.textColor, 0.6) - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter + Text { + visible: !delegateRoot.isExpanded && group.notes.length > 1 + width: parent.width + text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: Qt.alpha(root.textColor, 0.6) + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } } } } diff --git a/modules/notification/notificationServer/NotificationServer.qml b/modules/notification/notificationServer/NotificationServer.qml index 6fd9149..443ea1e 100644 --- a/modules/notification/notificationServer/NotificationServer.qml +++ b/modules/notification/notificationServer/NotificationServer.qml @@ -83,7 +83,7 @@ Item { repeat: false onTriggered: { - if(!notification.lastGeneration && notificationServerState.soundEnabled){ + if((!notification.lastGeneration && notificationServerState.soundEnabled) || Config.debug == "Notification"){ focusedMonitor = Hyprland.focusedMonitor notificationReceived(notification) notificationSound.play()