tray menu refresh

This commit is contained in:
Zacharias-Brohn
2025-11-08 14:42:20 +01:00
parent 70916e4755
commit 6d52325d89
12 changed files with 748 additions and 454 deletions
-47
View File
@@ -1,47 +0,0 @@
// CustomTrayMenu.qml
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Window // for Window, flags
import qs.Modules
PopupWindow {
id: popup
color: "#00202020"
required property QsMenuOpener trayMenu
Column {
id: contentColumn
anchors.fill: parent
spacing: 4
Repeater {
id: repeater
model: popup.trayMenu.children
Row {
id: entryRow
anchors.fill: parent
property var entry: modelData
MouseArea {
anchors.fill: parent
onClicked: {
if (entryRow.entry.triggered) {
entryRow.entry.triggered()
}
popup.visible = false
}
}
Image {
source: entryRow.entry.icon
width: 20; height: 20
visible: entryRow.entry.icon !== ""
}
Text {
text: entryRow.entry.text
color: "black"
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
font.family: "Material Symbols Rounded" font.family: "Material Symbols Rounded"
font.pixelSize: 16 font.pixelSize: 16
text: "󰢮" text: "\ue30f"
color: "#ffffff" color: "#ffffff"
} }
-91
View File
@@ -1,91 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
PopupWindow {
id: root
required property QsMenuHandle menu
property int height: entryCount * ( entryHeight + 3 )
property int entryHeight: 30
property int entryCount: 0
implicitWidth: 300
implicitHeight: height
color: "transparent"
QsMenuOpener {
id: menuOpener
menu: root.menu
}
HyprlandFocusGrab {
id: grab
windows: [ root ]
onCleared: {
root.visible = false;
}
}
onVisibleChanged: {
grab.active = root.visible;
}
Rectangle {
id: menuRect
anchors.fill: parent
color: "#90000000"
radius: 8
border.color: "#10FFFFFF"
ColumnLayout {
id: columnLayout
anchors.fill: parent
anchors.margins: 5
spacing: 2
Repeater {
id: repeater
model: menuOpener.children
Rectangle {
id: menuItem
width: root.implicitWidth
Layout.maximumWidth: parent.width
Layout.fillHeight: true
height: root.entryHeight
color: mouseArea.containsMouse && !modelData.isSeparator ? "#15FFFFFF" : "transparent"
radius: 4
visible: modelData.isSeparator ? false : true
required property QsMenuEntry modelData
Component.onCompleted: {
if ( !modelData.isSeparator ) {
root.entryCount += 1;
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: true
acceptedButtons: Qt.LeftButton
onClicked: {
if ( !menuItem.modelData.hasChildren ) {
menuItem.modelData.triggered();
root.visible = false;
}
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 10
text: menuItem.modelData.text
color: "white"
}
}
}
}
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ Item {
Process { Process {
id: swayncProcess id: swayncProcess
command: ["swaync-client", "-t", "-sw"] command: ["sh", "-c", "qs -p /home/zach/GitProjects/z-bar-qt/notification-test/shell.qml ipc call root showCenter"]
running: false running: false
} }
+13 -18
View File
@@ -3,8 +3,8 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import Caelestia
import Quickshell import Quickshell
import Quickshell.DBusMenu
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
@@ -38,35 +38,30 @@ MouseArea {
smooth: false smooth: false
asynchronous: true asynchronous: true
ImageAnalyser {
id: analyser
sourceItem: icon
rescaleSize: 22
}
TrayMenu { TrayMenu {
id: trayMenu id: trayMenu
menu: menuOpener trayMenu: root.item?.menu
trayItemRect: root.globalPos trayItemRect: root.globalPos
bar: root.bar bar: root.bar
} }
} }
Connections {
target: trayMenu
function onVisibleChanged() {
if ( !trayMenu.visible ) {
trayMenu.trayMenu = null;
}
}
}
onClicked: { onClicked: {
if ( mouse.button === Qt.LeftButton ) { if ( mouse.button === Qt.LeftButton ) {
root.item.activate(); root.item.activate();
} else if ( mouse.button === Qt.RightButton ) { } else if ( mouse.button === Qt.RightButton ) {
if ( trayMenu.menu != menuOpener ) { trayMenu.trayMenu = root.item?.menu;
trayMenu.menu = menuOpener;
}
trayMenu.visible = !trayMenu.visible; trayMenu.visible = !trayMenu.visible;
console.log(root.x); trayMenu.focusGrab = true;
} }
} }
QsMenuOpener {
id: menuOpener
menu: root.item?.menu
}
} }
+118 -136
View File
@@ -1,24 +1,34 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.DBusMenu
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import Quickshell.Hyprland import Quickshell.Hyprland
import QtQml
PanelWindow { PanelWindow {
id: root id: root
signal menuActionTriggered() signal menuActionTriggered()
required property QsMenuOpener menu required property QsMenuHandle trayMenu
required property point trayItemRect required property point trayItemRect
required property PanelWindow bar required property PanelWindow bar
property var menuStack: [] property var menuStack: []
property real scaleValue: 0 property real scaleValue: 0
property alias focusGrab: grab.active
property int height: calcSize("h")
property int entryHeight: 30 property int entryHeight: 30
property int maxWidth: calcSize("w") property int biggestWidth: 0
QsMenuOpener {
id: menuOpener
menu: root.trayMenu
}
// onTrayMenuChanged: {
// listLayout.forceLayout();
// }
visible: false visible: false
color: "transparent" color: "transparent"
@@ -29,61 +39,29 @@ PanelWindow {
bottom: true bottom: true
} }
function calcSize(String) { mask: Region { id: mask; item: menuRect }
if ( String === "w" ) {
let menuWidth = 0;
for ( let i = 0; i < listLayout.count; i++ ) {
if ( !listLayout.model.values[i].isSeparator ) {
let entry = listLayout.model.values[i];
tempMetrics.text = entry.text;
let textWidth = tempMetrics.width + 20 + (entry.icon ?? "" ? 30 : 0) + (entry.hasChildren ? 30 : 0);
if ( textWidth > menuWidth ) {
menuWidth = textWidth;
}
}
}
return menuWidth;
}
if ( String === "h" ) {
let count = 0;
let separatorCount = 0;
for (let i = 0; i < listLayout.count; i++) {
if (!listLayout.model.values[i].isSeparator) {
count++;
} else {
separatorCount++;
}
}
if ( root.menuStack.length > 0 ) {
backEntry.visible = true;
count++;
}
return (count * entryHeight) + ((count - 1) * 2) + (separatorCount * 3) + 10;
}
}
function goBack() { function goBack() {
if ( root.menuStack.length > 0 ) { if ( root.menuStack.length > 0 ) {
root.menu = root.menuStack.pop(); menuChangeAnimation.start();
root.biggestWidth = 0;
root.trayMenu = root.menuStack.pop();
listLayout.positionViewAtBeginning();
backEntry.visible = false; backEntry.visible = false;
} }
} }
function updateMask() {
root.mask.changed();
}
onVisibleChanged: { onVisibleChanged: {
if ( !visible ) { if ( visible ) {
goBack();
} else {
scaleValue = 0; scaleValue = 0;
scaleAnimation.start(); scaleAnimation.start();
} }
} }
TextMetrics {
id: tempMetrics
text: ""
}
NumberAnimation { NumberAnimation {
id: scaleAnimation id: scaleAnimation
target: root target: root
@@ -92,90 +70,85 @@ PanelWindow {
to: 1 to: 1
duration: 150 duration: 150
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onStopped: {
root.updateMask();
}
} }
MouseArea { HyprlandFocusGrab {
anchors.fill: parent id: grab
acceptedButtons: Qt.LeftButton | Qt.RightButton windows: [ root ]
onClicked: { active: false
onCleared: {
root.visible = false; root.visible = false;
} }
} }
Behavior on menu { SequentialAnimation {
SequentialAnimation { id: menuChangeAnimation
ParallelAnimation { ParallelAnimation {
NumberAnimation { NumberAnimation {
duration: MaterialEasing.standardTime / 2 duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: MaterialEasing.expressiveEffects
from: 0 from: 0
property: "x" property: "x"
target: translateAnim target: translateAnim
to: -listLayout.width / 2 to: -listLayout.width / 2
}
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.standard
from: 1
property: "opacity"
target: columnLayout
to: 0
}
} }
PropertyAction { NumberAnimation {
property: "menu" duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.standard
from: 1
property: "opacity"
target: columnLayout target: columnLayout
to: 0
}
}
PropertyAction {
property: "menu"
target: columnLayout
}
ParallelAnimation {
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.standard
from: 0
property: "opacity"
target: columnLayout
to: 1
} }
ParallelAnimation { NumberAnimation {
NumberAnimation { duration: MaterialEasing.standardTime / 2
duration: MaterialEasing.standardTime / 2 easing.bezierCurve: MaterialEasing.expressiveEffects
easing.bezierCurve: MaterialEasing.standard from: listLayout.width / 2
from: 0 property: "x"
property: "opacity" target: translateAnim
target: columnLayout to: 0
to: 1
}
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.expressiveEffects
from: listLayout.width / 2
property: "x"
target: translateAnim
to: 0
}
} }
} }
} }
onMenuActionTriggered: {
if ( root.menuStack.length > 0 ) {
backEntry.visible = true;
}
}
Rectangle { Rectangle {
id: menuRect id: menuRect
x: root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 )
y: root.trayItemRect.y - 5 y: Math.round( root.trayItemRect.y - 5 )
implicitHeight: root.height implicitWidth: listLayout.contentWidth + 10
implicitWidth: root.maxWidth + 20 implicitHeight: listLayout.contentHeight + ( root.menuStack.length > 0 ? root.entryHeight + 10 : 10 )
color: "#80151515" color: "#80151515"
radius: 8 radius: 8
border.color: "#40FFFFFF" border.color: "#40FFFFFF"
clip: true clip: true
Behavior on implicitHeight {
NumberAnimation {
duration: MaterialEasing.standardTime
easing.bezierCurve: MaterialEasing.standard
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: MaterialEasing.standardTime
easing.bezierCurve: MaterialEasing.standard
}
}
transform: [ transform: [
Scale { Scale {
origin.x: menuRect.width / 2 origin.x: menuRect.width / 2
@@ -185,6 +158,19 @@ PanelWindow {
} }
] ]
Behavior on implicitWidth {
NumberAnimation {
duration: MaterialEasing.standardTime
easing.bezierCurve: MaterialEasing.standard
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: MaterialEasing.standardTime
easing.bezierCurve: MaterialEasing.standard
}
}
ColumnLayout { ColumnLayout {
id: columnLayout id: columnLayout
@@ -201,31 +187,11 @@ PanelWindow {
ListView { ListView {
id: listLayout id: listLayout
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: root.height - ( root.menuStack.length > 0 ? root.entryHeight + 10 : 0 ) Layout.preferredHeight: contentHeight
spacing: 2 spacing: 2
model: ScriptModel { contentWidth: root.biggestWidth
values: [...root.menu?.children.values] contentHeight: contentItem.childrenRect.height
} model: menuOpener.children
add: Transition {
NumberAnimation {
property: "x"
from: listLayout.width
to: 0
duration: 200
easing.type: Easing.OutCubic
}
}
move: Transition {
NumberAnimation {
property: "x"
from: 0
to: -listLayout.width
duration: 200
easing.type: Easing.InCubic
}
}
delegate: Rectangle { delegate: Rectangle {
id: menuItem id: menuItem
@@ -235,7 +201,7 @@ PanelWindow {
} }
property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled
property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled
width: root.implicitWidth width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: menuItem.modelData.isSeparator ? 1 : root.entryHeight height: menuItem.modelData.isSeparator ? 1 : root.entryHeight
@@ -243,6 +209,19 @@ PanelWindow {
radius: 4 radius: 4
visible: true visible: true
Component.onCompleted: {
var biggestWidth = root.biggestWidth;
var currentWidth = widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20;
if ( currentWidth > biggestWidth ) {
root.biggestWidth = currentWidth;
}
}
TextMetrics {
id: widthMetrics
text: menuItem.modelData.text
}
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
@@ -254,12 +233,15 @@ PanelWindow {
if ( !menuItem.modelData.hasChildren ) { if ( !menuItem.modelData.hasChildren ) {
if ( menuItem.modelData.enabled ) { if ( menuItem.modelData.enabled ) {
menuItem.modelData.triggered(); menuItem.modelData.triggered();
root.menuActionTriggered();
root.visible = false; root.visible = false;
} }
} else { } else {
root.menuStack.push(root.menu); root.menuStack.push(root.trayMenu);
root.menu = menuItem.child; menuChangeAnimation.start();
root.biggestWidth = 0;
root.trayMenu = menuItem.modelData;
listLayout.positionViewAtBeginning();
root.menuActionTriggered();
} }
} }
} }
+155
View File
@@ -0,0 +1,155 @@
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import QtQuick.Layouts
import QtQuick
import Quickshell.Services.Notifications
PanelWindow {
id: root
color: "transparent"
anchors {
top: true
right: true
left: true
bottom: true
}
mask: Region { item: backgroundRect }
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
required property Notification notif
required property int centerX
required property list<int> notifIndex
property int index: notifIndex.indexOf(notif.id)
signal notifDestroy()
Component.onCompleted: {
openAnim.start();
}
Timer {
id: timeout
interval: 5000
onTriggered: {
closeAnim.start();
}
}
NumberAnimation {
id: openAnim
target: backgroundRect
property: "x"
from: root.centerX
to: root.centerX - backgroundRect.implicitWidth - 20
duration: 200
easing.type: Easing.InOutQuad
onStopped: { timeout.start(); }
}
NumberAnimation {
id: closeAnim
target: backgroundRect
property: "x"
from: root.centerX - backgroundRect.implicitWidth - 20
to: root.centerX
duration: 200
easing.type: Easing.InOutQuad
onStopped: {
root.destroy();
root.notifDestroy();
}
}
Rectangle {
id: backgroundRect
implicitWidth: 400
implicitHeight: 80
x: root.centerX - implicitWidth - 20
y: 34 + 20 + ( root.index * ( implicitHeight + 10 ))
color: "#801a1a1a"
border.color: "#555555"
radius: 8
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
IconImage {
source: root.notif.image
Layout.preferredWidth: 64
Layout.preferredHeight: 64
Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft
visible: root.notif.image !== ""
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: root.notif.image !== "" ? 0 : 16
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Text {
text: root.notif.appName
color: "white"
font.bold: true
font.pointSize: 12
elide: Text.ElideRight
wrapMode: Text.NoWrap
Layout.fillWidth: true
}
Text {
text: root.notif.summary
color: "white"
font.pointSize: 10
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Text {
text: root.notif.body
color: "#dddddd"
font.pointSize: 8
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
width: 18
height: 18
color: closeArea.containsMouse ? "#FF6077" : "transparent"
radius: 9
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "white" : "#888888"
font.pointSize: 12
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.notif.dismiss();
root.visible = false;
}
}
}
}
}
+420
View File
@@ -0,0 +1,420 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
import Quickshell.Io
import QtQuick.Layouts
import QtQuick.Controls.FluentWinUI3
import QtQuick.Effects
import QtQuick
import Quickshell.Services.Notifications
PanelWindow {
id: root
color: "transparent"
anchors {
top: true
right: true
left: true
bottom: true
}
required property list<Notification> notifications
property bool centerShown: false
property alias posX: backgroundRect.x
property alias doNotDisturb: dndSwitch.checked
property alias groupedData: groupedData
visible: false
IpcHandler {
id: ipcHandler
target: "root"
function showCenter(): void { root.centerShown = !root.centerShown; }
}
onVisibleChanged: {
if ( root.visible ) {
showAnimation.start();
}
}
onCenterShownChanged: {
if ( !root.centerShown ) {
closeAnimation.start();
closeTimer.start();
} else {
root.visible = true;
}
}
Timer {
id: closeTimer
interval: 300
onTriggered: {
root.visible = false;
}
}
NumberAnimation {
id: showAnimation
target: backgroundRect
property: "x"
from: Screen.width
to: Screen.width - backgroundRect.implicitWidth - 10
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
id: closeAnimation
target: backgroundRect
property: "x"
from: Screen.width - backgroundRect.implicitWidth - 10
to: Screen.width
duration: 300
easing.type: Easing.OutCubic
}
ListModel {
id: groupedData
property int totalCount: 0
property var groupMap: ({})
function mapGroups() {
groupedData.groupMap = {};
for ( var i = 0; i < groupedData.count; i++ ) {
var name = get(i).name;
groupMap[ name ] = i;
}
}
function updateCount() {
var count = 0;
for ( var i = 0; i < groupedData.count; i++ ) {
count += get(i).notifications.count;
}
totalCount = count;
}
function ensureGroup(appName) {
for ( var i = 0; i < count; i++ ) {
if ( get(i).name === appName ) {
return get(i).notifications;
}
}
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root);
append({ name: appName, notifications: model });
mapGroups();
return model;
}
function addNotification(notif) {
var appName = notif.appName
var model = ensureGroup(appName);
model.insert(0, notif);
updateCount();
}
function removeNotification(notif) {
var appName = notif.appName || "Unknown";
var group = get( groupMap[ appName ]);
if ( group.name === appName ) {
root.notifications[ idMap[ notif.id ]].dismiss();
if ( group.notifications.count === 0 ) {
remove( groupMap[ appName ], 1 );
mapGroups();
}
}
updateCount();
}
function removeGroup(notif) {
var appName = notif.appName || "Unknown";
var group = get(groupMap[ appName ]);
if ( group.name === appName ) {
for ( var i = 0; i < group.notifications.count; i++ ) {
var item = group.notifications.get( i );
item.dismiss();
}
remove( groupMap[ appName ], 1 );
updateCount();
mapGroups();
}
}
function resetGroups(notifications) {
groupedData.clear();
for (var i = 0; i < root.notifications.length; i++) {
addNotification(root.notifications[i]);
}
updateCount();
}
Component.onCompleted: {
resetGroups(root.notifications)
}
}
Connections {
target: root
function onNotificationsChanged() {
if ( root.notifications.length > groupedData.totalCount ) {
groupedData.addNotification( root.notifications[ root.notifications.length - 1 ] );
groupedData.mapGroups();
console.log(root.notifications)
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: {
if ( root.centerShown ) {
root.centerShown = false;
console.log("groups", groupedData.count);
console.log(root.notifications)
}
}
}
Rectangle {
id: backgroundRect
y: 10
x: Screen.width
implicitWidth: 400
implicitHeight: root.height - 20
color: "#801a1a1a"
radius: 8
border.color: "#555555"
border.width: 1
clip: true
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.preferredHeight: 30
Layout.fillWidth: true
Text {
text: "Notifications"
color: "white"
font.bold: true
font.pointSize: 16
Layout.fillWidth: true
}
Switch {
id: dndSwitch
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
text: "Do Not Disturb"
}
}
Rectangle {
color: "#333333"
Layout.preferredHeight: 1
Layout.fillWidth: true
}
Column {
id: notificationColumn
Layout.fillHeight: true
Layout.fillWidth: true
// width: mainLayout.width
spacing: 10
clip: true
move: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
add: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
ListView {
id: groupListView
model: groupedData
spacing: 10
width: 400
height: Math.min(contentHeight, notificationColumn.height)
// Layout.fillWidth: true
// Layout.fillHeight: true
delegate: ListView {
required property var modelData
required property int index
property bool isExpanded: false
property bool isExpandedAnim: false
id: listView
visible: true
property ListModel notificationsModel: modelData.notifications
model: notificationsModel
width: notificationColumn.width
implicitHeight: listView.isExpandedAnim ? contentHeight : 80
clip: true
spacing: 10
onIsExpandedChanged: {
if ( !isExpanded ) {
collapseAnim.start();
} else {
expandAnim.start();
}
}
NumberAnimation {
id: collapseAnim
target: listView
property: "implicitHeight"
to: 80
from: listView.contentHeight
duration: 80
easing.type: Easing.InOutQuad
onStopped: {
listView.isExpandedAnim = listView.isExpanded;
}
}
NumberAnimation {
id: expandAnim
target: listView
property: "implicitHeight"
from: 80
to: listView.contentHeight
duration: 80
easing.type: Easing.InOutQuad
onStopped: {
listView.isExpandedAnim = listView.isExpanded;
}
}
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
displaced: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
delegate: Rectangle {
id: notificationItem
required property var modelData
required property int index
width: listView.width
height: 80
color: "#801a1a1a"
border.color: "#555555"
border.width: 1
radius: 8
clip: true
visible: true
opacity: 1
MouseArea {
anchors.fill: parent
onClicked: {
listView.isExpanded ? ( notificationItem.index === 0 ? listView.isExpanded = false : null ) : listView.isExpanded = true;
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
IconImage {
source: notificationItem.modelData.image
Layout.preferredWidth: 48
Layout.preferredHeight: 48
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Text {
text: notificationItem.modelData.appName
color: "white"
font.bold: true
font.pointSize: 12
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: notificationItem.modelData.summary
color: "white"
font.pointSize: 10
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Text {
text: notificationItem.modelData.body
color: "#dddddd"
font.pointSize: 8
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
}
}
}
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
width: 18
height: 18
color: closeArea.containsMouse ? "#FF6077" : "transparent"
radius: 9
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "white" : "#888888"
font.pointSize: 12
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.notifications[modelData].dismiss();
}
}
}
}
}
}
}
}
}
}
+40
View File
@@ -0,0 +1,40 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
Scope {
id: root
property list<int> notifIds: []
NotificationServer {
id: notificationServer
imageSupported: true
actionsSupported: true
persistenceSupported: true
bodyImagesSupported: true
bodySupported: true
onNotification: {
notification.tracked = true;
notification.receivedTime = Date.now();
root.notifIds.push(notification.id);
notificationCenter.groupedData.addNotification(notification);
notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb });
}
}
Component {
id: notificationComponent
Notif {
centerX: notificationCenter.posX
notifIndex: root.notifIds
onNotifDestroy: {
root.notifIds.shift();
}
}
}
NotificationCenter {
id: notificationCenter
notifications: notificationServer.trackedNotifications.values
}
}
-16
View File
@@ -1,16 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
function getTrayIcon(id: string, icon: string): string {
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
}
return icon;
}
}
-50
View File
@@ -1,50 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
PopupWindow {
id: root
required property QsMenuHandle menu
property int height: menuOpener.children.values.length * entryHeight
property int entryHeight: 25
implicitWidth: 300
implicitHeight: height
QsMenuOpener {
id: menuOpener
menu: root.menu
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Repeater {
model: menuOpener.children
Rectangle {
id: menuItem
width: root.implicitWidth
height: modelData.isSeparator ? 5 : root.entryHeight
color: mouseArea.containsMouse ? "#22000000" : "transparent"
required property QsMenuEntry modelData
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onClicked: {
menuItem.modelData.triggered();
root.visible = false;
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 10
text: menuItem.modelData.text
}
}
}
}
}
}
-94
View File
@@ -1,94 +0,0 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Caelestia
import QtQuick
import QtQuick.Layouts
import qs.modules
Variants {
model: Quickshell.screens
Scope {
id: scope
required property ShellScreen modelData
PanelWindow {
screen: scope.modelData
color: "#99000000"
anchors {
top: true
left: true
right: true
}
implicitHeight: 34
Rectangle {
anchors.centerIn: parent
implicitHeight: parent.height
implicitWidth: rowL.implicitWidth + 10
color: "transparent"
RowLayout {
spacing: 8
id: rowL
anchors.centerIn: parent
Repeater {
model: SystemTray.items
MouseArea {
id: trayItem
required property SystemTrayItem modelData
property SystemTrayItem item: modelData
implicitWidth: 20
implicitHeight: 20
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton
IconImage {
id: icon
anchors.fill: parent
layer.enabled: true
layer.onEnabledChanged: {
if (layer.enabled && status === Image.Ready)
analyser.requestUpdate();
}
onStatusChanged: {
if (layer.enabled && status === Image.Ready)
analyser.requestUpdate();
}
source: GetIcons.getTrayIcon(trayItem.item.id, trayItem.item.icon)
mipmap: false
asynchronous: true
ImageAnalyser {
id: analyser
sourceItem: icon
}
}
Popout {
id: popout
menu: trayItem.item.menu
anchor.item: trayItem
anchor.edges: Edges.Bottom | Edges.Left
}
onClicked: {
if ( mouse.button === Qt.LeftButton ) {
trayItem.item.activate();
} else if ( mouse.button === Qt.RightButton ) {
popout.visible = !popout.visible;
}
}
}
}
}
}
}
}
}