diff --git a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml index 99a03aae..e6ef2df3 100644 --- a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml +++ b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -14,6 +14,8 @@ DankListView { property real stableContentHeight: 0 property bool cardAnimateExpansion: true property bool listInitialized: false + property int swipingCardIndex: -1 + property real swipingCardOffset: 0 property real __pendingStableHeight: 0 property real __heightUpdateThreshold: 20 @@ -132,6 +134,11 @@ DankListView { readonly property real dismissThreshold: width * 0.35 property bool __delegateInitialized: false + readonly property bool isAdjacentToSwipe: listView.count >= 2 && listView.swipingCardIndex !== -1 && + (index === listView.swipingCardIndex - 1 || index === listView.swipingCardIndex + 1) + readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? listView.swipingCardOffset * 0.10 : 0 + readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0 + Component.onCompleted: { Qt.callLater(() => { if (delegateRoot) @@ -140,13 +147,15 @@ DankListView { } width: ListView.view.width - height: isDismissing ? 0 : notificationCard.height - clip: isDismissing || notificationCard.isAnimating + height: notificationCard.height + clip: notificationCard.isAnimating NotificationCard { id: notificationCard width: parent.width - x: delegateRoot.swipeOffset + x: delegateRoot.swipeOffset + delegateRoot.adjacentSwipeInfluence + listLevelAdjacentScaleInfluence: delegateRoot.adjacentScaleInfluence + listLevelScaleAnimationsEnabled: listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe notificationGroup: modelData keyboardNavigationActive: listView.keyboardActive animateExpansion: listView.cardAnimateExpansion && listView.listInitialized @@ -188,7 +197,7 @@ DankListView { } Behavior on x { - enabled: !swipeDragHandler.active && listView.listInitialized + enabled: !swipeDragHandler.active && !delegateRoot.isDismissing && (listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe) && listView.listInitialized NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing @@ -210,12 +219,18 @@ DankListView { xAxis.enabled: true onActiveChanged: { - if (active || delegateRoot.isDismissing) + if (active) { + listView.swipingCardIndex = index; + return; + } + listView.swipingCardIndex = -1; + listView.swipingCardOffset = 0; + if (delegateRoot.isDismissing) return; if (Math.abs(delegateRoot.swipeOffset) > delegateRoot.dismissThreshold) { delegateRoot.isDismissing = true; - delegateRoot.swipeOffset = delegateRoot.swipeOffset > 0 ? delegateRoot.width : -delegateRoot.width; - dismissTimer.start(); + swipeDismissAnim.to = delegateRoot.swipeOffset > 0 ? delegateRoot.width : -delegateRoot.width; + swipeDismissAnim.start(); } else { delegateRoot.swipeOffset = 0; } @@ -225,13 +240,18 @@ DankListView { if (delegateRoot.isDismissing) return; delegateRoot.swipeOffset = translation.x; + listView.swipingCardOffset = translation.x; } } - Timer { - id: dismissTimer - interval: Theme.shortDuration - onTriggered: NotificationService.dismissGroup(delegateRoot.modelData?.key || "") + NumberAnimation { + id: swipeDismissAnim + target: delegateRoot + property: "swipeOffset" + to: 0 + duration: Theme.shortDuration + easing.type: Easing.OutCubic + onStopped: NotificationService.dismissGroup(delegateRoot.modelData?.key || "") } } diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index 62b594c7..63f5a50b 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -19,6 +19,10 @@ Rectangle { property bool isGroupSelected: false property int selectedNotificationIndex: -1 property bool keyboardNavigationActive: false + property int swipingNotificationIndex: -1 + property real swipingNotificationOffset: 0 + property real listLevelAdjacentScaleInfluence: 1.0 + property bool listLevelScaleAnimationsEnabled: true readonly property bool compactMode: SettingsData.notificationCompactMode readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding @@ -26,13 +30,14 @@ Rectangle { readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real badgeSize: compactMode ? 16 : 18 readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) + readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing width: parent ? parent.width : 400 height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) radius: Theme.cornerRadius + scale: (cardHoverHandler.hovered ? 1.01 : 1.0) * listLevelAdjacentScaleInfluence property bool __initialized: false Component.onCompleted: { @@ -42,6 +47,14 @@ Rectangle { }); } + Behavior on scale { + enabled: listLevelScaleAnimationsEnabled + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + Behavior on border.color { enabled: root.__initialized ColorAnimation { @@ -115,8 +128,8 @@ Rectangle { id: collapsedContent readonly property real expandedTextHeight: descriptionText.contentHeight - readonly property real collapsedLineCount: compactMode ? 1 : 3 - readonly property real collapsedLineHeight: descriptionText.font.pixelSize * 1.2 * collapsedLineCount + readonly property real collapsedLineCount: compactMode ? 1 : 2 + readonly property real collapsedLineHeight: Theme.fontSizeSmall * 1.2 * collapsedLineCount readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedLineHeight + 2) ? (expandedTextHeight - collapsedLineHeight) : 0 anchors.top: parent.top @@ -146,7 +159,7 @@ Rectangle { height: iconSize anchors.left: parent.left anchors.top: parent.top - anchors.topMargin: descriptionExpanded ? Math.max(0, (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - iconSize / 2) : Math.max(0, textContainer.height / 2 - iconSize / 2) + anchors.topMargin: descriptionExpanded ? Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - iconSize / 2) : Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - iconSize / 2) imageSource: { if (hasNotificationImage) @@ -223,47 +236,51 @@ Rectangle { spacing: Theme.notificationContentSpacing Row { - width: parent.width - ((notificationGroup?.count || 0) > 1 ? 10 : 0) + id: collapsedHeaderRow + width: parent.width spacing: Theme.spacingXS - visible: (collapsedTitleText.text.length > 0 || collapsedTimeText.text.length > 0) - readonly property real reservedTrailingWidth: collapsedSeparator.implicitWidth + Math.max(collapsedTimeText.implicitWidth, 72) + spacing + visible: (collapsedHeaderAppNameText.text.length > 0 || collapsedHeaderTimeText.text.length > 0) StyledText { - id: collapsedTitleText - width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) - text: { - let title = notificationGroup?.latestNotification?.summary || ""; - const appName = notificationGroup?.appName || ""; - const prefix = appName + " • "; - if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { - title = title.substring(prefix.length); - } - return title; - } - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium + id: collapsedHeaderAppNameText + text: notificationGroup?.appName || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal elide: Text.ElideRight maximumLineCount: 1 - visible: text.length > 0 + width: Math.min(implicitWidth, parent.width - collapsedHeaderSeparator.implicitWidth - collapsedHeaderTimeText.implicitWidth - parent.spacing * 2) } + StyledText { - id: collapsedSeparator - text: (collapsedTitleText.text.length > 0 && collapsedTimeText.text.length > 0) ? " • " : "" + id: collapsedHeaderSeparator + text: (collapsedHeaderAppNameText.text.length > 0 && collapsedHeaderTimeText.text.length > 0) ? " • " : "" color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) font.pixelSize: Theme.fontSizeSmall font.weight: Font.Normal } + StyledText { - id: collapsedTimeText + id: collapsedHeaderTimeText text: notificationGroup?.latestNotification?.timeStr || "" color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) font.pixelSize: Theme.fontSizeSmall font.weight: Font.Normal - visible: text.length > 0 } } + StyledText { + id: collapsedTitleText + width: parent.width + text: notificationGroup?.latestNotification?.summary || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { id: descriptionText property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || "" @@ -274,7 +291,7 @@ Rectangle { font.pixelSize: Theme.fontSizeSmall width: parent.width elide: Text.ElideRight - maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 3) + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) wrapMode: Text.WordWrap visible: text.length > 0 linkColor: Theme.primary @@ -380,7 +397,7 @@ Rectangle { id: expandedDelegateHoverHandler } readonly property real expandedItemPadding: compactMode ? Theme.spacingS : Theme.spacingM - readonly property real expandedBaseHeight: expandedItemPadding * 2 + expandedIconSize + actionButtonHeight + contentSpacing * 2 + readonly property real expandedBaseHeight: expandedItemPadding * 2 + Math.max(expandedIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * 2) + actionButtonHeight + contentSpacing * 2 property bool __delegateInitialized: false property real swipeOffset: 0 property bool isDismissing: false @@ -399,16 +416,34 @@ Rectangle { Rectangle { id: delegateRect - x: parent.swipeOffset width: parent.width + readonly property bool isAdjacentToSwipe: root.swipingNotificationIndex !== -1 && + (expandedDelegateWrapper.index === root.swipingNotificationIndex - 1 || + expandedDelegateWrapper.index === root.swipingNotificationIndex + 1) + readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? root.swipingNotificationOffset * 0.10 : 0 + readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(root.swipingNotificationOffset) / width * 0.02 : 1.0 + + x: expandedDelegateWrapper.swipeOffset + adjacentSwipeInfluence + scale: adjacentScaleInfluence + transformOrigin: Item.Center + Behavior on x { - enabled: !expandedSwipeHandler.active && !parent.parent.isDismissing + enabled: !expandedSwipeHandler.active && !expandedDelegateWrapper.isDismissing NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } + + Behavior on scale { + enabled: !expandedSwipeHandler.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + height: { if (!messageExpanded) return expandedBaseHeight; @@ -458,7 +493,7 @@ Rectangle { height: expandedIconSize anchors.left: parent.left anchors.top: parent.top - anchors.topMargin: compactMode ? Theme.spacingM : Theme.spacingXL + anchors.topMargin: Theme.fontSizeSmall * 1.2 + (compactMode ? Theme.spacingXS : Theme.spacingS) imageSource: { if (hasNotificationImage) @@ -504,46 +539,51 @@ Rectangle { spacing: Theme.notificationContentSpacing Row { + id: expandedDelegateHeaderRow width: parent.width spacing: Theme.spacingXS - readonly property real reservedTrailingWidth: expandedDelegateSeparator.implicitWidth + Math.max(expandedDelegateTimeText.implicitWidth, 72) + spacing + visible: (expandedDelegateHeaderAppNameText.text.length > 0 || expandedDelegateHeaderTimeText.text.length > 0) StyledText { - id: expandedDelegateTitleText - width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) - text: { - let title = modelData?.summary || ""; - const appName = modelData?.appName || ""; - const prefix = appName + " • "; - if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { - title = title.substring(prefix.length); - } - return title; - } - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium + id: expandedDelegateHeaderAppNameText + text: modelData?.appName || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal elide: Text.ElideRight maximumLineCount: 1 - visible: text.length > 0 + width: Math.min(implicitWidth, parent.width - expandedDelegateHeaderSeparator.implicitWidth - expandedDelegateHeaderTimeText.implicitWidth - parent.spacing * 2) } + StyledText { - id: expandedDelegateSeparator - text: (expandedDelegateTitleText.text.length > 0 && expandedDelegateTimeText.text.length > 0) ? " • " : "" + id: expandedDelegateHeaderSeparator + text: (expandedDelegateHeaderAppNameText.text.length > 0 && expandedDelegateHeaderTimeText.text.length > 0) ? " • " : "" color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) font.pixelSize: Theme.fontSizeSmall font.weight: Font.Normal } + StyledText { - id: expandedDelegateTimeText + id: expandedDelegateHeaderTimeText text: modelData?.timeStr || "" color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) font.pixelSize: Theme.fontSizeSmall font.weight: Font.Normal - visible: text.length > 0 } } + StyledText { + id: expandedDelegateTitleText + width: parent.width + text: modelData?.summary || "" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { id: bodyText property bool hasMoreText: truncated @@ -685,42 +725,49 @@ Rectangle { } } } - } - } - DragHandler { - id: expandedSwipeHandler - target: null - xAxis.enabled: true - yAxis.enabled: false - grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType + DragHandler { + id: expandedSwipeHandler + target: null + xAxis.enabled: true + yAxis.enabled: false + grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType - onActiveChanged: { - if (active || parent.isDismissing) - return; - if (Math.abs(parent.swipeOffset) > parent.dismissThreshold) { - parent.isDismissing = true; - expandedSwipeDismissAnim.start(); - } else { - parent.swipeOffset = 0; + onActiveChanged: { + if (active) { + root.swipingNotificationIndex = expandedDelegateWrapper.index; + } else { + root.swipingNotificationIndex = -1; + root.swipingNotificationOffset = 0; + } + if (active || expandedDelegateWrapper.isDismissing) + return; + if (Math.abs(expandedDelegateWrapper.swipeOffset) > expandedDelegateWrapper.dismissThreshold) { + expandedDelegateWrapper.isDismissing = true; + expandedSwipeDismissAnim.start(); + } else { + expandedDelegateWrapper.swipeOffset = 0; + } + } + + onTranslationChanged: { + if (expandedDelegateWrapper.isDismissing) + return; + expandedDelegateWrapper.swipeOffset = translation.x; + root.swipingNotificationOffset = translation.x; + } + } + + NumberAnimation { + id: expandedSwipeDismissAnim + target: expandedDelegateWrapper + property: "swipeOffset" + to: expandedDelegateWrapper.swipeOffset > 0 ? expandedDelegateWrapper.width : -expandedDelegateWrapper.width + duration: Theme.shortDuration + easing.type: Easing.OutCubic + onStopped: NotificationService.dismissNotification(modelData) } } - - onTranslationChanged: { - if (parent.isDismissing) - return; - parent.swipeOffset = translation.x; - } - } - - NumberAnimation { - id: expandedSwipeDismissAnim - target: parent - property: "swipeOffset" - to: parent.swipeOffset > 0 ? parent.width : -parent.width - duration: Theme.shortDuration - easing.type: Easing.OutCubic - onStopped: NotificationService.dismissNotification(modelData) } } } @@ -855,7 +902,7 @@ Rectangle { } Behavior on height { - enabled: root.userInitiatedExpansion && root.animateExpansion + enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion NumberAnimation { duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration easing.type: Easing.BezierSpline diff --git a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml index b7f09fa3..3bdfed06 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -81,6 +81,8 @@ DankPopout { } else { NotificationService.onOverlayClose(); keyboardController.keyboardNavigationActive = false; + NotificationService.expandedGroups = {}; + NotificationService.expandedMessages = {}; } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index b76e05e2..c4a74a0d 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -22,18 +22,25 @@ PanelWindow { property bool _finalized: false readonly property string clearText: I18n.tr("Dismiss") property bool descriptionExpanded: false + onDescriptionExpandedChanged: { + popupHeightChanged(); + } + onImplicitHeightChanged: { + popupHeightChanged(); + } readonly property bool compactMode: SettingsData.notificationCompactMode readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) + readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing signal entered signal exitStarted signal exitFinished + signal popupHeightChanged() function startExit() { if (exiting || _isDestroying) { @@ -104,7 +111,7 @@ PanelWindow { if (!descriptionExpanded) return basePopupHeight; const bodyTextHeight = bodyText.contentHeight || 0; - const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3); + const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2); if (bodyTextHeight > collapsedBodyHeight + 2) return basePopupHeight + bodyTextHeight - collapsedBodyHeight; return basePopupHeight; @@ -232,18 +239,41 @@ PanelWindow { width: alignedWidth height: alignedHeight visible: !win._finalized + scale: cardHoverHandler.hovered ? 1.01 : 1.0 + transformOrigin: Item.Center property real swipeOffset: 0 readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35 readonly property bool swipeActive: swipeDragHandler.active property bool swipeDismissing: false - property real shadowBlurPx: 10 - property real shadowSpreadPx: 0 + property real shadowBlurPx: cardHoverHandler.hovered ? 16 : 10 + property real shadowSpreadPx: cardHoverHandler.hovered ? 2 : 0 property real shadowBaseAlpha: 0.60 readonly property real popupSurfaceAlpha: SettingsData.popupTransparency readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) + Behavior on shadowBlurPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowSpreadPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + Item { id: bgShadowLayer anchors.fill: parent @@ -323,7 +353,7 @@ PanelWindow { id: notificationContent readonly property real expandedTextHeight: bodyText.contentHeight || 0 - readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3) + readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2) readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0 anchors.top: parent.top @@ -354,7 +384,7 @@ PanelWindow { height: popupIconSize anchors.left: parent.left anchors.top: parent.top - anchors.topMargin: descriptionExpanded ? Math.max(0, (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - popupIconSize / 2) : Math.max(0, textContainer.height / 2 - popupIconSize / 2) + anchors.topMargin: descriptionExpanded ? Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2) : Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2) imageSource: { if (!notificationData) @@ -410,6 +440,40 @@ PanelWindow { anchors.top: parent.top spacing: Theme.notificationContentSpacing + Row { + id: headerRow + width: parent.width + spacing: Theme.spacingXS + visible: headerAppNameText.text.length > 0 || headerTimeText.text.length > 0 + + StyledText { + id: headerAppNameText + text: notificationData ? (notificationData.appName || "") : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, parent.width - headerSeparator.implicitWidth - headerTimeText.implicitWidth - parent.spacing * 2) + } + + StyledText { + id: headerSeparator + text: (headerAppNameText.text.length > 0 && headerTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + + StyledText { + id: headerTimeText + text: notificationData ? (notificationData.timeStr || "") : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + } + StyledText { text: notificationData ? (notificationData.summary || "") : "" color: Theme.surfaceText @@ -432,7 +496,7 @@ PanelWindow { width: parent.width elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight horizontalAlignment: Text.AlignLeft - maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 3) + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) wrapMode: Text.WordWrap visible: text.length > 0 linkColor: Theme.primary @@ -607,7 +671,9 @@ PanelWindow { if (mouse.button === Qt.RightButton) { popupContextMenu.popup(); } else if (mouse.button === Qt.LeftButton) { - if (notificationData.actions && notificationData.actions.length > 0) { + if (bodyText.hasMoreText || win.descriptionExpanded) { + win.descriptionExpanded = !win.descriptionExpanded; + } else if (notificationData.actions && notificationData.actions.length > 0) { notificationData.actions[0].invoke(); NotificationService.dismissNotification(notificationData); } else { diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 0055472f..02a58bb5 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -13,7 +13,8 @@ QtObject { readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS - readonly property int baseNotificationHeight: cardPadding * 2 + popupIconSize + actionButtonHeight + contentSpacing + popupSpacing + readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) + readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing property int maxTargetNotifications: 4 property var popupWindows: [] // strong refs to windows (live until exitFinished) property var destroyingWindows: new Set() @@ -29,6 +30,7 @@ QtObject { onEntered: manager._onPopupEntered(this) onExitStarted: manager._onPopupExitStarted(this) onExitFinished: manager._onPopupExitFinished(this) + onPopupHeightChanged: manager._onPopupHeightChanged(this) } } @@ -127,10 +129,7 @@ QtObject { } if (toRemove.length) { popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1); - const survivors = _active().sort((a, b) => a.screenY - b.screenY); - for (let k = 0; k < survivors.length; ++k) { - survivors[k].screenY = topMargin + k * baseNotificationHeight; - } + _repositionAllActivePopups(); } if (popupWindows.length === 0) { sweeper.stop(); @@ -237,13 +236,6 @@ QtObject { function _doInsertNewestAtTop(wrapper) { if (!wrapper) return; - for (const p of popupWindows) { - if (!_isValidWindow(p)) - continue; - if (p.exiting) - continue; - p.screenY = p.screenY + baseNotificationHeight; - } const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : ""; const win = popupComponent.createObject(null, { "notificationData": wrapper, @@ -257,7 +249,10 @@ QtObject { win.destroy(); return; } - popupWindows.push(win); + popupWindows.unshift(win); + + _repositionAllActivePopups(); + createBusy = true; createTimer.restart(); if (!sweeper.running) @@ -283,12 +278,25 @@ QtObject { function _onPopupEntered(p) { } + function _onPopupHeightChanged(p) { + if (!p || p.exiting || p._isDestroying) + return; + _repositionAllActivePopups(); + } + + function _repositionAllActivePopups() { + const activeWindows = _active().sort((a, b) => a.screenY - b.screenY); + let currentY = topMargin; + for (const win of activeWindows) { + win.screenY = currentY; + currentY += win.implicitHeight + popupSpacing; + } + } + function _onPopupExitStarted(p) { if (!p) return; - const survivors = _active().sort((a, b) => a.screenY - b.screenY); - for (let k = 0; k < survivors.length; ++k) - survivors[k].screenY = topMargin + k * baseNotificationHeight; + _repositionAllActivePopups(); } function _onPopupExitFinished(p) { @@ -310,10 +318,7 @@ QtObject { } _scheduleDestroy(p); Qt.callLater(() => destroyingWindows.delete(windowId)); - const survivors = _active().sort((a, b) => a.screenY - b.screenY); - for (let k = 0; k < survivors.length; ++k) { - survivors[k].screenY = topMargin + k * baseNotificationHeight; - } + _repositionAllActivePopups(); } function cleanupAllWindows() {