diff --git a/AudioWidget.qml b/AudioWidget.qml index ef970bf..f110f48 100644 --- a/AudioWidget.qml +++ b/AudioWidget.qml @@ -2,9 +2,10 @@ import QtQuick import Quickshell.Io import Quickshell.Services.Pipewire import qs.Common +import qs.Common.Styled import qs.Services -Rectangle { +BackgroundRectangle { id: audioWidget property string monitor: "" @@ -15,8 +16,7 @@ Rectangle { implicitWidth: audioText.implicitWidth * 1.6 implicitHeight: Theme.heightGaps - color: Theme.backgroudColor - radius: 25 + states: [ State { name: "Mute" @@ -56,17 +56,13 @@ Rectangle { target: audioWidget.audioPipewireActive ? Pipewire.defaultAudioSink.audio : null } - Text { + StyledText { id: audioText property string audioTextText: audioWidget.audioPipewireActive ? audioWidget.icon + " " + Math.round(Pipewire.defaultAudioSink.audio.volume * 100).toString() + "%" : "" anchors.centerIn: parent text: audioTextText - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } Process { diff --git a/ClockWidget.qml b/ClockWidget.qml index b0c842e..0df7469 100644 --- a/ClockWidget.qml +++ b/ClockWidget.qml @@ -3,6 +3,7 @@ import Quickshell.Widgets import Quickshell.Io import qs.Services import qs.Common +import qs.Common.Styled Item { property var monitor: "" @@ -12,25 +13,19 @@ Item { leftMargin: Theme.gaps } - Rectangle { + BackgroundRectangle { id: clock - color: Theme.backgroudColor implicitWidth: clockText.implicitWidth * 1.6 implicitHeight: Theme.heightGaps - radius: 25 property string calendar: "" - Text { + StyledText { id: clockText anchors.centerIn: parent text: Time.time - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } Process { diff --git a/Common/Styled/BackgroundRectangle.qml b/Common/Styled/BackgroundRectangle.qml new file mode 100644 index 0000000..c3821a2 --- /dev/null +++ b/Common/Styled/BackgroundRectangle.qml @@ -0,0 +1,10 @@ +import QtQuick +import qs.Common + +Rectangle { + + color: Theme.backgroudColor + border.width: 1 + border.color: Theme.color2 + radius: 20 +} diff --git a/Common/Styled/ForegroundRectangle.qml b/Common/Styled/ForegroundRectangle.qml new file mode 100644 index 0000000..548b575 --- /dev/null +++ b/Common/Styled/ForegroundRectangle.qml @@ -0,0 +1,8 @@ +import QtQuick +import qs.Common + +Rectangle { + + color: Theme.backgroudColorBright + radius: 20 +} diff --git a/Common/Styled/StyledText.qml b/Common/Styled/StyledText.qml new file mode 100644 index 0000000..913998d --- /dev/null +++ b/Common/Styled/StyledText.qml @@ -0,0 +1,19 @@ +import QtQuick +import qs.Common + +Text { + id: clockText + + anchors.margins: 5 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + textFormat: Text.MarkdownText + font.bold: true + font.pixelSize: Theme.pixelSize + font.family: Theme.fontFamily + color: Theme.textColor + onLinkActivated: link => Qt.openUrlExternally(link) + + text: "" +} diff --git a/NotificationsWidget.qml b/NotificationsWidget.qml index b8e62a0..5ada60d 100644 --- a/NotificationsWidget.qml +++ b/NotificationsWidget.qml @@ -3,57 +3,96 @@ import Quickshell import Quickshell.Widgets import qs.Services import qs.Common +import qs.Common.Styled import qs.Widgets -import Quickshell.Services.Notifications Item { id: root property var monitor: "" property bool createWindow: false + property string notificationIcon: "" MarginWrapperManager { rightMargin: Theme.gaps leftMargin: Theme.gaps } + states: [ + State { + name: "MuteActive" + when: NotificationService.notificationsMuted && NotificationService.notificationsNumber + PropertyChanges { + root.notificationIcon : "\udb80\udc9b " + NotificationService.notificationsNumber + } + }, + State { + name: "Active" + when: !NotificationService.notificationsMuted && NotificationService.notificationsNumber + PropertyChanges { + root.notificationIcon : "\udb80\udc9a " + NotificationService.notificationsNumber + } + }, + State { + name: "MuteEmpty" + when: NotificationService.notificationsMuted + PropertyChanges { + root.notificationIcon : "\uec08" + } + }, + State { + name: "Empty" + when: !NotificationService.notificationsMuted && !NotificationService.notificationsNumber + PropertyChanges { + root.notificationIcon : "\ueaa2" + } + } + ] + Binding { target: root property: "createWindow" value: NotificationService.notificationsNumber > 0 && root.createWindow } - Rectangle { + BackgroundRectangle { color: Theme.backgroudColor - implicitWidth: clockText.implicitWidth * 1.6 + implicitWidth: 60 implicitHeight: Theme.heightGaps radius: 25 - Text { - id: clockText + StyledText { + id: notifText - anchors.centerIn: parent - text: { - NotificationService.notificationsNumber > 0 ? "\udb80\udc9a " + NotificationService.notificationsNumber : "\ueaa2"; - } - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor + anchors.fill: parent + text: root.notificationIcon + " " + // text: { + // NotificationService.notificationsNumber > 0 ? "\udb80\udc9a " + NotificationService.notificationsNumber : "\ueaa2"; + // } } MouseArea { anchors.fill: parent - onClicked: { + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + NotificationService.notificationsMuted = !NotificationService.notificationsMuted + return; + } root.createWindow = !root.createWindow; } } } + LazyLoader { id: windowLoader - active: NotificationService.notificationsNumber ? createWindow : false + activeAsync: NotificationService.notificationsNumber ? createWindow : false - component: NotificationWindow {} + component: NotificationWindow { + onClear: root.createWindow = false + } + } } diff --git a/Services/HyprlandService.qml b/Services/HyprlandService.qml index 3351456..0754e5f 100644 --- a/Services/HyprlandService.qml +++ b/Services/HyprlandService.qml @@ -8,6 +8,8 @@ Singleton { readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") + readonly property var focusedMon: Hyprland.focusedMonitor + property var sortedTopLevels: { if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { return []; diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index ce61d05..9cf4cc3 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Services.Notifications import qs.Widgets +import qs.Services Singleton { id: notificationRoot @@ -12,8 +13,9 @@ Singleton { readonly property var notificationServer: notificationServer readonly property var trackedNotifications: notificationServer.trackedNotifications readonly property var notificationsNumber: notificationServer.trackedNotifications.values.length - property bool shouldShowOsd: false - property var currentNotification: null + + property bool notificationsMuted: false + property ListModel globalList: ListModel {} NotificationServer { id: notificationServer @@ -21,54 +23,134 @@ Singleton { } Connections { - function onNotification(notif) { - if (notif.body == "MediaOngoingActivity") { - return; - } - if (notif.tracked) { - return; - } - notif.tracked = true; - notificationRoot.currentNotification = notif; - notificationRoot.shouldShowOsd = true; - notificationTimer.start(); - } - target: notificationServer + function onNotification(notif) { + if(notif.transient) return; + if (notif.body === "MediaOngoingActivity") + return; + notif.tracked = true; + notificationRoot.addNotification(globalList, notif); + + if (notif.lastGeneration) + return; + + // Use the refactored helper + notificationRoot.addNotification(notificationList, notif); + } } - Timer { - id: notificationTimer + ListModel { + id: notificationList + } + + /** + * @param {ListModel} model + * @param {var} targetNotif + */ + function removeNotification(model, targetNotif) { + + if (!model || typeof model.remove !== "function") { + console.warn("removeNotification(): invalid model"); + return; + } + + for (let i = 0; i < model.count; i++) { + if (model.get(i).notif === targetNotif) { + model.remove(i); + break; + } + } + } + + /** + * @param {ListModel} model + * @param {var} notif + */ + function addNotification(model, notif) { + if (!model || typeof model.append !== "function") { + console.warn("addNotification(): invalid model"); + return; + } + + // Avoid duplicates + for (let i = 0; i < model.count; i++) { + if (model.get(i).notif === notif) + return; + } + + model.append({ + notif + }); + + notif.closed.connect(function (reason) { + removeNotification(model, notif); + }); + } + + // function notificationDismiss(notif) { + // removeNotification(notificationList, notif); + // removeNotification(globalList, notif); + // notif.dismiss(); + // } + + function notificationDismiss(notif) { + removeNotification(notificationList, notif); + removeNotification(globalList, notif); + + if (notif && typeof notif.dismiss === "function") + notif.dismiss(); // dismiss first - interval: 5000 - onTriggered: parent.shouldShowOsd = false } LazyLoader { id: popupLoader - active: notificationRoot.currentNotification && notificationRoot.shouldShowOsd + active: notificationList.count && !notificationRoot.notificationsMuted component: PanelWindow { id: notificationWindow - + screen: { + Quickshell.screens.filter(screen => screen.name == HyprlandService.focusedMon.name)[0]; + } anchors.top: true margins.top: screen.height / 100 exclusiveZone: 0 - implicitWidth: 400 - implicitHeight: 905 + implicitHeight: screen.height color: "transparent" + mask: Region { item: listView } - NotificationWrapper { - id: notificationWrapper + ListView { + id: listView + model: notificationList + implicitWidth: parent.width + implicitHeight: contentHeight + orientation: ListView.Vertical + clip: true + spacing: 5 + interactive: false - notification: notificationRoot.currentNotification - implicitWidth: notificationWindow.implicitWidth + delegate: NotificationWrapper { + id: notifWrapper - onDismissed: { - notificationRoot.shouldShowOsd = false; - notificationRoot.currentNotification.dismiss(); + required property var modelData + notification: modelData + implicitWidth: listView.width + + + startTimer: NotificationUrgency.toString(notification?.urgency) === "Critical"? false: true + timerDuration: 5000 + + onDismissed: { + if (notification && typeof notification.dismiss === "function") + notificationRoot.notificationDismiss(notification); + } + + onTimedout: { + notificationRoot.removeNotification(notificationList, notification) + } } + + Component.onCompleted: positionViewAtEnd() } } } diff --git a/Services/PopUpHover.qml b/Services/PopUpHover.qml index de8a30d..1af8b85 100644 --- a/Services/PopUpHover.qml +++ b/Services/PopUpHover.qml @@ -6,6 +6,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import qs.Common +import qs.Common.Styled import qs.Services Singleton { @@ -35,18 +36,14 @@ Singleton { Component { id: stub - Text { + StyledText { text: "stub" - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } } Component { id: systray - Text { + StyledText { text: { if (!HoverMediator.component?.model) return ""; @@ -57,35 +54,24 @@ Singleton { else return ""; } - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } } Component { id: time - Text { + StyledText { property string calendar: (HoverMediator.component.calendar) ? HoverMediator.component.calendar : "" text: calendar - font.bold: true - font.pixelSize: Theme.pixelSize font.family: Theme.fontFamilyMono - color: Theme.textColor } } Component { id: audio - Text { + StyledText { property string sinkDescription: (HoverMediator.component.sink) ? HoverMediator.component.sink.description : "" text: sinkDescription - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } } @@ -114,13 +100,15 @@ Singleton { } } - WrapperRectangle { + BackgroundRectangle { id: wsPopUp - leftMargin: (Theme.gaps * 2) - rightMargin: (Theme.gaps * 2) - topMargin: Theme.gaps - bottomMargin: Theme.gaps + MarginWrapperManager { + leftMargin: (Theme.gaps * 2) + rightMargin: (Theme.gaps * 2) + topMargin: Theme.gaps + bottomMargin: Theme.gaps + } color: Theme.backgroudColor radius: 25 opacity: 1 diff --git a/SysTrayWidget.qml b/SysTrayWidget.qml index 42a3c10..97a8737 100644 --- a/SysTrayWidget.qml +++ b/SysTrayWidget.qml @@ -6,16 +6,18 @@ import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray import qs.Common +import qs.Common.Styled import qs.Services -WrapperRectangle { +BackgroundRectangle { id: systrayRoot - property string monitor: "" - rightMargin: Theme.gaps - leftMargin: Theme.gaps - radius: 25 - color: Theme.backgroudColor + MarginWrapperManager { + rightMargin: Theme.gaps + leftMargin: Theme.gaps + } + + property string monitor: "" // color: Theme.backgroudColor RowLayout { diff --git a/Widgets/NotificationWindow.qml b/Widgets/NotificationWindow.qml index 860dba4..ab9f684 100644 --- a/Widgets/NotificationWindow.qml +++ b/Widgets/NotificationWindow.qml @@ -1,41 +1,91 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Layouts import Quickshell +import Quickshell.Widgets import qs.Services import qs.Widgets +import qs.Common.Styled +import qs.Common import QtQuick.Window PopupWindow { id: notificationRoot anchor.item: root - anchor.rect.y: parentWindow?.height - implicitWidth: 400 - implicitHeight: Math.min(listView.contentHeight, 900) - color: "white" + implicitWidth: screen.width + // implicitWidth: 400 + implicitHeight: screen.height + color: "transparent" visible: true + signal clear - ListView { - id: listView + mask: Region { item: listView } + + MouseArea { anchors.fill: parent - model: NotificationService.trackedNotifications.values - orientation: ListView.Vertical - verticalLayoutDirection: ListView.BottomToTop - clip: true - spacing: 5 + onClicked: notificationRoot.clear() + } - delegate: NotificationWrapper { - required property var modelData - notification: modelData - width: ListView.width - onDismissed: { - if (notification && typeof notification.dismiss === "function") { - notification.dismiss(); + Rectangle { + id: notifWindow + + anchors{ + top: parent.top + left: parent.left + topMargin: 40 + } + border.width: 1 + color: Theme.color2 + implicitWidth: 400 + implicitHeight: Math.min(listView.contentHeight, 1090) + 50 + + ColumnLayout { + id: windowLayout + + anchors.fill: parent + spacing: 0 + + BackgroundRectangle { + implicitWidth: parent.width + implicitHeight: 25 + radius: 0 + border.width: 0 + + MouseArea { + anchors.fill: parent + onClicked: { + while (NotificationService.notificationsNumber != 0) { + NotificationService.trackedNotifications.values[0].dismiss(); + } + } + } + StyledText { + anchors.centerIn: parent + text: "NOTIFICAÇÕES" + } + } + + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.globalList + clip: true + spacing: 5 + leftMargin: 10 + rightMargin: 10 + + delegate: NotificationWrapper { + required property var modelData + notification: modelData + implicitWidth: listView.width - 20 + onDismissed: { + NotificationService.notificationDismiss(notification); + } } } } - Component.onCompleted: positionViewAtEnd() - onModelChanged: positionViewAtEnd() } } diff --git a/Widgets/NotificationWrapper.qml b/Widgets/NotificationWrapper.qml index 10f1d03..31868d8 100644 --- a/Widgets/NotificationWrapper.qml +++ b/Widgets/NotificationWrapper.qml @@ -2,134 +2,137 @@ import Quickshell import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import qs.Common.Styled -Rectangle { +BackgroundRectangle { id: notificationWrapper signal dismissed + signal timedout property bool hasImage: notification?.image ? true : false + property bool clicked: false property var notification: null + property bool startTimer: false + property int timerDuration: 2000 + property bool firstTime: true property string image: { if (hasImage) { - return notification.image; + return notification?.image; } if (notification?.appIcon === "") { return ""; } return Quickshell.iconPath(notification?.appIcon); } + property real targetHeight: notifLayout.implicitHeight + 20 implicitWidth: 400 - implicitHeight: notifLayout.implicitHeight - topLeftRadius: image ? 100 : 20 - bottomLeftRadius: image ? 100 : 20 - topRightRadius: 20 - bottomRightRadius: 20 - color: "#80000000" + implicitHeight: 0 + // implicitHeight: Math.max(notifLayout.implicitHeight, 100) + + Component.onCompleted: { + notificationWrapper.implicitHeight = targetHeight + } + clip: true + + Behavior on implicitHeight { + SequentialAnimation { + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + ScriptAction { + script: { + if(clicked) notificationWrapper.dismissed() + if(notificationWrapper.implicitHeight === 0) notificationWrapper.timedout() + } + } + } + } MouseArea { anchors.fill: parent onClicked: { - notificationWrapper.dismissed(); + notificationWrapper.clicked = true + notificationWrapper.implicitHeight = 0 + } + } + + Timer { + id: notifTimer + + interval: timerDuration + running: notificationWrapper.startTimer + + onTriggered: { + notificationWrapper.implicitHeight = 0; } } RowLayout { id: notifLayout + implicitHeight: Math.max(iconImage.implicitHeight, gridRoot.implicitHeight) + anchors { fill: parent } - ClippingRectangle { + IconImage { + id: iconImage - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - - implicitWidth: 100 - implicitHeight: 100 - visible: notificationWrapper.image ? true : false - - color: "grey" - - radius: 100 - - IconImage { - anchors.centerIn: parent - implicitSize: 100 - source: notificationWrapper.image - } + Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter + Layout.leftMargin: 10 + implicitSize: 80 + visible: notificationWrapper.image? true: false + source: notificationWrapper.image } ColumnLayout { + id: gridRoot Layout.leftMargin: 10 Layout.rightMargin: 10 + implicitHeight: summaryRectangle.implicitHeight + bodyRectangle.implicitHeight + + spacing: 5 + + ForegroundRectangle { + id: summaryRectangle - Rectangle { Layout.fillWidth: true implicitHeight: summaryText.implicitHeight + 10 - radius: 20 - color: "#50ffffff" - Rectangle { - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } + visible: summaryText.text ? true : false - implicitWidth: parent.width - radius: parent.radius - color: "white" + StyledText { + id: summaryText - Text { - id: summaryText + anchors.fill: parent - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 - wrapMode: Text.WordWrap - textFormat: Text.MarkdownText - - text: notificationWrapper.notification.summary - } + text: notificationWrapper.notification? notificationWrapper.notification.summary : "" } } - Rectangle { + ForegroundRectangle { + id: bodyRectangle + Layout.fillWidth: true + Layout.columnSpan: 5 implicitHeight: bodyText.implicitHeight + 10 - radius: 20 - Rectangle { - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } + visible: bodyText.text ? true : false - implicitWidth: parent.width - radius: parent.radius - color: "white" + StyledText { + id: bodyText - Text { - id: bodyText + anchors.fill: parent - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: 14 - wrapMode: Text.WordWrap - textFormat: Text.MarkdownText - - text: notificationWrapper.notification.body - onLinkActivated: link => Qt.openUrlExternally(link) - } + text: notificationWrapper.notification? notificationWrapper.notification.body : "" } } } diff --git a/Workspaces.qml b/Workspaces.qml index 7edf11b..5d96e82 100644 --- a/Workspaces.qml +++ b/Workspaces.qml @@ -7,6 +7,7 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Widgets import qs.Common +import qs.Common.Styled import qs.Services WrapperMouseArea { @@ -41,7 +42,7 @@ WrapperMouseArea { model: 5 - delegate: Rectangle { + delegate: BackgroundRectangle { id: workspacesRectangle property string type: "workspace" @@ -57,7 +58,6 @@ WrapperMouseArea { Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter implicitHeight: Theme.heightGaps - radius: 25 states: [ State { @@ -104,16 +104,12 @@ WrapperMouseArea { } } - Text { + StyledText { property int workspaceName: workspacesRectangle.workspaceIndexAlign > 5 ? workspacesRectangle.workspaceIndexAlign - 5 : workspacesRectangle.workspaceIndexAlign anchors.centerIn: parent text: workspaceName - font.bold: true - font.pixelSize: Theme.pixelSize - font.family: Theme.fontFamily - color: Theme.textColor } MouseArea {