diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index d0969d6a..2ffa6552 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -472,6 +472,8 @@ Singleton { property bool dockShowOverflowBadge: true property bool notificationOverlayEnabled: false + property bool notificationPopupShadowEnabled: true + property bool notificationPopupPrivacyMode: false property int overviewRows: 2 property int overviewColumns: 5 property real overviewScale: 0.16 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 91a69e63..24e83e39 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -297,6 +297,8 @@ var SPEC = { dockShowOverflowBadge: { def: true }, notificationOverlayEnabled: { def: false }, + notificationPopupShadowEnabled: { def: true }, + notificationPopupPrivacyMode: { def: false }, overviewRows: { def: 2, persist: false }, overviewColumns: { def: 5, persist: false }, overviewScale: { def: 0.16, persist: false }, diff --git a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml index 3bdfed06..134a4fd1 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -10,6 +10,17 @@ DankPopout { property bool notificationHistoryVisible: false property var triggerScreen: null + property real stablePopupHeight: 400 + property real _lastAlignedContentHeight: -1 + + function updateStablePopupHeight() { + const item = contentLoader.item; + const target = item ? Theme.px(item.implicitHeight, dpr) : 400; + if (Math.abs(target - _lastAlignedContentHeight) < 0.5) + return; + _lastAlignedContentHeight = target; + stablePopupHeight = target; + } NotificationKeyboardController { id: keyboardController @@ -21,10 +32,11 @@ DankPopout { } popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 - popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400 + popupHeight: stablePopupHeight positioning: "" animationScaleCollapsed: 1.0 animationOffset: 0 + suspendShadowWhileResizing: true screen: triggerScreen shouldBeVisible: notificationHistoryVisible @@ -68,14 +80,25 @@ DankPopout { Connections { target: contentLoader function onLoaded() { + root.updateStablePopupHeight(); if (root.shouldBeVisible) Qt.callLater(root.setupKeyboardNavigation); } } + Connections { + target: contentLoader.item + function onImplicitHeightChanged() { + root.updateStablePopupHeight(); + } + } + + onDprChanged: updateStablePopupHeight() + onShouldBeVisibleChanged: { if (shouldBeVisible) { NotificationService.onOverlayOpen(); + updateStablePopupHeight(); if (contentLoader.item) Qt.callLater(setupKeyboardNavigation); } else { diff --git a/quickshell/Modules/Notifications/Center/NotificationSettings.qml b/quickshell/Modules/Notifications/Center/NotificationSettings.qml index 38e40341..cffa29c1 100644 --- a/quickshell/Modules/Notifications/Center/NotificationSettings.qml +++ b/quickshell/Modules/Notifications/Center/NotificationSettings.qml @@ -262,6 +262,50 @@ Rectangle { } } + Item { + width: parent.width + height: Math.max(privacyRow.implicitHeight, privacyToggle.implicitHeight) + Theme.spacingS + + Row { + id: privacyRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "privacy_tip" + size: Theme.iconSizeSmall + color: SettingsData.notificationPopupPrivacyMode ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Privacy Mode") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Hide notification content until expanded") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + } + } + + DankToggle { + id: privacyToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.notificationPopupPrivacyMode + onToggled: toggled => SettingsData.set("notificationPopupPrivacyMode", toggled) + } + } + Rectangle { width: parent.width height: 1 diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index c4a74a0d..3ace8c55 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -20,12 +20,18 @@ PanelWindow { property bool exiting: false property bool _isDestroying: false property bool _finalized: false + property real _lastReportedAlignedHeight: -1 readonly property string clearText: I18n.tr("Dismiss") property bool descriptionExpanded: false + readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 onDescriptionExpandedChanged: { popupHeightChanged(); } onImplicitHeightChanged: { + const aligned = Theme.px(implicitHeight, dpr); + if (Math.abs(aligned - _lastReportedAlignedHeight) < 0.5) + return; + _lastReportedAlignedHeight = aligned; popupHeightChanged(); } @@ -35,7 +41,9 @@ PanelWindow { readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real actionButtonHeight: compactMode ? 20 : 24 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 privacyCollapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2) readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + readonly property real basePopupHeightPrivacy: cardPadding * 2 + privacyCollapsedContentHeight + actionButtonHeight + contentSpacing signal entered signal exitStarted @@ -108,6 +116,8 @@ PanelWindow { color: "transparent" implicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380 implicitHeight: { + if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) + return basePopupHeightPrivacy; if (!descriptionExpanded) return basePopupHeight; const bodyTextHeight = bodyText.contentHeight || 0; @@ -116,12 +126,26 @@ PanelWindow { return basePopupHeight + bodyTextHeight - collapsedBodyHeight; return basePopupHeight; } + + Behavior on implicitHeight { + enabled: !exiting && !_isDestroying + NumberAnimation { + id: implicitHeightAnim + duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.emphasized + } + } + onHasValidDataChanged: { if (!hasValidData && !exiting && !_isDestroying) { forceExit(); } } Component.onCompleted: { + _lastReportedAlignedHeight = Theme.px(implicitHeight, dpr); + if (SettingsData.notificationPopupPrivacyMode) + descriptionExpanded = false; if (hasValidData) { Qt.callLater(() => enterX.restart()); } else { @@ -130,6 +154,8 @@ PanelWindow { } onNotificationDataChanged: { if (!_isDestroying) { + if (SettingsData.notificationPopupPrivacyMode) + descriptionExpanded = false; wrapperConn.target = win.notificationData || null; notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null; } @@ -242,14 +268,22 @@ PanelWindow { scale: cardHoverHandler.hovered ? 1.01 : 1.0 transformOrigin: Item.Center + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + 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: cardHoverHandler.hovered ? 16 : 10 - property real shadowSpreadPx: cardHoverHandler.hovered ? 2 : 0 - property real shadowBaseAlpha: 0.60 + readonly property real radiusForShadow: Theme.cornerRadius + property real shadowBlurPx: SettingsData.notificationPopupShadowEnabled ? ((2 + radiusForShadow * 0.2) * (cardHoverHandler.hovered ? 1.2 : 1)) : 0 + property real shadowSpreadPx: SettingsData.notificationPopupShadowEnabled ? (radiusForShadow * (cardHoverHandler.hovered ? 0.06 : 0)) : 0 + property real shadowBaseAlpha: 0.35 readonly property real popupSurfaceAlpha: SettingsData.popupTransparency readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) @@ -267,18 +301,11 @@ PanelWindow { } } - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - Item { id: bgShadowLayer anchors.fill: parent anchors.margins: Theme.snap(4, win.dpr) - layer.enabled: !win._isDestroying && win.screenValid + layer.enabled: !win._isDestroying && win.screenValid && !implicitHeightAnim.running layer.smooth: false layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically @@ -288,7 +315,7 @@ PanelWindow { layer.effect: MultiEffect { id: shadowFx autoPaddingEnabled: true - shadowEnabled: true + shadowEnabled: SettingsData.notificationPopupShadowEnabled blurEnabled: false maskEnabled: false shadowBlur: Math.max(0, Math.min(1, content.shadowBlurPx / bgShadowLayer.blurMax)) @@ -300,7 +327,7 @@ PanelWindow { } Rectangle { - id: backgroundShape + id: shadowShapeSource anchors.fill: parent radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) @@ -310,7 +337,7 @@ PanelWindow { Rectangle { anchors.fill: parent - radius: backgroundShape.radius + radius: shadowShapeSource.radius visible: notificationData && notificationData.urgency === NotificationUrgency.Critical opacity: 1 clip: true @@ -346,6 +373,20 @@ PanelWindow { id: cardHoverHandler } + Connections { + target: cardHoverHandler + function onHoveredChanged() { + if (!notificationData || win.exiting || win._isDestroying) + return; + if (cardHoverHandler.hovered) { + if (notificationData.timer) + notificationData.timer.stop(); + } else if (notificationData.popup && notificationData.timer) { + notificationData.timer.restart(); + } + } + } + LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true @@ -354,6 +395,7 @@ PanelWindow { readonly property real expandedTextHeight: bodyText.contentHeight || 0 readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2) + readonly property real effectiveCollapsedHeight: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? win.privacyCollapsedContentHeight : win.collapsedContentHeight readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0 anchors.top: parent.top @@ -362,7 +404,8 @@ PanelWindow { anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin - height: collapsedContentHeight + extraHeight + height: effectiveCollapsedHeight + extraHeight + clip: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded DankCircularImage { id: iconContainer @@ -384,7 +427,15 @@ PanelWindow { height: popupIconSize anchors.left: parent.left anchors.top: parent.top - 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) + anchors.topMargin: { + if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) { + const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2; + return Math.max(0, headerSummary / 2 - popupIconSize / 2); + } + if (descriptionExpanded) + return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2); + return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2); + } imageSource: { if (!notificationData) @@ -499,6 +550,7 @@ PanelWindow { maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) wrapMode: Text.WordWrap visible: text.length > 0 + opacity: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? 0 : 1 linkColor: Theme.primary onLinkActivated: link => Qt.openUrlExternally(link) @@ -522,6 +574,14 @@ PanelWindow { } } } + + StyledText { + text: I18n.tr("Message Content", "notification privacy mode placeholder") + color: Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + width: parent.width + visible: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded && win.hasExpandableBody + } } } @@ -543,6 +603,25 @@ PanelWindow { } } + DankActionButton { + id: expandButton + + anchors.right: closeButton.left + anchors.rightMargin: Theme.spacingXS + anchors.top: parent.top + anchors.topMargin: cardPadding + iconName: descriptionExpanded ? "expand_less" : "expand_more" + iconSize: compactMode ? 14 : 16 + buttonSize: compactMode ? 20 : 24 + z: 15 + visible: SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody + + onClicked: { + if (win.hasExpandableBody) + win.descriptionExpanded = !win.descriptionExpanded; + } + } + Row { visible: cardHoverHandler.hovered opacity: visible ? 1 : 0 @@ -657,21 +736,14 @@ PanelWindow { cursorShape: Qt.PointingHandCursor propagateComposedEvents: true z: -1 - onEntered: { - if (notificationData && notificationData.timer) - notificationData.timer.stop(); - } - onExited: { - if (notificationData && notificationData.popup && notificationData.timer) - notificationData.timer.restart(); - } onClicked: mouse => { if (!notificationData || win.exiting) return; if (mouse.button === Qt.RightButton) { popupContextMenu.popup(); } else if (mouse.button === Qt.LeftButton) { - if (bodyText.hasMoreText || win.descriptionExpanded) { + const canExpand = bodyText.hasMoreText || win.descriptionExpanded || (SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody); + if (canExpand) { win.descriptionExpanded = !win.descriptionExpanded; } else if (notificationData.actions && notificationData.actions.length > 0) { notificationData.actions[0].invoke(); diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 02a58bb5..c5012fa8 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -289,7 +289,8 @@ QtObject { let currentY = topMargin; for (const win of activeWindows) { win.screenY = currentY; - currentY += win.implicitHeight + popupSpacing; + const popupHeight = win.alignedHeight || win.implicitHeight; + currentY += popupHeight + popupSpacing; } } diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index e38be3bc..2c3f9e75 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -270,6 +270,24 @@ Item { onToggled: checked => SettingsData.set("notificationCompactMode", checked) } + SettingsToggleRow { + settingKey: "notificationPopupShadowEnabled" + tags: ["notification", "popup", "shadow", "radius", "rounded"] + text: I18n.tr("Popup Shadow") + description: I18n.tr("Show drop shadow on notification popups") + checked: SettingsData.notificationPopupShadowEnabled + onToggled: checked => SettingsData.set("notificationPopupShadowEnabled", checked) + } + + SettingsToggleRow { + settingKey: "notificationPopupPrivacyMode" + tags: ["notification", "popup", "privacy", "body", "content", "hide"] + text: I18n.tr("Privacy Mode") + description: I18n.tr("Hide notification content until expanded; popups show collapsed by default") + checked: SettingsData.notificationPopupPrivacyMode + onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked) + } + Item { width: parent.width height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2 diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index d3f0798b..3dbc8205 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -25,10 +25,12 @@ Item { property real animationOffset: Theme.spacingL property list animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list animationExitCurve: Theme.expressiveCurves.emphasized + property bool suspendShadowWhileResizing: false property bool shouldBeVisible: false property var customKeyboardFocus: null property bool backgroundInteractive: true property bool contentHandlesKeys: false + property bool _resizeActive: false property real storedBarThickness: Theme.barHeight - 4 property real storedBarSpacing: 4 @@ -185,6 +187,26 @@ Item { readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr) + onAlignedHeightChanged: { + if (!suspendShadowWhileResizing || !shouldBeVisible) + return; + _resizeActive = true; + resizeSettleTimer.restart(); + } + onShouldBeVisibleChanged: { + if (!shouldBeVisible) { + _resizeActive = false; + resizeSettleTimer.stop(); + } + } + + Timer { + id: resizeSettleTimer + interval: 80 + repeat: false + onTriggered: root._resizeActive = false + } + readonly property real alignedX: Theme.snap((() => { const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; @@ -440,7 +462,7 @@ Item { readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) readonly property int blurMax: 64 - layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) layer.smooth: false layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)