notifs/toasts: reverted all changes to notifications to c0ea060f

This commit is contained in:
ATMDA 2025-11-12 20:57:38 -05:00
parent 7e6f327091
commit ffe14748a2
11 changed files with 61 additions and 609 deletions

View file

@ -8,10 +8,6 @@ import qs.modules.bar.popouts as BarPopouts
import qs.modules.utilities as Utilities
import qs.modules.utilities.toasts as Toasts
import qs.modules.sidebar as Sidebar
import qs.components
import qs.components.controls
import qs.components.effects
import qs.services
import Quickshell
import QtQuick
@ -31,7 +27,6 @@ Item {
readonly property alias utilities: utilities
readonly property alias toasts: toasts
readonly property alias sidebar: sidebar
readonly property alias clearAllButton: clearAllButton
anchors.fill: parent
anchors.margins: Config.border.thickness
@ -59,89 +54,6 @@ Item {
anchors.right: parent.right
}
// Clear all notifications button - positioned to the left of the notification panel
Item {
id: clearAllButton
readonly property bool hasNotifications: Notifs.notClosed.length > 0
readonly property bool panelVisible: notifications.height > 0 || notifications.implicitHeight > 0
readonly property bool shouldShow: hasNotifications && panelVisible
anchors.top: notifications.top
anchors.right: notifications.left
anchors.rightMargin: Appearance.padding.normal
anchors.topMargin: Appearance.padding.large
width: button.implicitWidth
height: button.implicitHeight
enabled: shouldShow
IconButton {
id: button
icon: "clear_all"
radius: Appearance.rounding.normal
padding: Appearance.padding.normal
font.pointSize: Math.round(Appearance.font.size.large * 1.2)
onClicked: {
// Clear all notifications
for (const notif of Notifs.list.slice())
notif.close();
}
Elevation {
anchors.fill: parent
radius: parent.radius
z: -1
level: button.stateLayer.containsMouse ? 4 : 3
}
}
// Keep notification panel visible when hovering over the button
MouseArea {
anchors.fill: button
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: {
if (notifications.content && Notifs.notClosed.length > 0) {
notifications.content.show();
}
}
onExited: {
// Panel will be hidden by Interactions.qml if mouse is not over panel or button
}
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
opacity: shouldShow ? 1 : 0
scale: shouldShow ? 1 : 0.5
}
Notifications.NotificationToasts {
id: notificationToasts
panels: root
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Config.border.thickness
anchors.rightMargin: Config.border.thickness
}
Session.Wrapper {
id: session

View file

@ -22,7 +22,7 @@ ColumnLayout {
StyledText {
Layout.fillWidth: true
text: Notifs.notClosed.length > 0 ? qsTr("%1 notification%2").arg(Notifs.notClosed.length).arg(Notifs.notClosed.length === 1 ? "" : "s") : qsTr("Notifications")
text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications")
color: Colours.palette.m3outline
font.family: Appearance.font.family.mono
font.weight: 500
@ -42,7 +42,7 @@ ColumnLayout {
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: Notifs.notClosed.length > 0 ? 0 : 1
opacity: Notifs.list.length > 0 ? 0 : 1
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.large

View file

@ -16,7 +16,7 @@ StyledRect {
required property string modelData
readonly property list<var> notifs: Notifs.notClosed.filter(notif => notif.appName === modelData)
readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData)
readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low"

View file

@ -1,57 +0,0 @@
import qs.components
import qs.components.effects
import qs.services
import qs.config
import qs.utils
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
StyledRect {
id: root
required property Notifs.Notif modelData
required property bool hasImage
required property bool hasAppIcon
radius: Appearance.rounding.full
color: modelData.getBadgeBackgroundColor()
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
Loader {
id: icon
active: root.hasAppIcon
asynchronous: false
visible: active
anchors.centerIn: parent
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: ColouredIcon {
anchors.fill: parent
source: Quickshell.iconPath(root.modelData.appIcon)
colour: root.modelData.getIconColor()
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
}
}
Loader {
active: !root.hasAppIcon
asynchronous: false
visible: active
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02
anchors.verticalCenterOffset: Appearance.font.size.large * 0.02
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
color: root.modelData.getIconColor()
font.pointSize: Appearance.font.size.large
}
}
}

View file

@ -13,8 +13,6 @@ Item {
required property Item panels
readonly property int padding: Appearance.padding.large
property bool shouldShow: false
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
@ -22,16 +20,13 @@ Item {
implicitWidth: Config.notifs.sizes.width + padding * 2
implicitHeight: {
const count = list.count;
if (count === 0 || !shouldShow)
if (count === 0)
return 0;
let height = (count - 1) * Appearance.spacing.smaller;
for (let i = 0; i < count; i++)
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0;
const screenHeight = QsWindow.window?.screen?.height ?? 0;
const maxHeight = Math.floor(screenHeight * 0.45);
if (visibilities && panels) {
if (visibilities.osd) {
const h = panels.osd.y - Config.border.rounding * 2 - padding * 2;
@ -46,8 +41,7 @@ Item {
}
}
const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2);
return Math.min(availableHeight, height + padding * 2);
return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2);
}
ClippingWrapperRectangle {
@ -61,7 +55,7 @@ Item {
id: list
model: ScriptModel {
values: [...Notifs.notClosed]
values: Notifs.popups.filter(n => !n.closed)
}
anchors.fill: parent
@ -198,52 +192,6 @@ Item {
}
}
Timer {
id: hideTimer
interval: 5000
onTriggered: {
if (list.count > 0)
root.shouldShow = false;
}
}
function show(): void {
if (list.count > 0) {
shouldShow = true;
hideTimer.restart();
}
}
Connections {
target: list
function onCountChanged(): void {
if (list.count === 0) {
root.shouldShow = false;
hideTimer.stop();
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: {
if (list.count > 0) {
root.shouldShow = true;
hideTimer.restart();
}
}
onExited: {
if (list.count > 0) {
root.shouldShow = false;
hideTimer.stop();
}
}
}
Behavior on implicitHeight {
Anim {}
}

View file

@ -17,31 +17,22 @@ StyledRect {
required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int nonAnimHeight: {
const baseHeight = summary.implicitHeight + inner.anchors.margins * 2;
return root.expanded
? baseHeight + appName.height + body.height + actions.height + actions.anchors.topMargin
: baseHeight + bodyPreview.height;
}
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
property bool expanded
property bool disableSlideIn: false
color: modelData.getBackgroundColor()
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.normal
implicitWidth: Config.notifs.sizes.width
implicitHeight: inner.implicitHeight
x: disableSlideIn ? 0 : Config.notifs.sizes.width
x: Config.notifs.sizes.width
Component.onCompleted: {
if (!root.disableSlideIn) {
x = 0;
}
x = 0;
modelData.lock(this);
}
Component.onDestruction: modelData.unlock(this)
Behavior on x {
enabled: !disableSlideIn
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
@ -143,18 +134,53 @@ StyledRect {
Loader {
id: appIcon
active: !root.hasImage || root.hasAppIcon
asynchronous: false
active: root.hasAppIcon || !root.hasImage
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: AppIconBadge {
modelData: root.modelData
hasImage: root.hasImage
hasAppIcon: root.hasAppIcon
sourceComponent: StyledRect {
radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
Loader {
id: icon
active: root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: ColouredIcon {
anchors.fill: parent
source: Quickshell.iconPath(root.modelData.appIcon)
colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
}
}
Loader {
active: !root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02
anchors.verticalCenterOffset: Appearance.font.size.large * 0.02
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
font.pointSize: Appearance.font.size.large
}
}
}
}
@ -296,7 +322,7 @@ StyledRect {
StateLayer {
radius: Appearance.rounding.full
color: root.modelData.getStateLayerColor()
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
function onClicked() {
root.expanded = !root.expanded;
@ -417,7 +443,7 @@ StyledRect {
required property var modelData
radius: Appearance.rounding.full
color: root.modelData.getActionBackgroundColor()
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2
Layout.preferredHeight: actionText.height + Appearance.padding.small * 2
@ -426,7 +452,7 @@ StyledRect {
StateLayer {
radius: Appearance.rounding.full
color: root.modelData.getStateLayerColor()
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface
function onClicked(): void {
action.modelData.invoke();
@ -438,7 +464,7 @@ StyledRect {
anchors.centerIn: parent
text: actionTextMetrics.elidedText
color: root.modelData.getActionTextColor()
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}

View file

@ -1,120 +0,0 @@
import qs.components
import qs.components.effects
import qs.services
import qs.config
import qs.utils
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.normal
color: Colours.palette.m3surface
border.width: 1
border.color: Colours.palette.m3outlineVariant
Elevation {
anchors.fill: parent
radius: parent.radius
opacity: parent.opacity
z: -1
level: 3
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
spacing: Appearance.spacing.normal
Item {
Layout.preferredWidth: Config.notifs.sizes.image
Layout.preferredHeight: Config.notifs.sizes.image
Loader {
id: imageLoader
active: root.hasImage
asynchronous: true
anchors.fill: parent
sourceComponent: ClippingRectangle {
radius: Appearance.rounding.full
implicitWidth: Config.notifs.sizes.image
implicitHeight: Config.notifs.sizes.image
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
}
Loader {
id: appIconLoader
active: root.hasAppIcon || !root.hasImage
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : parent.verticalCenter
anchors.right: root.hasImage ? parent.right : undefined
anchors.bottom: root.hasImage ? parent.bottom : undefined
sourceComponent: AppIconBadge {
modelData: root.modelData
hasImage: root.hasImage
hasAppIcon: root.hasAppIcon
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: title
Layout.fillWidth: true
text: root.modelData.summary
color: Colours.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
textFormat: Text.StyledText
text: root.modelData.body
color: Colours.palette.m3onSurface
opacity: 0.8
elide: Text.ElideRight
}
}
}
Behavior on border.color {
CAnim {}
}
}

View file

@ -1,186 +0,0 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import qs.services
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
required property Item panels
readonly property int spacing: Appearance.spacing.small
readonly property int maxToasts: 5
readonly property bool listVisible: panels.notifications.content.shouldShow
property bool flag
property var activeToasts: new Set()
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Appearance.padding.normal
implicitWidth: Config.notifs.sizes.width
implicitHeight: {
if (listVisible)
return 0;
let height = -spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i) as ToastWrapper;
if (item && !item.modelData.closed && !item.previewHidden)
height += item.implicitHeight + spacing;
}
return height;
}
opacity: listVisible ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
}
}
Repeater {
id: repeater
model: ScriptModel {
values: {
const toasts = [];
let visibleCount = 0;
for (const notif of Notifs.list) {
if (notif.showAsToast) {
root.activeToasts.add(notif);
}
if (notif.closed) {
root.activeToasts.delete(notif);
}
}
for (const notif of Notifs.list) {
if (root.activeToasts.has(notif)) {
toasts.push(notif);
if (notif.showAsToast && !notif.closed) {
visibleCount++;
if (visibleCount > root.maxToasts)
break;
}
}
}
return toasts;
}
onValuesChanged: root.flagChanged()
}
ToastWrapper {}
}
component ToastWrapper: MouseArea {
id: toast
required property int index
required property Notifs.Notif modelData
readonly property bool previewHidden: {
let extraHidden = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (item && item.modelData.closed)
extraHidden++;
}
return index >= root.maxToasts + extraHidden;
}
opacity: modelData.closed || previewHidden || !modelData.showAsToast ? 0 : 1
scale: modelData.closed || previewHidden || !modelData.showAsToast ? 0.7 : 1
anchors.topMargin: {
root.flag;
let margin = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i) as ToastWrapper;
if (item && !item.modelData.closed && !item.previewHidden)
margin += item.implicitHeight + root.spacing;
}
return margin;
}
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: toastInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: {
modelData.showAsToast = false;
modelData.close();
}
Component.onCompleted: modelData.lock(this)
onPreviewHiddenChanged: {
if (initAnim.running && previewHidden)
initAnim.stop();
}
Anim {
id: initAnim
Component.onCompleted: running = !toast.previewHidden
target: toast
properties: "opacity,scale"
from: 0
to: 1
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
ParallelAnimation {
running: toast.modelData.closed || (!toast.modelData.showAsToast && !toast.modelData.closed)
onStarted: toast.anchors.topMargin = toast.anchors.topMargin
onFinished: {
if (toast.modelData.closed)
toast.modelData.unlock(toast);
}
Anim {
target: toast
property: "opacity"
to: 0
}
Anim {
target: toast
property: "scale"
to: 0.7
}
}
NotificationToast {
id: toastInner
modelData: toast.modelData
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on anchors.topMargin {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}

View file

@ -8,8 +8,6 @@ Item {
required property var visibilities
required property Item panels
readonly property alias content: content
visible: height > 0
implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth)
implicitHeight: content.implicitHeight

View file

@ -28,13 +28,14 @@ StyledRect {
border.width: 1
border.color: {
let colour = Colours.palette.m3outlineVariant;
if (root.modelData.type === Toast.Success)
return Colours.palette.m3success;
colour = Colours.palette.m3success;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3secondaryContainer;
colour = Colours.palette.m3secondaryContainer;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3error;
return Colours.palette.m3outlineVariant;
colour = Colours.palette.m3error;
return Qt.alpha(colour, 0.3);
}
Elevation {

View file

@ -77,10 +77,8 @@ Singleton {
onNotification: notif => {
notif.tracked = true;
const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar);
const comp = notifComp.createObject(root, {
popup: shouldShowAsToast,
showAsToast: shouldShowAsToast,
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),
notification: notif
});
root.list = [comp, ...root.list];
@ -145,7 +143,6 @@ Singleton {
property bool popup
property bool closed
property bool showAsToast: false
property var locks: new Set()
property date time: new Date()
@ -179,74 +176,7 @@ Singleton {
property bool hasActionIcons
property list<var> actions
readonly property bool isCritical: urgency === NotificationUrgency.Critical
readonly property bool isLow: urgency === NotificationUrgency.Low
function getBackgroundColor(): color {
if (isCritical) return Colours.palette.m3secondaryContainer;
return Colours.tPalette.m3surfaceContainer;
}
function getBadgeBackgroundColor(): color {
if (isCritical) return Colours.palette.m3error;
if (isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2);
return Colours.palette.m3secondaryContainer;
}
function getIconColor(): color {
if (isCritical) return Colours.palette.m3onError;
if (isLow) return Colours.palette.m3onSurface;
return Colours.palette.m3onSecondaryContainer;
}
function getStateLayerColor(): color {
if (isCritical) return Colours.palette.m3onSecondaryContainer;
return Colours.palette.m3onSurface;
}
function getActionBackgroundColor(): color {
if (isCritical) return Colours.palette.m3secondary;
return Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
}
function getActionTextColor(): color {
if (isCritical) return Colours.palette.m3onSecondary;
return Colours.palette.m3onSurfaceVariant;
}
readonly property Timer timer: Timer {
id: toastTimer
running: notif.showAsToast
interval: {
let timeout = notif.expireTimeout;
if (timeout <= 0) {
switch (notif.urgency) {
case NotificationUrgency.Critical:
timeout = 10000;
break;
case NotificationUrgency.Normal:
timeout = 5000;
break;
case NotificationUrgency.Low:
timeout = 5000;
break;
default:
timeout = 5000;
}
}
return timeout;
}
onTriggered: {
if (Config.notifs.expire)
notif.popup = false;
if (notif.showAsToast) {
notif.showAsToast = false;
}
}
}
readonly property Timer popupTimer: Timer {
running: true
interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout
onTriggered: {