Compare commits

...
Author SHA1 Message Date
Hannes 510cbb0cec feat: refactor NotificationHistory.qml to improve hover tracking and auto-closing behavior 2026-09-02 00:34:14 +02:00
Hannes aea40a3e42 feat: removed wrong new line 2026-09-02 00:30:40 +02:00
Hannes 8eb08631f7 fix: autoclose only when not hovered 2026-09-02 00:27:32 +02:00
Hannes a827d1e92c fix: removed notes 2026-09-02 00:16:57 +02:00
Hannes 27401c25a0 feat: refactor notification system to manage hover states centrally and auto close 2026-09-02 00:16:15 +02:00
Hannes b59eb012ba feat: refactor: add helpers for summary/body placement in NotificationBody 2026-09-01 23:59:36 +02:00
Hannes 2df40a2a76 fix: Update NotificationBody.qml for expanded groups and clear-group functionality 2026-09-01 22:59:55 +02:00
Hannes 9788529f55 feat: Introduce NotificationHistoryLoader to manage opening and closing the NotificationHistory panel. 2026-09-01 22:41:34 +02:00
Hannes 94d9dd6fac feat: update bar height configuration and bar visibilty on fullscreen 2026-09-01 14:37:46 +02:00
Hannes 764f8197fb feat: refactor bar spacing functions to use dedicated functions 2026-09-01 13:52:03 +02:00
Hannes 36486179d5 fix: Update notification sound play logic 2026-08-31 21:42:38 +02:00
Hannes f2b69382c7 refactor: Introduce _hoveredActionCount for notification action hover tracking, avoiding direct 'root' lookup and improving code readability. 2026-08-31 17:07:43 +02:00
Hannes c7538750e3 feat: update notification layout and add avatar load functionality 2026-08-31 16:54:54 +02:00
Hannes 2c38de7c60 feat: refactor notification body to use a more concise conditional expression for handling app icons, images, and desktop entry icons 2026-08-31 16:34:18 +02:00
20 changed files with 781 additions and 97 deletions
+10 -4
View File
@@ -12,9 +12,13 @@ QtObject {
readonly property int small_spacing: 4
readonly property int spacing: 8
function small_spacing_fun(screenHeight) { return Math.floor(barHeight(screenHeight) / 16) }
function spacing_fun(barH) { return Math.floor(barH / 8) }
function big_spacing_fun(barH) { return Math.floor(barH / 6) }
function bar_small_bar_spacing_fun(barH) { return Math.floor(barHeight(barH) / 16) }
function bar_spacing_fun(barH) { return Math.floor(barH / 8) }
function bar_big_spacing_fun(barH) { return Math.floor(barH / 6) }
function screen_small_bar_spacing_fun(screenHeight) { return Math.floor((screenHeight / 1080)*2) }
function screen_spacing_fun(screenHeight) { return Math.floor((screenHeight / 1080)*4) }
function screen_big_spacing_fun(screenHeight) { return Math.floor((screenHeight / 1080)*8) }
function font_size(spaceHeight) { return Math.floor(spaceHeight * 0.75) }
function font_size_small(spaceHeight) { return Math.floor(spaceHeight * 0.625) }
@@ -29,6 +33,8 @@ QtObject {
readonly property int radius: 16
function small_radius_fun(barHeight) { return Math.floor(barHeight / 8) }
function radius_fun(barHeight) { return Math.floor(barHeight / 4) }
function screen_radius_fun(screenHeight) { return Math.floor((screenHeight / 1080)*4) }
function screen_big_radius_fun(screenHeight) { return Math.floor((screenHeight / 1080)*6) }
readonly property int big_radius: 10
readonly property int border_width: 2
@@ -75,5 +81,5 @@ QtObject {
return maxWidth;
}
property string debug: "Notification"
property string debug: ""
}
+6 -6
View File
@@ -14,7 +14,7 @@ PanelWindow {
anchors.left: true
anchors.right: true
implicitHeight: Math.floor(screen.height * Config.barHeightRatio(screen.height))
implicitHeight: Config.barHeight(screen.height)
exclusiveZone: height // this is so context menus don't clip into the bar
@@ -40,8 +40,8 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Config.big_spacing_fun(height)
spacing: Config.big_spacing_fun(height)
anchors.leftMargin: Config.bar_big_spacing_fun(height)
spacing: Config.bar_big_spacing_fun(height)
MenuButton {}
@@ -57,7 +57,7 @@ PanelWindow {
Row { // bar widgets center
id: widgetsCenter
anchors.centerIn: parent
spacing: Config.big_spacing_fun(height)
spacing: Config.bar_big_spacing_fun(height)
Clock {}
@@ -69,7 +69,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 0
spacing: Config.big_spacing_fun(height)
spacing: Config.bar_big_spacing_fun(height)
// TODO: Add bluetooth, network, audio, power, maybe HW monitor
Tray{}
@@ -83,4 +83,4 @@ PanelWindow {
}
}
}
}
+5 -1
View File
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
Item {
@@ -8,7 +9,10 @@ Item {
model: Quickshell.screens
delegate: Loader {
active: true // loads object
sourceComponent: Bar {screen: modelData} // creates instance of bar component
sourceComponent: Bar {
screen: modelData
visible: !Hyprland.monitorFor(modelData).activeWorkspace.hasFullscreen
} // creates instance of bar component
}
}
}
+4 -4
View File
@@ -6,7 +6,7 @@ import Quickshell.Hyprland
MouseArea {
id: audioButton
width: Math.max(audioRow.width + Config.spacing_fun(height)*3.5, height)
width: Math.max(audioRow.width + Config.bar_spacing_fun(height)*3.5, height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -22,9 +22,9 @@ MouseArea {
Row {
id: audioRow
width: audioSourceText.width + audioSinkText.width + audioPercentageText.width + Config.spacing_fun(height)*2
width: audioSourceText.width + audioSinkText.width + audioPercentageText.width + Config.bar_spacing_fun(height)*2
height: parent.height
spacing: Config.spacing_fun(height)/2
spacing: Config.bar_spacing_fun(height)/2
anchors.centerIn: parent
Text {
@@ -35,7 +35,7 @@ MouseArea {
font.pixelSize: Config.iconSize(bar.height)
font.family: Config.font
color: audioButton.containsMouse ? Colors.tertiary_fg : Colors.primary
rightPadding: Config.spacing_fun(height)*1.5
rightPadding: Config.bar_spacing_fun(height)*1.5
}
Text {
+1 -1
View File
@@ -5,7 +5,7 @@ import "../states/Audio"
MouseArea {
id: audioButton
width: Math.max(audioText.width + Config.spacing_fun(height)*2, height)
width: Math.max(audioText.width + Config.bar_spacing_fun(height)*2, height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
+3 -3
View File
@@ -6,7 +6,7 @@ import "../states/Battery"
MouseArea {
id: batteryButton
width: expanded ? Math.max(expandedRow.width + Config.spacing_fun(height)*3, bar.height) : Math.max(batteryText.width + Config.spacing_fun(height)*2, bar.height)
width: expanded ? Math.max(expandedRow.width + Config.bar_spacing_fun(height)*3, bar.height) : Math.max(batteryText.width + Config.bar_spacing_fun(height)*2, bar.height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -63,7 +63,7 @@ MouseArea {
id: expandedRow
visible: expanded
anchors.centerIn: parent
spacing: Config.spacing_fun(batteryButton.height)
spacing: Config.bar_spacing_fun(batteryButton.height)
Text {
id: batteryPercentageSymbol
@@ -121,7 +121,7 @@ MouseArea {
id: batteryPopup
anchorItem: batteryButton
anchorHovered: batteryButton.containsMouse
spacingValue: Config.spacing_fun(batteryButton.height)
spacingValue: Config.bar_spacing_fun(batteryButton.height)
radiusValue: Config.radius_fun(batteryButton.height)
Column {
+2 -2
View File
@@ -6,7 +6,7 @@ import "../states/Clock"
Rectangle { //workspace
id: clockRect
width: ClockState.expanded ? Math.max(dateText.width + timeText.width + Config.spacing_fun(height)*3, bar.height) : Math.max(timeText.width + Config.spacing_fun(height)*2, bar.height)
width: ClockState.expanded ? Math.max(dateText.width + timeText.width + Config.bar_spacing_fun(height)*3, bar.height) : Math.max(timeText.width + Config.bar_spacing_fun(height)*2, bar.height)
height: bar.height
radius: Config.radius_fun(height)
color: clockArea.containsMouse ? Colors.tertiaryContainer : Colors.primaryContainer
@@ -61,7 +61,7 @@ Rectangle { //workspace
id: clockPopup
anchorItem: clockRect
anchorHovered: clockArea.containsMouse
spacingValue: Config.spacing_fun(clockRect.height)
spacingValue: Config.bar_spacing_fun(clockRect.height)
radiusValue: Config.radius_fun(clockRect.height)
Column {
+1 -1
View File
@@ -7,7 +7,7 @@ import "../states/Idle"
MouseArea {
id: idleButton
width: Math.max(idleText.width + Config.spacing_fun(height)*2, height)
width: Math.max(idleText.width + Config.bar_spacing_fun(height)*2, height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
+2 -2
View File
@@ -5,7 +5,7 @@ import "../states/Network"
MouseArea {
id: networkButton
width: Math.max(networkText.width + Config.spacing_fun(height)*2, height)
width: Math.max(networkText.width + Config.bar_spacing_fun(height)*2, height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -37,7 +37,7 @@ MouseArea {
id: clockPopup
anchorItem: networkButton
anchorHovered: networkButton.containsMouse
spacingValue: Config.spacing_fun(networkButton.height)
spacingValue: Config.bar_spacing_fun(networkButton.height)
radiusValue: Config.radius_fun(networkButton.height)
Column {
+35 -11
View File
@@ -13,9 +13,40 @@ MouseArea {
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationS.soundEnabled = !NotificationS.soundEnabled
// NotificationS.notificationPopupEnabled = !NotificationS.notificationPopupEnabled
console.log("[NotificationButton] Opening NotificationHistory")
root.toggleNotificationHistory()
}
// ── Report hover to NotificationServer for auto-close logic ──
onContainsMouseChanged: NotificationS.buttonHovered = containsMouse
Component.onDestruction: {
if (containsMouse) NotificationS.buttonHovered = false
}
onVisibleChanged: {
if (!visible && NotificationS.buttonHovered) NotificationS.buttonHovered = false
}
Item {
anchors.top: parent.topBar
anchors.right: parent.right
anchors.topMargin: Config.spacing
anchors.rightMargin: Config.spacing
width: Config.screen_height_to_font_tiny(screen.height)
height: Config.screen_height_to_font_tiny(screen.height)
visible: NotificationS.notificationNum > 0
Text{
anchors.centerIn: parent
text: NotificationS.notificationNum
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_tiny(screen.height)
color: Colors.secondaryContainer_fg
}
}
Rectangle {
anchors.fill: parent
color: "transparent"
@@ -24,18 +55,11 @@ MouseArea {
Text {
id: notificationText
anchors.centerIn: parent
// text: Quickshell.iconPath("discord")
text: NotificationS.soundEnabled ? "󰂚" : "󰂛"
// text: Quickshell.iconPath("discord")
text: NotificationS.notificationPopupEnabled ? "󰂚" : "󰂛"
font.pixelSize: Config.bigIconSize(bar.height)
font.family: Config.font
color: Colors.secondaryContainer_fg
}
// Image{
// id: iconImage
// source: Quickshell.iconPath("discord")
// width: Config.bigIconSize(notificationButton.height)
// height: Config.bigIconSize(notificationButton.height)
// }
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import "../../../config"
MouseArea {
id: powerButton
width: Math.max(powerText.width + Config.spacing_fun(height), height)
width: Math.max(powerText.width + Config.bar_spacing_fun(height), height)
height: bar.height
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
+3 -3
View File
@@ -7,7 +7,7 @@ import "../../../config"
Row { // Workspace Row
id: trayRow
spacing: Config.small_spacing_fun(height)
spacing: Config.bar_small_bar_spacing_fun(height)
Repeater {
model: SystemTray.items
@@ -19,8 +19,8 @@ Row { // Workspace Row
Rectangle { //trayItem
id: trayItem
width: iconImage.width + Config.spacing_fun(bar.height)
height: iconImage.height + Config.spacing_fun(bar.height)
width: iconImage.width + Config.bar_spacing_fun(bar.height)
height: iconImage.height + Config.bar_spacing_fun(bar.height)
radius: Config.radius_fun(trayRow.height)
color: trayArea.containsMouse ? Colors.tertiaryContainer
: modelData.active ? Colors.primaryContainer
+2 -2
View File
@@ -7,7 +7,7 @@ import "../states/Apps"
Item {
id: root
property real pad: Config.spacing_fun(bar.height) * 1.5
property real pad: Config.bar_spacing_fun(bar.height) * 1.5
anchors.verticalCenter: parent.verticalCenter
@@ -39,7 +39,7 @@ Item {
Row {
id: contentRow
spacing: Config.spacing_fun(bar.height) * 1.25
spacing: Config.bar_spacing_fun(bar.height) * 1.25
anchors.centerIn: parent
Text {
+9 -9
View File
@@ -8,8 +8,8 @@ Rectangle {
id: workspacesContainer
required property HyprlandMonitor thisMonitor
height: bar.height - (Config.spacing_fun(bar.height) * 1.5)
width: workspacesRow.width + (Config.spacing_fun(bar.height) * 2)
height: bar.height - (Config.bar_spacing_fun(bar.height) * 1.5)
width: workspacesRow.width + (Config.bar_spacing_fun(bar.height) * 2)
radius: height / 2
@@ -20,7 +20,7 @@ Rectangle {
Row {
id: workspacesRow
anchors.centerIn: parent
spacing: Config.spacing_fun(bar.height) / 2
spacing: Config.bar_spacing_fun(bar.height) / 2
Repeater {
model: Hyprland.workspaces
@@ -30,9 +30,9 @@ Rectangle {
id: wsItem
property var workspaceData: modelData
width: Math.max(40, wsIconsRow.width==0 ? wsNum.width + 2*Config.spacing_fun(bar.height) : wsNum.width + wsIconsRow.width + 3*Config.spacing_fun(bar.height)+Config.spacing_fun(bar.height))
width: Math.max(40, wsIconsRow.width==0 ? wsNum.width + 2*Config.bar_spacing_fun(bar.height) : wsNum.width + wsIconsRow.width + 3*Config.bar_spacing_fun(bar.height)+Config.bar_spacing_fun(bar.height))
// width: {
// var spacing = Config.spacing_fun(bar.height)
// var spacing = Config.bar_spacing_fun(bar.height)
// var cw = wsNum.width
// if (wsIconsRow.children.length > 0) {
// cw += spacing + wsIconsRow.width + spacing
@@ -47,7 +47,7 @@ Rectangle {
Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height - (Config.spacing_fun(bar.height) * 1.5)
height: parent.height - (Config.bar_spacing_fun(bar.height) * 1.5)
radius: height / 2
@@ -79,9 +79,9 @@ Rectangle {
Row {
anchors.left: parent.left
anchors.leftMargin: wsIconsRow.width==0 ? (wsItem.width-wsNum.width)/2 : (wsItem.width-wsNum.width-wsIconsRow.width-Config.spacing_fun(bar.height))/2
anchors.leftMargin: wsIconsRow.width==0 ? (wsItem.width-wsNum.width)/2 : (wsItem.width-wsNum.width-wsIconsRow.width-Config.bar_spacing_fun(bar.height))/2
anchors.verticalCenter: parent.verticalCenter
spacing: Config.spacing_fun(bar.height)
spacing: Config.bar_spacing_fun(bar.height)
Text {
id: wsNum
@@ -97,7 +97,7 @@ Rectangle {
Row {
id: wsIconsRow
spacing: Config.spacing_fun(workspacesContainer.height)
spacing: Config.bar_spacing_fun(workspacesContainer.height)
anchors.verticalCenter: parent.verticalCenter
Repeater {
+1
View File
@@ -58,6 +58,7 @@ PanelWindow {
delegate: NotificationBody {
required property int index
required property var notificationData
implicitWidth: screen.width/7
screen: notificationWindow.screen
notification: notificationData
+120 -42
View File
@@ -1,21 +1,31 @@
import QtQuick
import Quickshell
import "../../config"
import "./notificationServer"
import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: root
//TODO: urgency, expireTimeoutText, ...
required property var notification
required property var screen
property var urgency: (notification.urgency ?? notification.hints["urgency"]) != null ? (notification.urgency ?? notification.hints["urgency"]) + 1 : 0
property bool closing: false
property int _hoveredActionCount: 0
// When used in history overview we don't want auto-timeout or click-to-close
property bool isHistory: false
property real duration: (
// Helpers to handle summary/body placement
// - "Message:" title is always static (fixed bug where appName replaced it when image existed)
// - If notification only has summary and no body, show summary on the right side
property bool hasBody: notification.body !== undefined && notification.body !== null && String(notification.body).trim() !== ""
property bool hasSummary: notification.summary !== undefined && notification.summary !== null && String(notification.summary).trim() !== ""
property bool showSummaryOnLeft: hasSummary && hasBody
property bool showSummaryOnRight: hasSummary && !hasBody
property real duration: isHistory ? 0 : (
notification.expireTimeout > 0 ? notification.expireTimeout*1000 :
urgency ? urgency * 2000 :
5000
@@ -30,9 +40,23 @@ Rectangle {
color: textBGColor
border.width: Config.border_width
border.color: textColor
radius: isHistory ? Config.small_radius : 0
clip: isHistory
property string textColor: urgency != 3 ? Colors.primary : Colors.tertiary
property string textBGColor: urgency != 3 ? Qt.alpha(Colors.primaryContainer, 0.75) : Qt.alpha(Colors.tertiaryContainer, 0.75)
property color textColor: urgency != 3 ? Colors.primary : Colors.tertiary
property color textBGColor: urgency != 3 ? Qt.alpha(Colors.primaryContainer, 0.75) : Qt.alpha(Colors.tertiaryContainer, 0.75)
// Expose hover state for history auto-close (reuse paused logic)
readonly property bool isHovered: mouseArea.containsMouse || closingMouseArea.containsMouse || inlineReplyButtonMouseArea.containsMouse || _hoveredActionCount > 0 || inlineReplyTextField.hovered || inlineReplyTextField.activeFocus
HoverHandler {
enabled: root.isHistory
blocking: false
grabPermissions: PointerHandler.CanTakeOverFromAnything
onHoveredChanged: if (hovered) NotificationS.historyHovered = true
}
// When in history, also keep historyHovered true while any inner element is hovered
onIsHoveredChanged: if (isHistory && isHovered) NotificationS.historyHovered = true
function closeNotification(dismiss){
if (closing) {
@@ -54,6 +78,8 @@ Rectangle {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: !root.isHistory
visible: !root.isHistory
onClicked: {
root.closeNotification(false)
}
@@ -62,7 +88,7 @@ Rectangle {
id: contentColumn
anchors.fill: parent
spacing: 0
// spacing: Config.spacing
Rectangle{
id: contentRowRectangle
height: Math.max(iconRectangle.height, contentRectangle.height)+contentRow.anchors.margins*2
@@ -77,26 +103,26 @@ Rectangle {
anchors.right: parent.right
anchors.margins: Config.spacing
width: closeIconText.implicitHeight + Config.spacing
height: closeIconText.implicitHeight + Config.spacing
width: closeIconText.implicitHeight
height: closeIconText.implicitHeight
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Rectangle {
anchors.fill: parent
radius: Config.radius
color: parent.containsMouse ? Colors.errorContainer : "transparent"
color: parent.containsMouse ? "red" : "transparent"
border.width: Config.border_width
border.color: parent.containsMouse ? Colors.error : textColor
border.color: parent.containsMouse ? "white" : textColor
Text {
id: closeIconText
anchors.centerIn: parent
text: ""; // or "✕"
text: ""; // or "✕"
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_small(screen.height)
color: parent.parent.containsMouse ? Colors.error_fg : textColor
font.pixelSize: Config.screen_height_to_font_medium(screen.height)
color: parent.parent.containsMouse ? "white" : textColor
}
}
onClicked: function(mouse){
@@ -108,8 +134,8 @@ Rectangle {
Row {
id: contentRow
anchors.fill: parent
anchors.margins: 10
spacing: Config.spacing*4
anchors.margins: Config.spacing
spacing: Config.spacing
width: parent.width
// Left side: icon + icon information
@@ -131,12 +157,16 @@ Rectangle {
height: iconImage.width
anchors.horizontalCenter: parent.horizontalCenter
source: (
notification.appIcon ? Quickshell.iconPath(notification.appIcon) :
notification.image ? notification.image :
Quickshell.iconPath(DesktopEntries.byId(notification.desktopEntry).icon) ? Quickshell.iconPath(DesktopEntries.byId(notification.desktopEntry).icon) :
Quickshell.iconPath("notifications")
)
source: {
if (notification.image) return notification.image
if (notification.appIcon) return Quickshell.iconPath(notification.appIcon)
const entry = notification.desktopEntry ? DesktopEntries.byId(notification.desktopEntry) : null
if (entry && entry.icon) {
const p = Quickshell.iconPath(entry.icon)
if (p && p !== "") return p
}
return Quickshell.iconPath("notifications")
}
fillMode: Image.PreserveAspectFit
asynchronous: true
@@ -144,10 +174,10 @@ Rectangle {
Text {
id: summaryText
// height: implicitHeight + font.pixelSize + Config.spacing
text: root.notification.summary
visible: root.notification.summary ? true : false
// Only show summary under icon when both summary and body exist.
// If only summary exists (no body), it will be shown on the right instead.
visible: root.showSummaryOnLeft
width: iconColumn.width
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_small(screen.height)
@@ -162,25 +192,55 @@ Rectangle {
// Right side: notification information
Rectangle{
id: contentRectangle
width: childrenRect.width
height: childrenRect.height
width: root.width - iconColumn.width - contentRow.spacing - contentRow.anchors.margins*2
height: Math.max(iconRectangle.height, contentBodyColumn.implicitHeight)
color: "transparent"
Column {
id: contentBodyColumn
width: root.width - iconColumn.width
width: parent.width
spacing: Config.spacing
Text {
id: bodyText
text: root.notification.body
visible: root.notification.body ? true : false
Item {
id: titleTextItem
width: parent.width-closingMouseArea.width
height: closingMouseArea.height
Text {
id: bodyTitleText
anchors.centerIn: parent
text: (!notification.appName || !notification.appName.split(".").pop()
? "Message"
: notification.appName.split(".").pop().charAt(0).toUpperCase()
+ notification.appName.split(".").pop().slice(1))
font.family: Config.font
font.bold: true
font.pixelSize: Config.screen_height_to_font_medium(screen.height)
color: textColor
}
}
Rectangle {
width: parent.width
height: Math.max(iconRectangle.height - titleTextItem.height - contentBodyColumn.spacing, 64)
color: textBGColor
radius: Config.screen_big_radius_fun(screen.height)
border.width: Config.border_width
border.color: textColor
Item{
anchors.fill: parent
anchors.margins: Config.spacing
Text {
id: bodyText
// If only summary exists (no body), show summary on the right instead of left
text: root.hasBody ? root.notification.body : (root.showSummaryOnRight ? root.notification.summary : "")
visible: root.hasBody || root.showSummaryOnRight
width: contentBodyColumn.width
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_tiny(screen.height)
color: textColor
wrapMode: Text.Wrap
width: parent.width - 2*parent.anchors.margins
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_tiny(screen.height)
color: textColor
wrapMode: Text.Wrap
}
}
}
}
}
@@ -200,6 +260,8 @@ Rectangle {
spacing: Config.spacing
anchors.fill: parent
anchors.margins: Config.spacing
// Expose root to delegates without direct id lookup (avoids ReferenceError inside Repeater)
property var bodyRoot: root
Repeater {
id: notificationActionRepeater
@@ -207,13 +269,28 @@ Rectangle {
delegate: MouseArea{
id: actionMouseArea
width: actionsRow.width/notification.actions.length
width: parent.width / parent.bodyRoot.notification.actions.length
height: actionRectangle.height
anchors.verticalCenter: parent.verticalCenter
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: {
// Track hover for pause logic via parent's bodyRoot (avoids direct 'root' lookup)
const body = parent.bodyRoot
if (!body) return
if (containsMouse) body._hoveredActionCount++
else body._hoveredActionCount = Math.max(0, body._hoveredActionCount - 1)
}
Component.onDestruction: {
if (containsMouse && parent && parent.bodyRoot) {
const body = parent.bodyRoot
body._hoveredActionCount = Math.max(0, body._hoveredActionCount - 1)
}
}
Rectangle {
id: actionRectangle
width: parent.width
@@ -248,7 +325,7 @@ Rectangle {
mouse.accepted=true
modelData.invoke()
// resident check is allready in .invoke()
root.closeNotification(false)
parent.bodyRoot.closeNotification(false)
}
}
}
@@ -281,6 +358,8 @@ Rectangle {
width: parent.width - inlineReplyButtonMouseArea.width
height: inlineReplyButtonText.height
anchors.verticalCenter: parent.verticalCenter
hoverEnabled: true
activeFocusOnTab: true
placeholderText: notification.hints["x-kde-reply-placeholder-text"] != null ? notification.hints["x-kde-reply-placeholder-text"] : notification.inlineReplyPlaceholder
placeholderTextColor: inlineReplyTextField.activeFocus ? Colors.secondary_fg : Colors.secondary
@@ -352,9 +431,8 @@ Rectangle {
running: root.duration > 0
from: 1.0
to: 0.0
duration: root.duration
paused: mouseArea.containsMouse || closingMouseArea.containsMouse || inlineReplyButtonMouseArea.containsMouse
duration: root.duration
paused: isHovered && !isHistory
onFinished: root.closeNotification(false)
}
}
@@ -390,7 +468,7 @@ Rectangle {
console.log("\x1b[31m\t"+"notification.transient: " + notification.transient+"\x1b[0m")
console.log("\x1b[31m\t"+"notification.hasInlineReply: " + notification.hasInlineReply+"\x1b[0m")
console.log("\x1b[31m\t"+"notification.hasActionIcons: " + notification.hasActionIcons+"\x1b[0m")
console.log("\x1b[31m\t"+"image.source: " + iconImage.source+"\x1b[0m")
console.log("\x1b[31m\t"+"urgency: " + urgency+"\x1b[0m")
console.log("\x1b[31m\t"+"duration: " + duration+"\x1b[0m")
@@ -0,0 +1,482 @@
import QtQuick
import Quickshell
import QtQuick.Controls
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Notifications
import "../../config"
import "./notificationServer"
PanelWindow {
id: root
implicitWidth: screen.width/5
implicitHeight: screen.height - Config.barHeight(screen.height)
color: "transparent"
visible: true
focusable: true
anchors {
top: true
right: true
}
property string textColor: Colors.primary
property string textBGColor: Qt.alpha(Colors.primaryContainer, 0.75)
property string bGColor: Qt.alpha(Colors.primaryContainer, 0.5)
property real bigFontSize: Config.screen_height_to_font_medium(screen.height)
property real fontSize: Config.screen_height_to_font_small(screen.height)
property var expandedGroups: ({})
property var grouped: {
const vals = NotificationS.trackedNotifications.values
if (!vals || vals.length === 0) return []
let map = {}
for (let i = 0; i < vals.length; ++i) {
const n = vals[i]
const key = (n && n.appName && n.appName !== "") ? n.appName : "Unknown"
if (!map[key]) map[key] = []
map[key].push(n)
}
let result = []
for (const k in map) result.push({ appName: k, notes: map[k] })
result.sort((a,b)=>a.appName.localeCompare(b.appName))
return result
}
function toggleGroup(appName) {
let copy = Object.assign({}, expandedGroups)
copy[appName] = !(copy[appName] === true)
expandedGroups = copy
}
function clearGroup(appName) {
const vals = NotificationS.trackedNotifications.values.slice()
for (let i=0;i<vals.length;++i){
const n=vals[i]
const key = (n && n.appName && n.appName !== "") ? n.appName : "Unknown"
if (key===appName) { if (n.tracked) n.dismiss(); else if (n.close) n.close() }
}
}
function clearAll() {
const vals = NotificationS.trackedNotifications.values.slice()
for (let i=0;i<vals.length;++i) if (vals[i].tracked) vals[i].dismiss()
expandedGroups = ({})
}
function closeNHistory() {
console.log("[NotificationHistory] closeNHistory() requested")
NotificationS.historyHovered = false
NotificationS.closeHistoryRequested()
}
property bool _allowAutoClose: false
Timer {
id: graceTimer
interval: 500
running: root.visible && !_allowAutoClose
repeat: false
onTriggered: _allowAutoClose = true
}
Timer {
id: hoverCloseTimer
interval: NotificationS.historyAutoCloseDelay
running: root.visible && _allowAutoClose && !NotificationS.historyHovered && !NotificationS.buttonHovered
repeat: false
onTriggered: {
if (!NotificationS.historyHovered && !NotificationS.buttonHovered) {
console.log("[NotificationHistory] Auto-closing after hover timeout (" + interval + "ms)")
root.closeNHistory()
}
}
}
onVisibleChanged: {
if (!visible) {
NotificationS.historyHovered = false
_allowAutoClose = false
}
}
Component.onDestruction: NotificationS.historyHovered = false
FocusScope {
id: nHistoryScope
anchors.fill: parent
focus: true
Keys.onReleased: function(event) {
if (event.key === Qt.Key_Escape) {
console.log("Pressed Escape, closing NotificationHistory")
root.closeNHistory()
}
}
}
Rectangle {
id: background
color: bGColor
// opacity: 0.5
anchors.fill: parent
border.width: Config.border_width
border.color: textColor
radius: Config.screen_big_radius_fun(screen.height)
HoverHandler {
id: backgroundHoverHandler
blocking: false
grabPermissions: PointerHandler.CanTakeOverFromAnything
onHoveredChanged: {
NotificationS.historyHovered = hovered
if (Config.debug == "Notification") console.log("[NotificationHistory] background hovered:", hovered)
}
}
Column{
id: bgColumn
anchors.fill: parent
anchors.margins: Config.screen_spacing_fun(screen.height)
spacing: Config.screen_spacing_fun(screen.height)
Rectangle {
id: topBar
width: parent.width
height: bigFontSize*2.5
color: textBGColor
radius: Config.screen_big_radius_fun(screen.height)
border.width: Config.border_width
border.color: textColor
Row{
id: backgroundLeft
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Config.screen_spacing_fun(screen.height)
spacing: Config.screen_spacing_fun(screen.height)
Text{
text: "Notifications:"
font.family: Config.font
font.pixelSize: topBar.height*0.75
color: textColor
}
}
Row{
id: backgroundRight
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Config.screen_spacing_fun(screen.height)
spacing: Config.screen_spacing_fun(screen.height)
Text{
text: NotificationS.notificationPopupEnabled ? "󰂚" : "󰂛"
font.family: Config.font
font.pixelSize: topBar.height*0.75
color: textColor
}
Switch {
id: notificationVolumeSwitch
implicitWidth: notificationVolumeSwitch.height*2
implicitHeight: topBar.height*0.75
anchors.verticalCenter: parent.verticalCenter
checked: NotificationS.notificationPopupEnabled
hoverEnabled: true
HoverHandler {
id: switchHoverHandler
cursorShape: Qt.PointingHandCursor
blocking: false
grabPermissions: PointerHandler.CanTakeOverFromAnything
onHoveredChanged: if (hovered) NotificationS.historyHovered = true
}
indicator: Rectangle {
implicitWidth: notificationVolumeSwitch.width
implicitHeight: notificationVolumeSwitch.height
radius: height / 2
color: notificationVolumeSwitch.checked ? Colors.secondary : Colors.secondaryContainer
Behavior on color {
ColorAnimation { duration: 150 }
}
Rectangle {
width: parent.height*0.8
height: parent.height*0.8
radius: width / 2
anchors.verticalCenter: parent.verticalCenter
x: notificationVolumeSwitch.checked ? parent.width - width - 3 : 3
color: notificationVolumeSwitch.checked ? Colors.secondaryContainer : Colors.secondary
Behavior on x {
NumberAnimation {
duration: 180
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation { duration: 150 }
}
}
}
onCheckedChanged: {
NotificationS.notificationPopupEnabled = notificationVolumeSwitch.checked
console.log("NotificationS.notificationPopupEnabled:", NotificationS.notificationPopupEnabled)
}
}
MouseArea {
id: closeArea
width: closeArea.height
height: topBar.height*0.75
anchors.verticalCenter: parent.verticalCenter
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) NotificationS.historyHovered = true
Rectangle{
anchors.fill: parent
color: closeArea.containsMouse ? "red" : textBGColor
radius: Config.screen_big_radius_fun(screen.height)
border.width: Config.border_width
border.color: closeArea.containsMouse ? "white" : textColor
Text{
text: ""
anchors.centerIn: parent
font.family: Config.font
font.pixelSize: closeArea.height - Config.spacing
color: closeArea.containsMouse ? "white" : textColor
}
}
onClicked: {
root.clearAll()
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - topBar.height - bgColumn.spacing
color: textBGColor
radius: Config.screen_big_radius_fun(screen.height)
border.width: Config.border_width
border.color: textColor
Text {
id: noNotificationText
anchors.centerIn: parent
visible: NotificationS.notificationNum == 0
text: "No notifications"
font.family: Config.font
font.pixelSize: bigFontSize
color: textColor
}
Flickable {
id: flick
anchors.fill: parent
anchors.margins: Config.screen_spacing_fun(screen.height)
contentWidth: width
contentHeight: contentColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: contentColumn
width: flick.width
spacing: Config.spacing
Repeater {
id: groupedRepeater
model: root.grouped
delegate: Item {
id: delegateRoot
required property var modelData
required property int index
property var group: modelData
property bool isGrouped: group.notes.length > 1
property bool isExpanded: root.expandedGroups[group.appName] === true
width: contentColumn.width
implicitHeight: isGrouped ? groupedCard.implicitHeight : singleBody.implicitHeight
height: implicitHeight
HoverHandler {
blocking: false
grabPermissions: PointerHandler.CanTakeOverFromAnything
onHoveredChanged: if (hovered) NotificationS.historyHovered = true
}
NotificationBody {
id: singleBody
visible: !delegateRoot.isGrouped
width: parent.width
screen: root.screen
notification: delegateRoot.group.notes[0]
isHistory: true
onDismissed: function(notification, dismiss) {
if (notification && notification.tracked) notification.dismiss()
else if (notification && notification.close) notification.close()
}
}
Rectangle {
id: groupedCard
visible: delegateRoot.isGrouped
width: parent.width
implicitHeight: groupColumn.implicitHeight + Config.spacing * 2
color: Qt.alpha(Colors.surfaceContainerHigh, 0.45)
radius: Config.screen_big_radius_fun(root.screen.height)
border.width: Config.border_width
border.color: root.textColor
clip: true
Column {
id: groupColumn
anchors.fill: parent
anchors.margins: Config.spacing
spacing: Config.spacing
Rectangle {
id: groupHeader
width: parent.width
height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5
color: root.bGColor
radius: Config.radius
border.width: Config.border_width
border.color: root.textColor
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onContainsMouseChanged: if (containsMouse) NotificationS.historyHovered = true
onClicked: root.toggleGroup(group.appName)
}
Row {
id: headerLeft
anchors.left: parent.left
anchors.leftMargin: Config.spacing
anchors.verticalCenter: parent.verticalCenter
spacing: Config.spacing
Text {
id: expandIcon
text: delegateRoot.isExpanded ? "󰅀" : "󰅂"
font.family: Config.font
font.pixelSize: root.fontSize
color: root.textColor
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: appNameText
text: group.appName
font.family: Config.font
font.pixelSize: root.fontSize
font.bold: true
color: root.textColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
id: countText
text: "(" + group.notes.length + ")"
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height)
color: Qt.alpha(root.textColor, 0.8)
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: headerRight
anchors.right: parent.right
anchors.rightMargin: Config.spacing
anchors.verticalCenter: parent.verticalCenter
spacing: Config.spacing
MouseArea {
id: clearGroupArea
width: bigFontSize + Config.spacing
height: bigFontSize + Config.spacing
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) NotificationS.historyHovered = true
onClicked: function(mouse) {
mouse.accepted = true
root.clearGroup(group.appName)
}
Rectangle{
anchors.fill: parent
radius: Config.screen_big_radius_fun(screen.height)
color: clearGroupArea.containsMouse ? "red" : textBGColor
border.width: Config.border_width
border.color: clearGroupArea.containsMouse ? "white" : textColor
Text{
id: clearGroupIcon
anchors.centerIn: parent
text: ""
font.family: Config.font
font.pixelSize: bigFontSize
color: clearGroupArea.containsMouse ? "white" : textColor
}
}
}
}
}
Column {
id: notificationsColumn
width: parent.width
spacing: Config.spacing
visible: delegateRoot.isExpanded
Repeater {
model: group.notes
delegate: NotificationBody {
required property var modelData
required property int index
width: notificationsColumn.width
screen: root.screen
notification: modelData
isHistory: true
onDismissed: function(notification, dismiss) {
if (notification && notification.tracked) notification.dismiss()
else if (notification && notification.close) notification.close()
}
}
}
}
Text {
visible: !delegateRoot.isExpanded && group.notes.length > 1
width: parent.width
text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand"
font.family: Config.font
font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height)
color: Qt.alpha(root.textColor, 0.6)
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,68 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import "../../config"
import "./notificationServer"
Item { // container, invisible
id: nHistoryLoader
LazyLoader {
id: notifificationHistoryLoader
// active: true
active: false
NotificationHistory {
id: nHistory
// closeNHistory() is defined inside NotificationHistory.qml and
// emits NotificationS.closeHistoryRequested(); the loader listens below
// and actually deactivates. Keeping instance id for potential direct access.
}
}
// React to close requests coming from NotificationHistory (hover timer, Escape)
// or any other component via the singleton signal. This is the single
// source of truth for deactivating the history panel.
Connections {
target: NotificationS
function onCloseHistoryRequested() {
console.log("[NotificationHistoryLoader] closeHistoryRequested received -> closing")
nHistoryLoader.closeNHistory()
}
}
GlobalShortcut {
name: "NotificationHistory"
description: "Open Notification History"
onPressed: {
console.log("Pressed Meta+Shift+N (Hyprland global shortcut)")
notifificationHistoryLoader.active = !notifificationHistoryLoader.active
}
}
function openNHistory() {
notifificationHistoryLoader.active = true
}
function closeNHistory() {
// Reset hover states so timer doesn't immediately re-trigger on next open
NotificationS.historyHovered = false
// buttonHovered is managed by the button itself; don't force false here
// in case button is still hovered when history closes
notifificationHistoryLoader.active = false
}
function toggleNHistory() {
if (notifificationHistoryLoader.active) {
closeNHistory()
} else {
openNHistory()
}
}
Component.onCompleted: {
console.log("[NotificationHistoryLoader]:", NotificationS)
}
}
@@ -12,10 +12,24 @@ Item {
property var focusedMonitor: null
property var newestNotification: null
property bool soundEnabled: true
property bool notificationPopupEnabled: true
property alias trackedNotifications: notificationServer.trackedNotifications
property int notificationNum: trackedNotifications.values.length
signal notificationReceived(var notification)
// ── Hover state for history auto-close ──
// Updated by NotificationHistory.qml (PanelWindow hover) and NotificationButton.qml (bar button hover)
property bool historyHovered: false
property bool buttonHovered: false
property int historyAutoCloseDelay: 1000
signal closeHistoryRequested()
// Debug logging for hover transitions
onHistoryHoveredChanged: Config.debug == "Notification" && console.log("[NotificationServer] historyHovered:", historyHovered)
onButtonHoveredChanged: Config.debug == "Notification" && console.log("[NotificationServer] buttonHovered:", buttonHovered)
function correctMonitor(screen) {
return (Hyprland.monitorFor(screen) === focusedMonitor)
}
@@ -55,7 +69,7 @@ Item {
// return
// }
notification.tracked = true
Config.debug == "Notification" && console.log(
!notification.lastGeneration && Config.debug == "Notification" && console.log(
"[NotificationServer] Notification received:",
notification.appName, notification.id, notification.summary, notification.body
)
@@ -80,9 +94,9 @@ Item {
repeat: false
onTriggered: {
focusedMonitor = Hyprland.focusedMonitor
notificationReceived(notification)
if(!notification.lastGeneration && notificationServerState.soundEnabled){
if((!notification.lastGeneration && notificationServerState.notificationPopupEnabled) || Config.debug == "Notification"){
focusedMonitor = Hyprland.focusedMonitor
notificationReceived(notification)
notificationSound.play()
}
+7
View File
@@ -19,6 +19,9 @@ ShellRoot {
// Notifications
NotificationLoader {}
NotificationHistoryLoader {
id: nHistoryLoader
}
//Wallpaper picker
WallpaperPickerLoader {}
@@ -41,6 +44,10 @@ ShellRoot {
wlLoader.openWlogout()
}
function toggleNotificationHistory() {
nHistoryLoader.toggleNHistory()
}
GlobalShortcut {
name: "QsReload"