summaryrefslogtreecommitdiff
path: root/modules/notifications
diff options
context:
space:
mode:
Diffstat (limited to 'modules/notifications')
-rw-r--r--modules/notifications/Content.qml58
-rw-r--r--modules/notifications/Notification.qml67
-rw-r--r--modules/notifications/NotificationToast.qml154
-rw-r--r--modules/notifications/NotificationToasts.qml186
-rw-r--r--modules/notifications/Wrapper.qml2
5 files changed, 449 insertions, 18 deletions
diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml
index 2d4590e..035a228 100644
--- a/modules/notifications/Content.qml
+++ b/modules/notifications/Content.qml
@@ -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 {}
}
diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml
index 95507fc..091da2c 100644
--- a/modules/notifications/Notification.qml
+++ b/modules/notifications/Notification.qml
@@ -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
}
diff --git a/modules/notifications/NotificationToast.qml b/modules/notifications/NotificationToast.qml
new file mode 100644
index 0000000..1ce334b
--- /dev/null
+++ b/modules/notifications/NotificationToast.qml
@@ -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 {}
+ }
+}
diff --git a/modules/notifications/NotificationToasts.qml b/modules/notifications/NotificationToasts.qml
new file mode 100644
index 0000000..96fe817
--- /dev/null
+++ b/modules/notifications/NotificationToasts.qml
@@ -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
+ }
+ }
+ }
+}
diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml
index 61acc56..4b54883 100644
--- a/modules/notifications/Wrapper.qml
+++ b/modules/notifications/Wrapper.qml
@@ -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