notification popups

This commit is contained in:
end-4 2025-04-21 23:29:31 +02:00
parent 5dc0dc133d
commit 54fdf043c9
12 changed files with 240 additions and 46 deletions

View file

@ -0,0 +1,8 @@
import QtQuick
import Quickshell
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
property int sidebarRightOpenCount: 0
}

View file

@ -163,32 +163,31 @@ Item {
Layout.fillHeight: true
onPressed: Hyprland.dispatch(`workspace ${index+1}`)
width: workspaceButtonWidth
contentItem: StyledText {
z: 3
property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspacesShown + index + 1
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.small
text: `${workspaceValue}`
elide: Text.ElideRight
color: (monitor.activeWorkspace?.id == workspaceValue) ? Appearance.m3colors.m3onPrimary : (workspaceOccupied[index] ? Appearance.colors.colOnLayer1 : Appearance.colors.colOnLayer1Inactive)
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
background: Item {
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
StyledText {
z: 3
property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspacesShown + index + 1
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10"))
text: `${workspaceValue}`
elide: Text.ElideRight
color: (monitor.activeWorkspace?.id == workspaceValue) ? Appearance.m3colors.m3onPrimary : (workspaceOccupied[index] ? Appearance.colors.colOnLayer1 : Appearance.colors.colOnLayer1Inactive)
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
}

View file

@ -195,6 +195,7 @@ Singleton {
property int barCenterSideModuleWidth: 360
property int barPreferredSideSectionWidth: 400
property int sidebarWidth: 450
property int hyprlandGapsOut: 5
property int elevationMargin: 7
property int fabShadowRadius: 5

View file

@ -1,7 +1,8 @@
// From https://github.com/rafzby/circular-progressbar
// License: LGPL-3.0 - A copy can be found in `licenses` folder of repo
// Modified so it looks like in Material 3: https://m3.material.io/components/progress-indicators/specs
import QtQuick 2.9
import QtQuick
import "root:/modules/common"
Item {
id: root
@ -9,15 +10,19 @@ Item {
property int size: 30
property int lineWidth: 2
property real value: 0
property color primaryColor: "#70585D"
property color secondaryColor: "#FFF8F7"
property color primaryColor: Appearance.m3colors.m3onSecondaryContainer
property color secondaryColor: Appearance.m3colors.m3secondaryContainer
property real gapAngle: Math.PI / 10
property bool fill: false
property int fillOverflow: 2
property int animationDuration: 1000
property var easingType: Easing.OutCubic
width: size
height: size
signal animationFinished();
onValueChanged: {
canvas.degree = value * 360;
}
@ -62,12 +67,12 @@ Item {
// Secondary
ctx.beginPath();
ctx.arc(x, y, radius, progressAngle + gapAngle, startAngle - gapAngle);
ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle);
ctx.strokeStyle = root.secondaryColor;
ctx.stroke();
// Primary (value indication)
var endAngle = (progressAngle === startAngle) ? startAngle + epsilon : progressAngle;
var endAngle = progressAngle + (value > 0 ? 0 : epsilon);
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.strokeStyle = root.primaryColor;
@ -77,7 +82,7 @@ Item {
Behavior on degree {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.OutCubic
easing.type: root.easingType
}
}

View file

@ -13,13 +13,15 @@ import "./notification_utils.js" as NotificationUtils
Item {
id: root
property var notificationObject
property bool popup: false
property bool expanded: false
property bool enableAnimation: true
property int notificationListSpacing: 5
property bool ready: false
property int defaultTimeoutValue: 5000
Layout.fillWidth: true
clip: true
clip: !popup
Process {
id: closeSidebarProcess
@ -42,6 +44,16 @@ Item {
Component.onCompleted: {
root.ready = true
if (popup) timeoutTimer.start()
}
Timer {
id: timeoutTimer
interval: notificationObject.expireTimeout ?? root.defaultTimeoutValue
repeat: false
onTriggered: {
Notifications.timeoutNotification(notificationObject.id);
}
}
function destroyWithAnimation(delay = 0) {
@ -104,7 +116,7 @@ Item {
root.toggleExpanded()
}
// Flick right to dismiss
// Flick right to dismiss/discard
property real startX: 0
property real dragStartThreshold: 10
property real dragConfirmThreshold: 70
@ -124,7 +136,7 @@ Item {
}
onDragStartedChanged: () => {
// Prevent drag focus being shifted to parent flickable
root.parent.parent.parent.interactive = !dragStarted
if (root.parent.parent.parent.interactive !== undefined) root.parent.parent.parent.interactive = !dragStarted
root.enableAnimation = !dragStarted
}
onReleased: (mouse) => {
@ -196,6 +208,18 @@ Item {
}
}
}
DropShadow {
visible: popup
id: notificationShadow
anchors.fill: notificationBackground
source: notificationBackground
radius: 5
samples: radius * 2 + 1
color: Appearance.colors.colShadow
verticalOffset: 2
horizontalOffset: 0
}
}
@ -334,6 +358,21 @@ Item {
elide: Text.ElideRight
}
CircularProgress {
id: notificationProgress
visible: popup
Layout.alignment: Qt.AlignVCenter
lineWidth: 2
value: popup ? 1 : 0
size: 20
animationDuration: notificationObject.expireTimeout ?? root.defaultTimeoutValue
easingType: Easing.Linear
Component.onCompleted: {
value = 0
}
}
StyledText { // Time
id: notificationTimeText
Layout.fillWidth: false

View file

@ -0,0 +1,120 @@
import "root:/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
Scope {
id: screenCorners
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
Variants {
model: Quickshell.screens
PanelWindow {
id: root
visible: true
property var modelData
property Component notifComponent: NotificationWidget {}
property list<NotificationWidget> notificationWidgetList: []
screen: modelData
WlrLayershell.namespace: "quickshell:notificationPopup"
WlrLayershell.layer: WlrLayer.Overlay
exclusiveZone: 0
anchors {
top: true
right: true
bottom: true
}
mask: Region {
item: columnLayout
}
color: "transparent"
width: Appearance.sizes.notificationPopupWidth
height: columnLayout.implicitHeight
// Signal handlers to add/remove notifications
Connections {
target: Notifications
function onNotify(notification) {
if (GlobalStates.sidebarRightOpenCount > 0) {
return
}
// notificationRepeater.model = [notification, ...notificationRepeater.model]
const notif = root.notifComponent.createObject(columnLayout, {
notificationObject: notification,
popup: true
});
notificationWidgetList.unshift(notif)
// Remove stuff from t he column, add back
for (let i = 0; i < notificationWidgetList.length; i++) {
if (notificationWidgetList[i].parent === columnLayout) {
notificationWidgetList[i].parent = null;
}
}
// Add notification widgets to the column
for (let i = 0; i < notificationWidgetList.length; i++) {
if (notificationWidgetList[i].parent === null) {
notificationWidgetList[i].parent = columnLayout;
}
}
}
function onDiscard(id) {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject && widget.notificationObject.id === id) {
widget.destroyWithAnimation();
notificationWidgetList.splice(i, 1);
}
}
}
function onTimeout(id) {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject && widget.notificationObject.id === id) {
widget.destroyWithAnimation();
notificationWidgetList.splice(i, 1);
}
}
}
function onDiscardAll() {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject) {
widget.destroyWithAnimation();
}
}
notificationWidgetList = [];
}
}
ColumnLayout { // Scrollable window content
id: columnLayout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - Appearance.sizes.hyprlandGapsOut * 2
spacing: 0 // The widgets themselves have margins for spacing
Item {
implicitHeight: 1
implicitWidth: 1
}
// Notifications are added by the above signal handlers
}
}
}
}

View file

@ -57,8 +57,6 @@ Scope {
anchors {
top: true
left: true
right: true
}
mask: Region {
item: osdValuesWrapper

View file

@ -57,8 +57,6 @@ Scope {
anchors {
top: true
left: true
right: true
}
mask: Region {
item: osdValuesWrapper

View file

@ -1,6 +1,7 @@
import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "./quickToggles/"
import QtQuick
import QtQuick.Controls
@ -25,6 +26,10 @@ Scope {
visible: false
focusable: true
onVisibleChanged: {
GlobalStates.sidebarRightOpenCount += visible ? 1 : -1
}
property var modelData
screen: modelData
@ -191,6 +196,7 @@ Scope {
let panelWindow = sidebarVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = !panelWindow.visible;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}
@ -209,6 +215,7 @@ Scope {
let panelWindow = sidebarVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = true;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}

View file

@ -22,6 +22,7 @@ Item {
notificationWidgetList.push(notif)
})
}
function onNotify(notification) {
// notificationRepeater.model = [notification, ...notificationRepeater.model]
const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification });
@ -41,6 +42,7 @@ Item {
}
}
}
function onDiscard(id) {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
@ -50,6 +52,16 @@ Item {
}
}
}
function onDiscardAll() {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject) {
widget.destroyWithAnimation();
}
}
notificationWidgetList = [];
}
}
Flickable { // Scrollable window
@ -145,15 +157,7 @@ Item {
buttonIcon: "clear_all"
buttonText: "Clear"
onClicked: () => {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
// for (let i = 0; i < notificationWidgetList.length; i++) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject) {
widget.destroyWithAnimation();
}
}
notificationWidgetList = [];
Notifications.discardAll()
Notifications.discardAllNotifications()
}
}
}

View file

@ -18,6 +18,8 @@ Singleton {
signal initDone();
signal notify(notification: var);
signal discard(id: var);
signal discardAll();
signal timeout(id: var);
NotificationServer {
id: notifServer
@ -69,13 +71,24 @@ Singleton {
root.discard(id);
}
function discardAll() {
function discardAllNotifications() {
root.list = []
triggerListChange()
notifFileView.setText(JSON.stringify(root.list, null, 2))
notifServer.trackedNotifications.values.forEach((notif) => {
notif.dismiss()
})
root.discardAll();
}
function timeoutNotification(id) {
root.timeout(id);
}
function timeoutAll() {
root.list.forEach((notif) => {
root.timeout(notif.id);
})
}
function attemptInvokeAction(id, notifIdentifier) {

View file

@ -1,6 +1,7 @@
//@ pragma UseQApplication
import "./modules/bar/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/screenCorners/"
import "./modules/session/"
@ -13,6 +14,7 @@ import Quickshell
ShellRoot {
Bar {}
NotificationPopup {}
OnScreenDisplayBrightness {}
OnScreenDisplayVolume {}
ReloadPopup {}