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 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue