notifs/toasts: reworked notifications and toasts and how they display and work together. see pull request comment.

This commit is contained in:
ATMDA 2025-11-12 14:51:22 -05:00
parent 3e977f212d
commit c1510b5476
12 changed files with 627 additions and 27 deletions

View file

@ -140,6 +140,7 @@ Variants {
property bool dashboard property bool dashboard
property bool utilities property bool utilities
property bool sidebar property bool sidebar
property bool notifications
Component.onCompleted: Visibilities.load(scope.modelData, this) Component.onCompleted: Visibilities.load(scope.modelData, this)
} }

View file

@ -198,6 +198,52 @@ CustomMouseArea {
utilitiesShortcutActive = false; utilitiesShortcutActive = false;
} }
// Show notifications panel on hover
// Try using inTopPanel first (works when panel is visible), fallback to corner detection only when panel is collapsed
const panelHeight = panels.notifications.height || panels.notifications.implicitHeight || 0;
const panelWidth = panels.notifications.width || panels.notifications.implicitWidth || Config.notifs.sizes.width;
const panelX = bar.implicitWidth + panels.notifications.x;
const isPanelCollapsed = panelHeight < 10; // Consider collapsed if height is very small
let showNotifications = inTopPanel(panels.notifications, x, y);
// Only use fallback corner detection when panel is collapsed
if (!showNotifications && isPanelCollapsed) {
// Use panel's actual width and position for fallback, with some padding
const cornerPadding = Config.border.rounding || 20;
showNotifications = x >= panelX - cornerPadding &&
x <= panelX + panelWidth + cornerPadding &&
y < Config.border.thickness + cornerPadding;
}
// Check if mouse is over the clear all button area
// Button is positioned to the left of the notification panel
if (!showNotifications && panels.notifications.height > 0 && panels.clearAllButton && panels.clearAllButton.visible) {
const buttonX = bar.implicitWidth + panels.clearAllButton.x;
const buttonY = Config.border.thickness + panels.clearAllButton.y;
const buttonWidth = panels.clearAllButton.width;
const buttonHeight = panels.clearAllButton.height;
const inButtonArea = x >= buttonX &&
x <= buttonX + buttonWidth &&
y >= buttonY &&
y <= buttonY + buttonHeight;
if (inButtonArea) {
showNotifications = true;
}
}
// Show or hide notification panel based on hover
if (panels.notifications.content) {
if (showNotifications) {
panels.notifications.content.show();
} else {
// Hide if not hovering over panel or button
panels.notifications.content.shouldShow = false;
}
}
// Show popouts on hover // Show popouts on hover
if (x < bar.implicitWidth) { if (x < bar.implicitWidth) {
bar.checkPopout(y); bar.checkPopout(y);

View file

@ -8,6 +8,10 @@ import qs.modules.bar.popouts as BarPopouts
import qs.modules.utilities as Utilities import qs.modules.utilities as Utilities
import qs.modules.utilities.toasts as Toasts import qs.modules.utilities.toasts as Toasts
import qs.modules.sidebar as Sidebar import qs.modules.sidebar as Sidebar
import qs.components
import qs.components.controls
import qs.components.effects
import qs.services
import Quickshell import Quickshell
import QtQuick import QtQuick
@ -27,6 +31,7 @@ Item {
readonly property alias utilities: utilities readonly property alias utilities: utilities
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias clearAllButton: clearAllButton
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: Config.border.thickness
@ -54,6 +59,89 @@ Item {
anchors.right: parent.right 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 { Session.Wrapper {
id: session id: session

View file

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

View file

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

View file

@ -13,6 +13,8 @@ Item {
required property Item panels required property Item panels
readonly property int padding: Appearance.padding.large readonly property int padding: Appearance.padding.large
property bool shouldShow: false
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
@ -20,13 +22,16 @@ Item {
implicitWidth: Config.notifs.sizes.width + padding * 2 implicitWidth: Config.notifs.sizes.width + padding * 2
implicitHeight: { implicitHeight: {
const count = list.count; const count = list.count;
if (count === 0) if (count === 0 || !shouldShow)
return 0; return 0;
let height = (count - 1) * Appearance.spacing.smaller; let height = (count - 1) * Appearance.spacing.smaller;
for (let i = 0; i < count; i++) for (let i = 0; i < count; i++)
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; 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 && panels) {
if (visibilities.osd) { if (visibilities.osd) {
const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; const h = panels.osd.y - Config.border.rounding * 2 - padding * 2;
@ -41,7 +46,8 @@ Item {
} }
} }
return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); const availableHeight = Math.min(maxHeight, screenHeight - Config.border.thickness * 2);
return Math.min(availableHeight, height + padding * 2);
} }
ClippingWrapperRectangle { ClippingWrapperRectangle {
@ -55,7 +61,7 @@ Item {
id: list id: list
model: ScriptModel { model: ScriptModel {
values: Notifs.popups.filter(n => !n.closed) values: [...Notifs.notClosed]
} }
anchors.fill: parent anchors.fill: parent
@ -192,6 +198,52 @@ 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 { Behavior on implicitHeight {
Anim {} Anim {}
} }

View file

@ -17,22 +17,35 @@ StyledRect {
required property Notifs.Notif modelData required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 readonly property bool isCritical: modelData.urgency === NotificationUrgency.Critical
readonly property bool isLow: modelData.urgency === NotificationUrgency.Low
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;
}
property bool expanded property bool expanded
property bool disableSlideIn: false
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer color: root.isCritical
? Colours.palette.m3secondaryContainer
: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
implicitWidth: Config.notifs.sizes.width implicitWidth: Config.notifs.sizes.width
implicitHeight: inner.implicitHeight implicitHeight: inner.implicitHeight
x: Config.notifs.sizes.width x: disableSlideIn ? 0 : Config.notifs.sizes.width
Component.onCompleted: { Component.onCompleted: {
if (!root.disableSlideIn) {
x = 0; x = 0;
}
modelData.lock(this); modelData.lock(this);
} }
Component.onDestruction: modelData.unlock(this) Component.onDestruction: modelData.unlock(this)
Behavior on x { Behavior on x {
enabled: !disableSlideIn
Anim { Anim {
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
} }
@ -134,8 +147,8 @@ StyledRect {
Loader { Loader {
id: appIcon id: appIcon
active: root.hasAppIcon || !root.hasImage active: !root.hasImage || root.hasAppIcon
asynchronous: true asynchronous: false
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
@ -144,7 +157,11 @@ StyledRect {
sourceComponent: StyledRect { sourceComponent: StyledRect {
radius: Appearance.rounding.full 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 color: {
if (root.isCritical) return Colours.palette.m3error;
if (root.isLow) return Colours.layer(Colours.palette.m3surfaceContainerHighest, 2);
return Colours.palette.m3secondaryContainer;
}
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
@ -152,7 +169,8 @@ StyledRect {
id: icon id: icon
active: root.hasAppIcon active: root.hasAppIcon
asynchronous: true asynchronous: false
visible: active
anchors.centerIn: parent anchors.centerIn: parent
@ -162,14 +180,19 @@ StyledRect {
sourceComponent: ColouredIcon { sourceComponent: ColouredIcon {
anchors.fill: parent anchors.fill: parent
source: Quickshell.iconPath(root.modelData.appIcon) 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 colour: {
if (root.isCritical) return Colours.palette.m3onError;
if (root.isLow) return Colours.palette.m3onSurface;
return Colours.palette.m3onSecondaryContainer;
}
layer.enabled: root.modelData.appIcon.endsWith("symbolic") layer.enabled: root.modelData.appIcon.endsWith("symbolic")
} }
} }
Loader { Loader {
active: !root.hasAppIcon active: !root.hasAppIcon
asynchronous: true asynchronous: false
visible: active
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02
anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 anchors.verticalCenterOffset: Appearance.font.size.large * 0.02
@ -177,7 +200,11 @@ StyledRect {
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) 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 color: {
if (root.isCritical) return Colours.palette.m3onError;
if (root.isLow) return Colours.palette.m3onSurface;
return Colours.palette.m3onSecondaryContainer;
}
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
} }
} }
@ -322,7 +349,9 @@ StyledRect {
StateLayer { StateLayer {
radius: Appearance.rounding.full radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface color: root.isCritical
? Colours.palette.m3onSecondaryContainer
: Colours.palette.m3onSurface
function onClicked() { function onClicked() {
root.expanded = !root.expanded; root.expanded = !root.expanded;
@ -442,8 +471,12 @@ StyledRect {
required property var modelData required property var modelData
readonly property bool isCritical: root.isCritical
radius: Appearance.rounding.full radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) color: isCritical
? Colours.palette.m3secondary
: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2
Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2
@ -452,7 +485,9 @@ StyledRect {
StateLayer { StateLayer {
radius: Appearance.rounding.full radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface color: isCritical
? Colours.palette.m3onSecondary
: Colours.palette.m3onSurface
function onClicked(): void { function onClicked(): void {
action.modelData.invoke(); action.modelData.invoke();
@ -464,7 +499,9 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
text: actionTextMetrics.elidedText text: actionTextMetrics.elidedText
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant color: isCritical
? Colours.palette.m3onSecondary
: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.small
} }

View file

@ -0,0 +1,154 @@
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: 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: appIcon
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
}
}
}
}
}
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

@ -0,0 +1,186 @@
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,6 +8,8 @@ Item {
required property var visibilities required property var visibilities
required property Item panels required property Item panels
readonly property alias content: content
visible: height > 0 visible: height > 0
implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth)
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight

View file

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

View file

@ -77,8 +77,10 @@ Singleton {
onNotification: notif => { onNotification: notif => {
notif.tracked = true; notif.tracked = true;
const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar);
const comp = notifComp.createObject(root, { const comp = notifComp.createObject(root, {
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), popup: shouldShowAsToast,
showAsToast: shouldShowAsToast,
notification: notif notification: notif
}); });
root.list = [comp, ...root.list]; root.list = [comp, ...root.list];
@ -143,6 +145,7 @@ Singleton {
property bool popup property bool popup
property bool closed property bool closed
property bool showAsToast: false
property var locks: new Set() property var locks: new Set()
property date time: new Date() property date time: new Date()
@ -177,6 +180,38 @@ Singleton {
property list<var> actions property list<var> actions
readonly property Timer timer: Timer { 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 running: true
interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout
onTriggered: { onTriggered: {