feat: refactor: add helpers for summary/body placement in NotificationBody

This commit is contained in:
Hannes
2026-09-01 23:59:36 +02:00
parent 2df40a2a76
commit b59eb012ba
4 changed files with 237 additions and 194 deletions
+1 -1
View File
@@ -81,5 +81,5 @@ QtObject {
return maxWidth; return maxWidth;
} }
property string debug: "Notification" property string debug: ""
} }
+59 -35
View File
@@ -16,6 +16,14 @@ Rectangle {
// When used in history overview we don't want auto-timeout or click-to-close // When used in history overview we don't want auto-timeout or click-to-close
property bool isHistory: false 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 : ( property real duration: isHistory ? 0 : (
notification.expireTimeout > 0 ? notification.expireTimeout*1000 : notification.expireTimeout > 0 ? notification.expireTimeout*1000 :
urgency ? urgency * 2000 : urgency ? urgency * 2000 :
@@ -26,12 +34,13 @@ Rectangle {
implicitWidth: screen.width/7 implicitWidth: screen.width/7
// implicitHeight: screen.height/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 color: textBGColor
border.width: Config.border_width border.width: Config.border_width
border.color: textColor border.color: textColor
radius: isHistory ? Config.radius : 0 radius: isHistory ? Config.small_radius : 0
clip: isHistory clip: isHistory
property color textColor: urgency != 3 ? Colors.primary : Colors.tertiary 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 height: Math.max(iconRectangle.height, contentRectangle.height)+contentRow.anchors.margins*2
width: parent.width width: parent.width
color: "transparent" color: "transparent"
radius: isHistory ? Config.radius : 0
border.width: Config.border_width border.width: Config.border_width
border.color: textColor border.color: textColor
@@ -83,26 +91,26 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Config.spacing anchors.margins: Config.spacing
width: closeIconText.implicitHeight + Config.spacing width: closeIconText.implicitHeight
height: closeIconText.implicitHeight + Config.spacing height: closeIconText.implicitHeight
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Config.radius radius: Config.radius
color: parent.containsMouse ? Colors.errorContainer : "transparent" color: parent.containsMouse ? "red" : "transparent"
border.width: Config.border_width border.width: Config.border_width
border.color: parent.containsMouse ? Colors.error : textColor border.color: parent.containsMouse ? "white" : textColor
Text { Text {
id: closeIconText id: closeIconText
anchors.centerIn: parent anchors.centerIn: parent
text: ""; // or "✕" text: ""; // or "✕"
font.family: Config.font font.family: Config.font
font.pixelSize: Config.screen_height_to_font_small(screen.height) font.pixelSize: Config.screen_height_to_font_medium(screen.height)
color: parent.parent.containsMouse ? Colors.error_fg : textColor color: parent.parent.containsMouse ? "white" : textColor
} }
} }
onClicked: function(mouse){ onClicked: function(mouse){
@@ -154,10 +162,10 @@ Rectangle {
Text { Text {
id: summaryText id: summaryText
// height: implicitHeight + font.pixelSize + Config.spacing
text: root.notification.summary 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 width: iconColumn.width
font.family: Config.font font.family: Config.font
font.pixelSize: Config.screen_height_to_font_small(screen.height) font.pixelSize: Config.screen_height_to_font_small(screen.height)
@@ -173,37 +181,54 @@ Rectangle {
Rectangle{ Rectangle{
id: contentRectangle id: contentRectangle
width: root.width - iconColumn.width - contentRow.spacing - contentRow.anchors.margins*2 width: root.width - iconColumn.width - contentRow.spacing - contentRow.anchors.margins*2
height: iconRectangle.height height: Math.max(iconRectangle.height, contentBodyColumn.implicitHeight)
color: "transparent" color: "transparent"
Column { Column {
id: contentBodyColumn id: contentBodyColumn
width: parent.width width: parent.width
height: Math.max(childrenRect.height, iconRectangle.height)
spacing: Config.spacing spacing: Config.spacing
Text { Item {
id: bodyTitleText id: titleTextItem
text: { width: parent.width-closingMouseArea.width
if (notification.image) return notification.appName.charAt(0).toUpperCase() + notification.appName.slice(1) height: closingMouseArea.height
return "Message:" 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 { width: parent.width - 2*parent.anchors.margins
id: bodyText font.family: Config.font
text: root.notification.body font.pixelSize: Config.screen_height_to_font_tiny(screen.height)
visible: root.notification.body ? true : false color: textColor
wrapMode: Text.Wrap
width: contentBodyColumn.width }
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 to: 0.0
duration: root.duration duration: root.duration
paused: (mouseArea.containsMouse || closingMouseArea.containsMouse || inlineReplyButtonMouseArea.containsMouse || root._hoveredActionCount > 0 || inlineReplyTextField.hovered || inlineReplyTextField.activeFocus) && !isHistory paused: (mouseArea.containsMouse || closingMouseArea.containsMouse || inlineReplyButtonMouseArea.containsMouse || root._hoveredActionCount > 0 || inlineReplyTextField.hovered || inlineReplyTextField.activeFocus) && !isHistory
onFinished: root.closeNotification(false) onFinished: root.closeNotification(false)
} }
} }
+176 -157
View File
@@ -10,7 +10,7 @@ import "./notificationServer"
PanelWindow { PanelWindow {
id: root id: root
implicitWidth: screen.width/5 implicitWidth: screen.width/5
implicitHeight: screen.height - Config.barHeight(screen.height)*2 implicitHeight: screen.height - Config.barHeight(screen.height)
color: "transparent" color: "transparent"
visible: true visible: true
focusable: true focusable: true
@@ -101,14 +101,14 @@ PanelWindow {
// text: NotificationS.trackedNotifications.values[0] // text: NotificationS.trackedNotifications.values[0]
// } // }
Component.onCompleted: { // Component.onCompleted: {
for (var notification in NotificationS.trackedNotifications.values) { // for (var notification in NotificationS.trackedNotifications.values) {
console.log("[NotificationHistory]: " + notification.appName + " Notifications") // console.log("[NotificationHistory]: " + notification.appName + " Notifications")
} // }
for (var i =0; i < NotificationS.notificationNum; i++) { // for (var i =0; i < NotificationS.notificationNum; i++) {
console.log("[NotificationHistory]: " + NotificationS.trackedNotifications.values[i].appName + " Notifications") // console.log("[NotificationHistory]: " + NotificationS.trackedNotifications.values[i].appName + " Notifications")
} // }
} // }
Column{ Column{
id: bgColumn id: bgColumn
@@ -119,7 +119,7 @@ PanelWindow {
Rectangle { Rectangle {
id: topBar id: topBar
width: parent.width width: parent.width
height: bigFontSize*1.25 height: bigFontSize*2.5
color: textBGColor color: textBGColor
radius: Config.screen_big_radius_fun(screen.height) radius: Config.screen_big_radius_fun(screen.height)
border.width: Config.border_width border.width: Config.border_width
@@ -135,20 +135,28 @@ PanelWindow {
Text{ Text{
text: "Notifications:" text: "Notifications:"
font.family: Config.font font.family: Config.font
font.pixelSize: bigFontSize font.pixelSize: topBar.height*0.75
color: textColor color: textColor
} }
} }
// Row{
// id: backgroundCenter
// anchors.centerIn: parent
// spacing: Config.screen_small_bar_spacing_fun(screen.height)
// }
Row{ Row{
id: backgroundCenter id: backgroundRight
anchors.centerIn: parent anchors.verticalCenter: parent.verticalCenter
spacing: Config.screen_small_bar_spacing_fun(screen.height) anchors.right: parent.right
anchors.rightMargin: Config.screen_spacing_fun(screen.height)
spacing: Config.screen_spacing_fun(screen.height)
Text{ Text{
text: NotificationS.soundEnabled ? "󰂚" : "󰂛" text: NotificationS.soundEnabled ? "󰂚" : "󰂛"
font.family: Config.font font.family: Config.font
font.pixelSize: bigFontSize font.pixelSize: topBar.height*0.75
color: textColor color: textColor
} }
@@ -156,7 +164,7 @@ PanelWindow {
id: notificationVolumeSwitch id: notificationVolumeSwitch
implicitWidth: notificationVolumeSwitch.height*2 implicitWidth: notificationVolumeSwitch.height*2
implicitHeight: bigFontSize implicitHeight: topBar.height*0.75
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: NotificationS.soundEnabled checked: NotificationS.soundEnabled
@@ -199,19 +207,12 @@ PanelWindow {
console.log("NotificationS.soundEnabled:", NotificationS.soundEnabled) 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 { MouseArea {
id: closeArea id: closeArea
width: bigFontSize width: closeArea.height
height: bigFontSize height: topBar.height*0.75
anchors.verticalCenter: parent.verticalCenter
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
Rectangle{ Rectangle{
@@ -221,15 +222,16 @@ PanelWindow {
border.width: Config.border_width border.width: Config.border_width
border.color: closeArea.containsMouse ? "white" : textColor border.color: closeArea.containsMouse ? "white" : textColor
Text{ Text{
text: "" text: ""
anchors.centerIn: parent anchors.centerIn: parent
font.family: Config.font font.family: Config.font
font.pixelSize: bigFontSize font.pixelSize: closeArea.height - Config.spacing
color: closeArea.containsMouse ? "white" : textColor color: closeArea.containsMouse ? "white" : textColor
} }
} }
onClicked: { onClicked: {
closeNHistory() root.clearAll()
// closeNHistory()
} }
} }
} }
@@ -268,161 +270,178 @@ PanelWindow {
width: flick.width width: flick.width
spacing: Config.spacing spacing: Config.spacing
// ── Grouped view ────────────────────────────── // ── Grouped view (only groups with >1 notification are grouped) ──
// Uses root.grouped (sorted by appName). Each group // Uses root.grouped (sorted by appName). Each app group with
// shows a header with appName + count + expand/collapse // more than one notification is rendered as a collapsible
// + clear-group button. The notifications themselves // card with a header (appName + count + expand/collapse +
// are rendered via NotificationBody.qml with // clear-group). Singletons are rendered directly as a
// isHistory:true (no auto-timeout, no click-to-close). // NotificationBody without a header.
Repeater { Repeater {
id: groupedRepeater id: groupedRepeater
model: root.grouped model: root.grouped
delegate: Rectangle { delegate: Item {
id: groupDelegate id: delegateRoot
required property var modelData required property var modelData
required property int index required property int index
property var group: modelData property var group: modelData
// collapsed by default (expandedGroups[appName] === true means expanded) property bool isGrouped: group.notes.length > 1
// to default to expanded use: root.expandedGroups[group.appName] !== false
property bool isExpanded: root.expandedGroups[group.appName] === true property bool isExpanded: root.expandedGroups[group.appName] === true
width: contentColumn.width width: contentColumn.width
implicitHeight: groupColumn.implicitHeight + Config.spacing * 2 // height adapts to which component is visible
color: Qt.alpha(Colors.surfaceContainerHigh, 0.45) implicitHeight: isGrouped ? groupedCard.implicitHeight : singleBody.implicitHeight
radius: Config.screen_big_radius_fun(root.screen.height) height: implicitHeight
border.width: Config.border_width
border.color: root.textColor
clip: true
Column { // ── Singleton: plain NotificationBody ──
id: groupColumn NotificationBody {
anchors.fill: parent id: singleBody
anchors.margins: Config.spacing visible: !delegateRoot.isGrouped
spacing: Config.spacing 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 // ── Grouped card (count > 1) ──
Rectangle { Rectangle {
id: groupHeader id: groupedCard
width: parent.width visible: delegateRoot.isGrouped
height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5 width: parent.width
color: root.bGColor implicitHeight: groupColumn.implicitHeight + Config.spacing * 2
radius: Config.radius color: Qt.alpha(Colors.surfaceContainerHigh, 0.45)
border.width: Config.border_width radius: Config.screen_big_radius_fun(root.screen.height)
border.color: root.textColor border.width: Config.border_width
border.color: root.textColor
clip: true
MouseArea { Column {
anchors.fill: parent id: groupColumn
cursorShape: Qt.PointingHandCursor anchors.fill: parent
onClicked: root.toggleGroup(group.appName) anchors.margins: Config.spacing
} spacing: Config.spacing
Row { Rectangle {
id: headerLeft id: groupHeader
anchors.left: parent.left width: parent.width
anchors.leftMargin: Config.spacing height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5
anchors.verticalCenter: parent.verticalCenter color: root.bGColor
spacing: Config.spacing radius: Config.radius
border.width: Config.border_width
Text { border.color: root.textColor
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
MouseArea { MouseArea {
id: clearGroupArea anchors.fill: parent
width: clearGroupIcon.implicitWidth + Config.spacing
height: clearGroupIcon.implicitHeight + Config.spacing * 0.6
hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) { onClicked: root.toggleGroup(group.appName)
mouse.accepted = true }
root.clearGroup(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 { Text {
anchors.fill: parent id: appNameText
radius: Config.radius text: group.appName
color: clearGroupArea.containsMouse ? Colors.errorContainer : "transparent" font.family: Config.font
border.width: Config.border_width font.pixelSize: root.fontSize
border.color: clearGroupArea.containsMouse ? Colors.error : root.textColor font.bold: true
Text { color: root.textColor
id: clearGroupIcon elide: Text.ElideRight
anchors.centerIn: parent maximumLineCount: 1
text: "󰆴" }
font.family: Config.font Text {
font.pixelSize: root.fontSize id: countText
color: clearGroupArea.containsMouse ? Colors.error_fg : root.textColor 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 {
Column { id: notificationsColumn
id: notificationsColumn width: parent.width
width: parent.width spacing: Config.spacing
spacing: Config.spacing visible: delegateRoot.isExpanded
visible: groupDelegate.isExpanded Repeater {
Repeater { model: group.notes
model: group.notes delegate: NotificationBody {
delegate: NotificationBody { required property var modelData
required property var modelData required property int index
required property int index width: notificationsColumn.width
width: notificationsColumn.width screen: root.screen
screen: root.screen notification: modelData
notification: modelData isHistory: true
isHistory: true onDismissed: function(notification, dismiss) {
onDismissed: function(notification, dismiss) { if (notification && notification.tracked) notification.dismiss()
if (notification && notification.tracked) { else if (notification && notification.close) notification.close()
notification.dismiss()
} else if (notification && notification.close) {
notification.close()
} }
} }
} }
} }
}
// Hint when collapsed Text {
Text { visible: !delegateRoot.isExpanded && group.notes.length > 1
visible: !groupDelegate.isExpanded && group.notes.length > 0 width: parent.width
width: parent.width text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand"
text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand" font.family: Config.font
font.family: Config.font font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height)
font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) color: Qt.alpha(root.textColor, 0.6)
color: Qt.alpha(root.textColor, 0.6) elide: Text.ElideRight
elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter
horizontalAlignment: Text.AlignHCenter }
} }
} }
} }
@@ -83,7 +83,7 @@ Item {
repeat: false repeat: false
onTriggered: { onTriggered: {
if(!notification.lastGeneration && notificationServerState.soundEnabled){ if((!notification.lastGeneration && notificationServerState.soundEnabled) || Config.debug == "Notification"){
focusedMonitor = Hyprland.focusedMonitor focusedMonitor = Hyprland.focusedMonitor
notificationReceived(notification) notificationReceived(notification)
notificationSound.play() notificationSound.play()