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 {
anchors.fill: parent
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
Behavior on color {

View file

@ -67,6 +67,18 @@ M.HoverPanel {
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
readonly property var _groups: {
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: {
const arr = [];
for (const g of _groups) {
const collapsed = !!_collapsedGroups[g.appName];
arr.push({
type: "header",
appName: g.appName,
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)
arr.push({
type: "notif",
data: n
});
}
}
return arr;
}
@ -218,11 +235,25 @@ M.HoverPanel {
readonly property var _notif: _type === "notif" ? modelData.data : null
width: menuWindow.contentWidth
height: _targetHeight * _heightScale
height: _displayTargetHeight * _heightScale
clip: true
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 bool _skipDismiss: false
@ -257,11 +288,29 @@ M.HoverPanel {
visible: notifDelegate._type === "header"
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 {
id: _headerIcon
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.topMargin: (28 - height) / 2
width: M.Theme.fontSize + 2
height: M.Theme.fontSize + 2
source: {
@ -278,12 +327,29 @@ M.HoverPanel {
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 {
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
anchors.right: _chevron.left
anchors.rightMargin: 4
anchors.top: parent.top
height: 28
verticalAlignment: Text.AlignVCenter
text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1
@ -292,28 +358,50 @@ M.HoverPanel {
elide: Text.ElideRight
}
// Dismiss button opacity-hidden when header not hovered
Text {
id: _groupDismissBtn
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
height: 28
verticalAlignment: Text.AlignVCenter
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.family: M.Theme.iconFontFamily
opacity: _headerHover.hovered ? 1 : 0
MouseArea {
id: _groupDismissArea
anchors.fill: parent
anchors.margins: -4
hoverEnabled: true
HoverHandler {
id: _groupDismissHover
}
TapHandler {
cursorShape: Qt.PointingHandCursor
onClicked: {
onTapped: {
if (notifDelegate._type === "header")
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 ----