notifs/toasts: reworked notifications and toasts and how they display and work together. see pull request comment.
This commit is contained in:
parent
3e977f212d
commit
c1510b5476
12 changed files with 627 additions and 27 deletions
|
|
@ -140,6 +140,7 @@ Variants {
|
|||
property bool dashboard
|
||||
property bool utilities
|
||||
property bool sidebar
|
||||
property bool notifications
|
||||
|
||||
Component.onCompleted: Visibilities.load(scope.modelData, this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,52 @@ CustomMouseArea {
|
|||
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
|
||||
if (x < bar.implicitWidth) {
|
||||
bar.checkPopout(y);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ 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
|
||||
|
||||
|
|
@ -27,6 +31,7 @@ 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
|
||||
|
|
@ -54,6 +59,89 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ ColumnLayout {
|
|||
|
||||
StyledText {
|
||||
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
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
|
|
@ -42,7 +42,7 @@ ColumnLayout {
|
|||
anchors.centerIn: parent
|
||||
asynchronous: true
|
||||
active: opacity > 0
|
||||
opacity: Notifs.list.length > 0 ? 0 : 1
|
||||
opacity: Notifs.notClosed.length > 0 ? 0 : 1
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.large
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ StyledRect {
|
|||
|
||||
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 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"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ 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
|
||||
|
|
@ -20,13 +22,16 @@ Item {
|
|||
implicitWidth: Config.notifs.sizes.width + padding * 2
|
||||
implicitHeight: {
|
||||
const count = list.count;
|
||||
if (count === 0)
|
||||
if (count === 0 || !shouldShow)
|
||||
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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -55,7 +61,7 @@ Item {
|
|||
id: list
|
||||
|
||||
model: ScriptModel {
|
||||
values: Notifs.popups.filter(n => !n.closed)
|
||||
values: [...Notifs.notClosed]
|
||||
}
|
||||
|
||||
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 {
|
||||
Anim {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,22 +17,35 @@ 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: 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 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
|
||||
implicitWidth: Config.notifs.sizes.width
|
||||
implicitHeight: inner.implicitHeight
|
||||
|
||||
x: Config.notifs.sizes.width
|
||||
x: disableSlideIn ? 0 : Config.notifs.sizes.width
|
||||
Component.onCompleted: {
|
||||
x = 0;
|
||||
if (!root.disableSlideIn) {
|
||||
x = 0;
|
||||
}
|
||||
modelData.lock(this);
|
||||
}
|
||||
Component.onDestruction: modelData.unlock(this)
|
||||
|
||||
Behavior on x {
|
||||
enabled: !disableSlideIn
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
|
|
@ -134,8 +147,8 @@ StyledRect {
|
|||
Loader {
|
||||
id: appIcon
|
||||
|
||||
active: root.hasAppIcon || !root.hasImage
|
||||
asynchronous: true
|
||||
active: !root.hasImage || root.hasAppIcon
|
||||
asynchronous: false
|
||||
|
||||
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
|
||||
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
|
||||
|
|
@ -144,7 +157,11 @@ StyledRect {
|
|||
|
||||
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
|
||||
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
|
||||
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
|
||||
|
||||
|
|
@ -152,7 +169,8 @@ StyledRect {
|
|||
id: icon
|
||||
|
||||
active: root.hasAppIcon
|
||||
asynchronous: true
|
||||
asynchronous: false
|
||||
visible: active
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
|
|
@ -162,14 +180,19 @@ StyledRect {
|
|||
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
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !root.hasAppIcon
|
||||
asynchronous: true
|
||||
asynchronous: false
|
||||
visible: active
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02
|
||||
anchors.verticalCenterOffset: Appearance.font.size.large * 0.02
|
||||
|
|
@ -177,7 +200,11 @@ StyledRect {
|
|||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +349,9 @@ StyledRect {
|
|||
|
||||
StateLayer {
|
||||
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() {
|
||||
root.expanded = !root.expanded;
|
||||
|
|
@ -442,8 +471,12 @@ StyledRect {
|
|||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool isCritical: root.isCritical
|
||||
|
||||
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.preferredHeight: actionText.height + Appearance.padding.small * 2
|
||||
|
|
@ -452,7 +485,9 @@ StyledRect {
|
|||
|
||||
StateLayer {
|
||||
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 {
|
||||
action.modelData.invoke();
|
||||
|
|
@ -464,7 +499,9 @@ StyledRect {
|
|||
|
||||
anchors.centerIn: parent
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
154
modules/notifications/NotificationToast.qml
Normal file
154
modules/notifications/NotificationToast.qml
Normal 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 {}
|
||||
}
|
||||
}
|
||||
186
modules/notifications/NotificationToasts.qml
Normal file
186
modules/notifications/NotificationToasts.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -28,14 +28,13 @@ StyledRect {
|
|||
|
||||
border.width: 1
|
||||
border.color: {
|
||||
let colour = Colours.palette.m3outlineVariant;
|
||||
if (root.modelData.type === Toast.Success)
|
||||
colour = Colours.palette.m3success;
|
||||
return Colours.palette.m3success;
|
||||
if (root.modelData.type === Toast.Warning)
|
||||
colour = Colours.palette.m3secondaryContainer;
|
||||
return Colours.palette.m3secondaryContainer;
|
||||
if (root.modelData.type === Toast.Error)
|
||||
colour = Colours.palette.m3error;
|
||||
return Qt.alpha(colour, 0.3);
|
||||
return Colours.palette.m3error;
|
||||
return Colours.palette.m3outlineVariant;
|
||||
}
|
||||
|
||||
Elevation {
|
||||
|
|
|
|||
|
|
@ -77,8 +77,10 @@ Singleton {
|
|||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
|
||||
const shouldShowAsToast = !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar);
|
||||
const comp = notifComp.createObject(root, {
|
||||
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),
|
||||
popup: shouldShowAsToast,
|
||||
showAsToast: shouldShowAsToast,
|
||||
notification: notif
|
||||
});
|
||||
root.list = [comp, ...root.list];
|
||||
|
|
@ -143,6 +145,7 @@ Singleton {
|
|||
|
||||
property bool popup
|
||||
property bool closed
|
||||
property bool showAsToast: false
|
||||
property var locks: new Set()
|
||||
|
||||
property date time: new Date()
|
||||
|
|
@ -177,6 +180,38 @@ Singleton {
|
|||
property list<var> actions
|
||||
|
||||
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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue