notifcenter: collapsible app groups, hover-only group dismiss, full opacity on notif hover

This commit is contained in:
Damocles 2026-04-17 10:34:11 +02:00
parent a502faef19
commit 862169aba0
2 changed files with 110 additions and 22 deletions

View file

@ -28,7 +28,7 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: _hover.hovered ? M.Theme.base02 : M.Theme.base01 color: _hover.hovered ? M.Theme.base02 : M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.9) opacity: _hover.hovered ? 1.0 : Math.max(M.Theme.barOpacity, 0.9)
radius: M.Theme.radius radius: M.Theme.radius
Behavior on color { Behavior on color {

View file

@ -67,6 +67,18 @@ M.HoverPanel {
property var _pendingDismissIds: [] property var _pendingDismissIds: []
// Collapsed groups set reassign to trigger reactivity
property var _collapsedGroups: ({})
function _toggleCollapse(appName) {
const next = Object.assign({}, _collapsedGroups);
if (next[appName])
delete next[appName];
else
next[appName] = true;
_collapsedGroups = next;
}
// Group notifications by appName, sorted by max urgency desc then most recent time desc // Group notifications by appName, sorted by max urgency desc then most recent time desc
readonly property var _groups: { readonly property var _groups: {
const map = {}; const map = {};
@ -93,22 +105,27 @@ M.HoverPanel {
}); });
} }
// Flat model: group header followed by its notifications // Flat model: group header followed by its notifications (omitted when collapsed)
readonly property var _flatModel: { readonly property var _flatModel: {
const arr = []; const arr = [];
for (const g of _groups) { for (const g of _groups) {
const collapsed = !!_collapsedGroups[g.appName];
arr.push({ arr.push({
type: "header", type: "header",
appName: g.appName, appName: g.appName,
appIcon: g.appIcon, appIcon: g.appIcon,
count: g.notifs.length count: g.notifs.length,
collapsed: collapsed,
summaries: g.notifs.map(n => n.summary || "").filter(Boolean).join(" · ")
}); });
if (!collapsed) {
for (const n of g.notifs) for (const n of g.notifs)
arr.push({ arr.push({
type: "notif", type: "notif",
data: n data: n
}); });
} }
}
return arr; return arr;
} }
@ -218,11 +235,25 @@ M.HoverPanel {
readonly property var _notif: _type === "notif" ? modelData.data : null readonly property var _notif: _type === "notif" ? modelData.data : null
width: menuWindow.contentWidth width: menuWindow.contentWidth
height: _targetHeight * _heightScale height: _displayTargetHeight * _heightScale
clip: true clip: true
opacity: 0 opacity: 0
readonly property real _targetHeight: _type === "header" ? 28 : _notifCard.implicitHeight readonly property real _targetHeight: {
if (_type === "header")
return modelData.collapsed ? (28 + M.Theme.fontSize + 4) : 28;
return _notifCard.implicitHeight;
}
// Animated version of _targetHeight smoothly transitions header height on collapse
property real _displayTargetHeight: _targetHeight
Behavior on _displayTargetHeight {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
property real _heightScale: 1 property real _heightScale: 1
property bool _skipDismiss: false property bool _skipDismiss: false
@ -257,11 +288,29 @@ M.HoverPanel {
visible: notifDelegate._type === "header" visible: notifDelegate._type === "header"
anchors.fill: parent anchors.fill: parent
HoverHandler {
id: _headerHover
}
// Tap target for collapse covers header row only, excludes dismiss button
Item {
anchors.left: parent.left
anchors.right: _groupDismissBtn.left
anchors.top: parent.top
height: 28
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: menuWindow._toggleCollapse(notifDelegate.modelData.appName)
}
}
Image { Image {
id: _headerIcon id: _headerIcon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
anchors.topMargin: (28 - height) / 2
width: M.Theme.fontSize + 2 width: M.Theme.fontSize + 2
height: M.Theme.fontSize + 2 height: M.Theme.fontSize + 2
source: { source: {
@ -278,12 +327,29 @@ M.HoverPanel {
asynchronous: true asynchronous: true
} }
// Collapse chevron
Text {
id: _chevron
anchors.right: _groupDismissBtn.left
anchors.rightMargin: 8
anchors.top: parent.top
height: 28
verticalAlignment: Text.AlignVCenter
text: notifDelegate._type === "header" && notifDelegate.modelData.collapsed ? "\u25B8" : "\u25BE"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
// App name
Text { Text {
anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left
anchors.leftMargin: _headerIcon.visible ? 6 : 10 anchors.leftMargin: _headerIcon.visible ? 6 : 10
anchors.right: _groupDismissBtn.left anchors.right: _chevron.left
anchors.rightMargin: 6 anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
height: 28
verticalAlignment: Text.AlignVCenter
text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : "" text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : ""
color: M.Theme.base05 color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1 font.pixelSize: M.Theme.fontSize - 1
@ -292,28 +358,50 @@ M.HoverPanel {
elide: Text.ElideRight elide: Text.ElideRight
} }
// Dismiss button opacity-hidden when header not hovered
Text { Text {
id: _groupDismissBtn id: _groupDismissBtn
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 10 anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
height: 28
verticalAlignment: Text.AlignVCenter
text: "\uF1F8" text: "\uF1F8"
color: _groupDismissArea.containsMouse ? M.Theme.base08 : M.Theme.base04 color: _groupDismissHover.hovered ? M.Theme.base08 : M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1 font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.iconFontFamily font.family: M.Theme.iconFontFamily
opacity: _headerHover.hovered ? 1 : 0
MouseArea { HoverHandler {
id: _groupDismissArea id: _groupDismissHover
anchors.fill: parent }
anchors.margins: -4
hoverEnabled: true TapHandler {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onTapped: {
if (notifDelegate._type === "header") if (notifDelegate._type === "header")
menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName); menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName);
} }
} }
} }
// Collapsed preview: notification summaries on a subtitle row
Text {
visible: notifDelegate._type === "header" && notifDelegate.modelData.collapsed
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
anchors.top: parent.top
anchors.topMargin: 28
height: M.Theme.fontSize + 4
verticalAlignment: Text.AlignVCenter
text: notifDelegate._type === "header" ? (notifDelegate.modelData.summaries ?? "") : ""
elide: Text.ElideRight
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
color: M.Theme.base04
}
} }
// ---- Individual notification ---- // ---- Individual notification ----