Files
DankMaterialShell/quickshell/Modules/Notifications/Center/NotificationCard.qml
T
bbedward 014c69b09a settings: tile color restyle and re-org, use materials own shift for
them towards theme
controlCenter: make a real grid, add numerous more tile varieties, add
icon size option
surfaces: fix elevation levels in general, and shift them down
settings: restyles and re-org
sliders: scale down universally
2026-09-18 14:32:55 -04:00

292 lines
13 KiB
QML

import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Notifications
import qs.Modules.Notifications as Notifications
Item {
id: root
property var notificationGroup
property bool expanded: NotificationService.expandedGroups[notificationGroup?.key] || false
property bool animateExpansion: true
property bool isGroupSelected: false
property int selectedNotificationIndex: -1
property bool keyboardNavigationActive: false
property var transientSurfaceTracker: null
property bool firstInList: true
property bool lastInList: true
property bool nested: false
readonly property color cardSurfaceColor: Theme.foregroundColor(nested ? Theme.chipSurface : Theme.cardSurface, Theme.isFloatingWindow(root))
readonly property color cardChipColor: nested ? Theme.withAlpha(Theme.onSurface, Theme.stateLayerFocus) : Theme.chipSurface
readonly property real expandedTargetHeight: {
let total = groupHeader.height;
for (const child of expandedContent.children) {
if (child.targetHeight === undefined)
continue;
total += Theme.groupedListGap + child.targetHeight;
}
return total;
}
readonly property real targetHeight: expanded ? expandedTargetHeight : collapsedCard.targetHeight
readonly property bool isAnimating: heightAnimation.running
width: parent ? parent.width : NotificationMetrics.popupWidth
height: targetHeight
clip: true
function toggleGroup() {
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
function invokeAction(action) {
if (!action?.invoke)
return;
action.invoke();
PopoutService.closeNotificationCenter();
}
function openContextMenu(item, x, y) {
notificationCardContextMenu.popupAnchorItem = item;
notificationCardContextMenu.showDropdownMenu();
}
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
notificationCardContextMenu.closeDropdownMenu();
}
}
Behavior on height {
enabled: root.animateExpansion && NotificationMetrics.animationsEnabled
NumberAnimation {
id: heightAnimation
duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: NotificationMetrics.expandCurve
}
}
Notifications.NotificationCard {
id: collapsedCard
width: parent.width
surfaceColor: root.cardSurfaceColor
chipColor: root.cardChipColor
visible: !root.expanded
notificationData: root.notificationGroup?.latestNotification ?? null
groupCount: root.notificationGroup?.count || 0
descriptionExpanded: NotificationService.expandedMessages[(notificationData?.notification?.id || "") + "_desc"] || false
firstInGroup: root.firstInList
lastInGroup: root.lastInList
keyboardSelected: root.keyboardNavigationActive && root.isGroupSelected
keyboardHints: keyboardSelected
animateHeight: false
onExpandRequested: NotificationService.toggleMessageExpansion((notificationData?.notification?.id || "") + "_desc")
onGroupToggleRequested: root.toggleGroup()
onDismissRequested: NotificationService.dismissGroup(root.notificationGroup?.key || "")
onActionRequested: action => root.invokeAction(action)
onBodyClicked: {
if (groupCount > 1) {
root.toggleGroup();
return;
}
root.invokeAction(contextActions.defaultAction(notificationData));
}
onContextMenuRequested: (x, y) => root.openContextMenu(collapsedCard, x, y)
}
Column {
id: expandedContent
objectName: "expandedContent"
property int swipingIndex: -1
property real swipingOffset: 0
width: parent.width
visible: root.expanded
spacing: Theme.groupedListGap
Rectangle {
id: groupHeader
width: parent.width
height: NotificationMetrics.cardPadding * 2 + NotificationMetrics.controlSize
radius: Theme.groupedListOuterRadius
color: "transparent"
border.width: root.keyboardNavigationActive && root.isGroupSelected ? Theme.focusRingWidth : 0
border.color: Theme.focusRingColor
StyledText {
anchors.left: parent.left
anchors.leftMargin: NotificationMetrics.cardPadding
anchors.right: groupControls.left
anchors.verticalCenter: parent.verticalCenter
text: root.notificationGroup?.appName || ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.primary
elide: Text.ElideRight
}
Row {
id: groupControls
anchors.right: parent.right
anchors.rightMargin: NotificationMetrics.cardPadding
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankButton {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Dismiss")
buttonHeight: NotificationMetrics.controlSize
horizontalPadding: NotificationMetrics.actionPadding
backgroundColor: "transparent"
textColor: Theme.primary
onClicked: NotificationService.dismissGroup(root.notificationGroup?.key || "")
}
DankButton {
anchors.verticalCenter: parent.verticalCenter
text: (root.notificationGroup?.count || 0).toString()
iconName: "expand_less"
buttonHeight: NotificationMetrics.controlSize
horizontalPadding: Theme.spacingS
backgroundColor: root.cardChipColor
textColor: Theme.onSurfaceVariant
Accessible.name: I18n.tr("Collapse")
onClicked: root.toggleGroup()
}
}
}
Repeater {
id: notificationRepeater
objectName: "notificationRepeater"
model: ScriptModel {
values: root.expanded ? (root.notificationGroup?.notifications?.slice(0, NotificationMetrics.expandedLimit) || []) : []
}
Item {
id: row
required property var modelData
required property int index
property real swipeOffset: 0
property bool dismissing: false
property bool collapsing: false
property var notificationToDismiss: null
readonly property real targetHeight: collapsing ? 0 : message.targetHeight
readonly property bool adjacentToSwipe: expandedContent.swipingIndex !== -1 && Math.abs(index - expandedContent.swipingIndex) === 1
readonly property real adjacentSwipeInfluence: adjacentToSwipe ? expandedContent.swipingOffset * NotificationMetrics.adjacentSwipeInfluence : 0
width: expandedContent.width
height: message.height
clip: true
Notifications.NotificationCard {
id: message
surfaceColor: root.cardSurfaceColor
chipColor: root.cardChipColor
x: row.swipeOffset + row.adjacentSwipeInfluence
width: parent.width
notificationData: row.modelData
firstInGroup: row.index === 0
lastInGroup: row.index === notificationRepeater.count - 1
keyboardSelected: root.keyboardNavigationActive && root.selectedNotificationIndex === row.index
keyboardHints: keyboardSelected
descriptionExpanded: NotificationService.expandedMessages[(notificationData?.notification?.id || "") + "_desc"] || false
onExpandRequested: NotificationService.toggleMessageExpansion((notificationData?.notification?.id || "") + "_desc")
onDismissRequested: NotificationService.dismissNotification(row.modelData)
onActionRequested: action => root.invokeAction(action)
onBodyClicked: root.invokeAction(contextActions.defaultAction(notificationData))
onContextMenuRequested: (x, y) => root.openContextMenu(message, x, y)
Behavior on x {
enabled: !swipe.active && !row.dismissing && !row.adjacentToSwipe && NotificationMetrics.animationsEnabled
NumberAnimation {
duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: NotificationMetrics.dismissCurve
}
}
}
DragHandler {
id: swipe
target: null
yAxis.enabled: false
grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType
onTranslationChanged: {
if (row.dismissing)
return;
row.swipeOffset = translation.x;
expandedContent.swipingOffset = translation.x;
}
onActiveChanged: {
if (active) {
expandedContent.swipingIndex = row.index;
return;
}
expandedContent.swipingIndex = -1;
expandedContent.swipingOffset = 0;
if (row.dismissing)
return;
if (Math.abs(row.swipeOffset) <= row.width * NotificationMetrics.swipeThreshold) {
row.swipeOffset = 0;
return;
}
row.notificationToDismiss = row.modelData;
row.dismissing = true;
dismissAnimation.start();
}
}
SequentialAnimation {
id: dismissAnimation
NumberAnimation {
target: row
property: "swipeOffset"
to: row.swipeOffset > 0 ? row.width : -row.width
duration: NotificationMetrics.animationsEnabled ? Theme.notificationExitDuration : 0
easing.type: Easing.BezierSpline
easing.bezierCurve: NotificationMetrics.dismissCurve
}
PropertyAction {
target: row
property: "collapsing"
value: true
}
NumberAnimation {
target: row
property: "height"
to: 0
duration: NotificationMetrics.animationsEnabled ? Theme.notificationExpandDuration : 0
easing.type: Easing.BezierSpline
easing.bezierCurve: NotificationMetrics.expandCurve
}
ScriptAction {
script: NotificationService.dismissNotification(row.notificationToDismiss)
}
}
}
}
}
NotificationActions {
id: contextActions
appName: root.notificationGroup?.appName || ""
desktopEntry: root.notificationGroup?.latestNotification?.desktopEntry || ""
onDismissRequested: NotificationService.dismissGroup(root.notificationGroup?.key || "")
onAppMuted: NotificationService.dismissGroup(root.notificationGroup?.key || "")
}
DankDropdown {
id: notificationCardContextMenu
showTrigger: false
popupWidth: NotificationMetrics.menuWidth
transientSurfaceTracker: root.transientSurfaceTracker
options: contextActions.items.map(item => item.label)
onValueChanged: value => {
const item = contextActions.items.find(item => item.label === value);
if (item)
contextActions.trigger(item.action);
}
}
}