diff --git a/modules/NotifCard.qml b/modules/NotifCard.qml index bd19ee0..fa4b8da 100644 --- a/modules/NotifCard.qml +++ b/modules/NotifCard.qml @@ -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 { diff --git a/modules/NotifCenter.qml b/modules/NotifCenter.qml index ee07ebf..8b54608 100644 --- a/modules/NotifCenter.qml +++ b/modules/NotifCenter.qml @@ -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,21 +105,26 @@ 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(" · ") }); - for (const n of g.notifs) - arr.push({ - type: "notif", - data: n - }); + 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 ----