From 62bc25782cc1dcc5f71e3c3718d7eefbcc88029e Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 13 Feb 2026 21:44:27 -0500 Subject: [PATCH] notifications: fix compact spacing, reveal header bar, add bottom center position, pointing hand cursor fix --- .../Notifications/Center/NotificationCard.qml | 431 +++++++++--------- .../Notifications/Popup/NotificationPopup.qml | 75 +-- .../Popup/NotificationPopupManager.qml | 5 +- .../Modules/Settings/NotificationsTab.qml | 23 +- 4 files changed, 268 insertions(+), 266 deletions(-) diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index 83d0130c..7c4cab21 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -124,7 +124,7 @@ Rectangle { anchors.right: parent.right anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL + ((cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? Theme.notificationHoverRevealMargin : 0) + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin height: collapsedContentHeight + extraHeight visible: !expanded @@ -146,9 +146,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.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - iconSize / 2) : Math.max(0, textContainer.height / 2 - iconSize / 2) imageSource: { if (hasNotificationImage) @@ -327,7 +325,7 @@ Rectangle { Row { anchors.left: parent.left anchors.right: parent.right - anchors.rightMargin: Theme.spacingL + ((cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? Theme.notificationHoverRevealMargin : 0) + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS @@ -375,7 +373,7 @@ Rectangle { required property int index readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false readonly property bool isSelected: root.selectedNotificationIndex === index - readonly property bool actionsVisible: expandedDelegateHoverHandler.hovered || (root.keyboardNavigationActive && isSelected) + readonly property bool actionsVisible: true readonly property real expandedIconSize: compactMode ? Theme.notificationExpandedIconSizeCompact : Theme.notificationExpandedIconSizeNormal HoverHandler { @@ -437,196 +435,240 @@ Rectangle { } Item { - anchors.fill: parent - anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM - anchors.bottomMargin: contentSpacing + anchors.fill: parent + anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM + anchors.bottomMargin: contentSpacing - DankCircularImage { - id: messageIcon + DankCircularImage { + id: messageIcon - readonly property string rawImage: modelData?.image || "" - readonly property string iconFromImage: { - if (rawImage.startsWith("image://icon/")) - return rawImage.substring(13); - return ""; - } - readonly property bool imageHasSpecialPrefix: { - const icon = iconFromImage; - return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:"); - } - readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/") - - width: expandedIconSize - height: expandedIconSize - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: compactMode ? Theme.spacingM : Theme.spacingXL - - imageSource: { - if (hasNotificationImage) - return modelData.cleanImage; - if (imageHasSpecialPrefix) + readonly property string rawImage: modelData?.image || "" + readonly property string iconFromImage: { + if (rawImage.startsWith("image://icon/")) + return rawImage.substring(13); return ""; - const appIcon = modelData?.appIcon; - if (!appIcon) - return iconFromImage ? "image://icon/" + iconFromImage : ""; - if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) - return appIcon; - if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) - return ""; - return Quickshell.iconPath(appIcon, true); - } + } + readonly property bool imageHasSpecialPrefix: { + const icon = iconFromImage; + return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:"); + } + readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/") - fallbackIcon: { - if (imageHasSpecialPrefix) - return iconFromImage; - return modelData?.appIcon || iconFromImage || ""; - } - - fallbackText: { - const appName = modelData?.appName || "?"; - return appName.charAt(0).toUpperCase(); - } - } - - Item { - anchors.left: messageIcon.right - anchors.leftMargin: Theme.spacingM - anchors.right: parent.right - anchors.rightMargin: Theme.spacingM - anchors.top: parent.top - anchors.bottom: parent.bottom - - Column { + width: expandedIconSize + height: expandedIconSize anchors.left: parent.left - anchors.right: parent.right anchors.top: parent.top - anchors.bottom: buttonArea.top - anchors.bottomMargin: contentSpacing - spacing: Theme.notificationContentSpacing + anchors.topMargin: compactMode ? Theme.spacingM : Theme.spacingXL - Row { - width: parent.width - spacing: Theme.spacingXS - readonly property real reservedTrailingWidth: expandedDelegateSeparator.implicitWidth + Math.max(expandedDelegateTimeText.implicitWidth, 72) + spacing - - 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 - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } - StyledText { - id: expandedDelegateSeparator - text: (expandedDelegateTitleText.text.length > 0 && expandedDelegateTimeText.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 - 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 - } + imageSource: { + if (hasNotificationImage) + return modelData.cleanImage; + if (imageHasSpecialPrefix) + return ""; + const appIcon = modelData?.appIcon; + if (!appIcon) + return iconFromImage ? "image://icon/" + iconFromImage : ""; + if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) + return appIcon; + if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) + return ""; + return Quickshell.iconPath(appIcon, true); } - StyledText { - id: bodyText - property bool hasMoreText: truncated + fallbackIcon: { + if (imageHasSpecialPrefix) + return iconFromImage; + return modelData?.appIcon || iconFromImage || ""; + } - text: modelData?.htmlBody || "" - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - width: parent.width - elide: messageExpanded ? Text.ElideNone : Text.ElideRight - maximumLineCount: messageExpanded ? -1 : 2 - wrapMode: Text.WordWrap - visible: text.length > 0 - linkColor: Theme.primary - onLinkActivated: link => Qt.openUrlExternally(link) - MouseArea { - anchors.fill: parent - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor - - onClicked: mouse => { - if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { - NotificationService.toggleMessageExpansion(modelData?.notification?.id || ""); - } - } - - propagateComposedEvents: true - onPressed: mouse => { - if (parent.hoveredLink) { - mouse.accepted = false; - } - } - onReleased: mouse => { - if (parent.hoveredLink) { - mouse.accepted = false; - } - } - } + fallbackText: { + const appName = modelData?.appName || "?"; + return appName.charAt(0).toUpperCase(); } } Item { - id: buttonArea - anchors.left: parent.left + anchors.left: messageIcon.right + anchors.leftMargin: Theme.spacingM anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.top: parent.top anchors.bottom: parent.bottom - height: actionButtonHeight + contentSpacing - Row { - visible: expandedDelegateWrapper.actionsVisible - opacity: visible ? 1 : 0 + Column { + anchors.left: parent.left anchors.right: parent.right - anchors.bottom: parent.bottom - spacing: contentSpacing + anchors.top: parent.top + anchors.bottom: buttonArea.top + anchors.bottomMargin: contentSpacing + spacing: Theme.notificationContentSpacing - Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + Row { + width: parent.width + spacing: Theme.spacingXS + readonly property real reservedTrailingWidth: expandedDelegateSeparator.implicitWidth + Math.max(expandedDelegateTimeText.implicitWidth, 72) + spacing + + 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 + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { + id: expandedDelegateSeparator + text: (expandedDelegateTitleText.text.length > 0 && expandedDelegateTimeText.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 + 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 + } } - Repeater { - model: modelData?.actions || [] + StyledText { + id: bodyText + property bool hasMoreText: truncated + + text: modelData?.htmlBody || "" + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + elide: messageExpanded ? Text.ElideNone : Text.ElideRight + maximumLineCount: messageExpanded ? -1 : 2 + wrapMode: Text.WordWrap + visible: text.length > 0 + linkColor: Theme.primary + onLinkActivated: link => Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: mouse => { + if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { + NotificationService.toggleMessageExpansion(modelData?.notification?.id || ""); + } + } + + propagateComposedEvents: true + onPressed: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false; + } + } + onReleased: mouse => { + if (parent.hoveredLink) { + mouse.accepted = false; + } + } + } + } + } + + Item { + id: buttonArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: actionButtonHeight + contentSpacing + + Row { + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: contentSpacing + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Repeater { + model: modelData?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + height: actionButtonHeight + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + StyledText { + id: expandedActionText + text: { + const baseText = modelData.text || "Open"; + if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) + return `${baseText} (${index + 1})`; + return baseText; + } + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) + modelData.invoke(); + } + } + } + } Rectangle { + id: expandedDelegateDismissBtn property bool isHovered: false - width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 + width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight radius: Theme.notificationButtonCornerRadius color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" - StyledText { - id: expandedActionText - text: { - const baseText = modelData.text || "Open"; - if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) - return `${baseText} (${index + 1})`; - return baseText; + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } + } + + StyledText { + id: expandedClearText + text: I18n.tr("Dismiss") color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium anchors.centerIn: parent - elide: Text.ElideRight } MouseArea { @@ -635,53 +677,15 @@ Rectangle { cursorShape: Qt.PointingHandCursor onEntered: parent.isHovered = true onExited: parent.isHovered = false - onClicked: { - if (modelData && modelData.invoke) - modelData.invoke(); - } + onClicked: NotificationService.dismissNotification(modelData) } } } - - Rectangle { - id: expandedDelegateDismissBtn - property bool isHovered: false - - visible: expandedDelegateWrapper.actionsVisible - opacity: visible ? 1 : 0 - width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) - height: actionButtonHeight - radius: Theme.notificationButtonCornerRadius - color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" - - Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } - } - - StyledText { - id: expandedClearText - text: I18n.tr("Dismiss") - color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - anchors.centerIn: parent - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: parent.isHovered = true - onExited: parent.isHovered = false - onClicked: NotificationService.dismissNotification(modelData) - } - } } } } } } - } } DragHandler { @@ -722,18 +726,13 @@ Rectangle { } Row { - visible: !expanded && (cardHoverHandler.hovered || (keyboardNavigationActive && isGroupSelected)) - opacity: visible ? 1 : 0 + visible: !expanded anchors.right: clearButton.visible ? clearButton.left : parent.right anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL anchors.top: collapsedContent.bottom anchors.topMargin: contentSpacing spacing: contentSpacing - Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } - } - Repeater { model: notificationGroup?.latestNotification?.actions || [] @@ -783,11 +782,7 @@ Rectangle { property bool isHovered: false readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length - visible: !expanded && actionCount < 3 && (cardHoverHandler.hovered || (keyboardNavigationActive && isGroupSelected)) - opacity: visible ? 1 : 0 - Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } - } + visible: !expanded && actionCount < 3 anchors.right: parent.right anchors.rightMargin: Theme.spacingL anchors.top: collapsedContent.bottom @@ -819,6 +814,7 @@ Rectangle { MouseArea { anchors.fill: parent visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded + cursorShape: Qt.PointingHandCursor onClicked: { root.userInitiatedExpansion = true; NotificationService.toggleGroupExpansion(notificationGroup?.key || ""); @@ -834,15 +830,6 @@ Rectangle { anchors.rightMargin: Theme.spacingL width: compactMode ? 52 : 60 height: compactMode ? 24 : 28 - opacity: (cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } DankActionButton { anchors.left: parent.left @@ -949,7 +936,7 @@ Rectangle { MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton - z: 10 + z: -2 onClicked: mouse => { if (mouse.button === Qt.RightButton && notificationGroup) { notificationCardContextMenu.popup(); diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 5390f457..c2ea2871 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -141,9 +141,11 @@ PanelWindow { } property bool isTopCenter: SettingsData.notificationPopupPosition === -1 + property bool isBottomCenter: SettingsData.notificationPopupPosition === SettingsData.Position.BottomCenter + property bool isCenterPosition: isTopCenter || isBottomCenter anchors.top: isTopCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left - anchors.bottom: SettingsData.notificationPopupPosition === SettingsData.Position.Bottom || SettingsData.notificationPopupPosition === SettingsData.Position.Right + anchors.bottom: isBottomCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom || SettingsData.notificationPopupPosition === SettingsData.Position.Right anchors.left: SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom anchors.right: SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Right @@ -182,7 +184,7 @@ PanelWindow { function getBottomMargin() { const popupPos = SettingsData.notificationPopupPosition; - const isBottom = popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right; + const isBottom = isBottomCenter || popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right; if (!isBottom) return 0; @@ -192,7 +194,7 @@ PanelWindow { } function getLeftMargin() { - if (isTopCenter) + if (isCenterPosition) return screen ? (screen.width - implicitWidth) / 2 : 0; const popupPos = SettingsData.notificationPopupPosition; @@ -205,7 +207,7 @@ PanelWindow { } function getRightMargin() { - if (isTopCenter) + if (isCenterPosition) return 0; const popupPos = SettingsData.notificationPopupPosition; @@ -232,7 +234,7 @@ PanelWindow { visible: !win._finalized property real swipeOffset: 0 - readonly property real dismissThreshold: isTopCenter ? height * 0.4 : width * 0.35 + readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35 readonly property bool swipeActive: swipeDragHandler.active property bool swipeDismissing: false @@ -329,7 +331,7 @@ PanelWindow { anchors.right: parent.right anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL + (cardHoverHandler.hovered ? Theme.notificationHoverRevealMargin : 0) + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin height: collapsedContentHeight + extraHeight DankCircularImage { @@ -352,9 +354,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.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - popupIconSize / 2) : Math.max(0, textContainer.height / 2 - popupIconSize / 2) imageSource: { if (!notificationData) @@ -469,19 +469,9 @@ PanelWindow { anchors.topMargin: cardPadding anchors.rightMargin: Theme.spacingL iconName: "close" - iconSize: compactMode ? 16 : 18 - buttonSize: compactMode ? 24 : 28 + iconSize: compactMode ? 14 : 16 + buttonSize: compactMode ? 20 : 24 z: 15 - opacity: cardHoverHandler.hovered ? 1 : 0 - visible: opacity > 0 - enabled: cardHoverHandler.hovered - - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } onClicked: { if (notificationData && !win.exiting) @@ -500,7 +490,10 @@ PanelWindow { z: 20 Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } } Repeater { @@ -552,7 +545,10 @@ PanelWindow { visible: actionCount < 3 && cardHoverHandler.hovered opacity: visible ? 1 : 0 Behavior on opacity { - NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } } anchors.right: parent.right anchors.rightMargin: Theme.spacingL @@ -594,6 +590,7 @@ PanelWindow { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor propagateComposedEvents: true z: -1 onEntered: { @@ -624,8 +621,8 @@ PanelWindow { DragHandler { id: swipeDragHandler target: null - xAxis.enabled: !isTopCenter - yAxis.enabled: isTopCenter + xAxis.enabled: !isCenterPosition + yAxis.enabled: isCenterPosition onActiveChanged: { if (active || win.exiting || content.swipeDismissing) @@ -643,9 +640,11 @@ PanelWindow { if (win.exiting) return; - const raw = isTopCenter ? translation.y : translation.x; + const raw = isCenterPosition ? translation.y : translation.x; if (isTopCenter) { content.swipeOffset = Math.min(0, raw); + } else if (isBottomCenter) { + content.swipeOffset = Math.max(0, raw); } else { const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw); @@ -653,7 +652,7 @@ PanelWindow { } } - opacity: 1 - Math.abs(content.swipeOffset) / (isTopCenter ? content.height : content.width * 0.6) + opacity: 1 - Math.abs(content.swipeOffset) / (isCenterPosition ? content.height : content.width * 0.6) Behavior on opacity { enabled: !content.swipeActive @@ -674,7 +673,7 @@ PanelWindow { id: swipeDismissAnim target: content property: "swipeOffset" - to: isTopCenter ? -content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width) + to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width) duration: Theme.shortDuration easing.type: Easing.OutCubic onStopped: { @@ -686,18 +685,18 @@ PanelWindow { transform: [ Translate { id: swipeTx - x: isTopCenter ? 0 : content.swipeOffset - y: isTopCenter ? content.swipeOffset : 0 + x: isCenterPosition ? 0 : content.swipeOffset + y: isCenterPosition ? content.swipeOffset : 0 }, Translate { id: tx x: { - if (isTopCenter) + if (isCenterPosition) return 0; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; return isLeft ? -Anims.slidePx : Anims.slidePx; } - y: isTopCenter ? -Anims.slidePx : 0 + y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0 } ] } @@ -706,20 +705,22 @@ PanelWindow { id: enterX target: tx - property: isTopCenter ? "y" : "x" + property: isCenterPosition ? "y" : "x" from: { if (isTopCenter) return -Anims.slidePx; + if (isBottomCenter) + return Anims.slidePx; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; return isLeft ? -Anims.slidePx : Anims.slidePx; } to: 0 duration: Theme.notificationEnterDuration easing.type: Easing.BezierSpline - easing.bezierCurve: isTopCenter ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel + easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel onStopped: { if (!win.exiting && !win._isDestroying) { - if (isTopCenter) { + if (isCenterPosition) { if (Math.abs(tx.y) < 0.5) win.entered(); } else { @@ -737,11 +738,13 @@ PanelWindow { PropertyAnimation { target: tx - property: isTopCenter ? "y" : "x" + property: isCenterPosition ? "y" : "x" from: 0 to: { if (isTopCenter) return -Anims.slidePx; + if (isBottomCenter) + return Anims.slidePx; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; return isLeft ? -Anims.slidePx : Anims.slidePx; } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index a26eb6b2..0055472f 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -11,8 +11,9 @@ QtObject { readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real popupSpacing: Theme.spacingXS - readonly property int baseNotificationHeight: cardPadding * 2 + popupIconSize + actionButtonHeight + Theme.spacingS + popupSpacing + 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 property int maxTargetNotifications: 4 property var popupWindows: [] // strong refs to windows (live until exitFinished) property var destroyingWindows: new Set() diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index 421222c2..c57a9f29 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -211,22 +211,33 @@ Item { return I18n.tr("Top Left", "screen position option"); case SettingsData.Position.Right: return I18n.tr("Bottom Right", "screen position option"); + case SettingsData.Position.BottomCenter: + return I18n.tr("Bottom Center", "screen position option"); default: return I18n.tr("Top Right", "screen position option"); } } - options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option")] + options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option")] onValueChanged: value => { - if (value === I18n.tr("Top Right", "screen position option")) { + switch (value) { + case I18n.tr("Top Right", "screen position option"): SettingsData.set("notificationPopupPosition", SettingsData.Position.Top); - } else if (value === I18n.tr("Top Left", "screen position option")) { + break; + case I18n.tr("Top Left", "screen position option"): SettingsData.set("notificationPopupPosition", SettingsData.Position.Left); - } else if (value === I18n.tr("Top Center", "screen position option")) { + break; + case I18n.tr("Top Center", "screen position option"): SettingsData.set("notificationPopupPosition", -1); - } else if (value === I18n.tr("Bottom Right", "screen position option")) { + break; + case I18n.tr("Bottom Center", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.BottomCenter); + break; + case I18n.tr("Bottom Right", "screen position option"): SettingsData.set("notificationPopupPosition", SettingsData.Position.Right); - } else if (value === I18n.tr("Bottom Left", "screen position option")) { + break; + case I18n.tr("Bottom Left", "screen position option"): SettingsData.set("notificationPopupPosition", SettingsData.Position.Bottom); + break; } SettingsData.sendTestNotifications(); }