Files
quickshell/modules/notification/NotificationHistory.qml
T

518 lines
24 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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() {
// Centralized close via singleton signal fixes previous ReferenceError
// where this file tried to access notifificationHistoryLoader.id directly
// (that id lives in NotificationHistoryLoader.qml and is not in scope here).
console.log("[NotificationHistory] closeNHistory() requested")
NotificationS.historyHovered = false
NotificationS.closeHistoryRequested()
}
// ── Hover tracking via NotificationServer ──
// History reports its hover state to the singleton; button does the same.
// Timer auto-closes when neither is hovered for historyAutoCloseDelay ms.
property bool _allowAutoClose: false
HoverHandler {
id: historyHoverHandler
onHoveredChanged: NotificationS.historyHovered = hovered
}
// Fallback MouseArea that doesn't steal clicks (acceptedButtons: NoButton)
// Ensures hover is detected even if HoverHandler is missed on PanelWindow boundaries.
// Placed with high z but NoButton so it doesn't block inner MouseAreas.
MouseArea {
id: historyHoverMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onContainsMouseChanged: NotificationS.historyHovered = containsMouse
}
// Grace timer: don't allow auto-close immediately after opening,
// giving keyboard users time to move mouse to history.
Timer {
id: graceTimer
interval: 500
running: root.visible && !_allowAutoClose
repeat: false
onTriggered: _allowAutoClose = true
}
Timer {
id: hoverCloseTimer
interval: NotificationS.historyAutoCloseDelay
// Only run when panel is visible, grace passed, and neither element is hovered
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()
}
}
}
// Ensure hover state is cleared when window hides/destroys
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 {
anchors.fill: parent
color: "lightblue"
visible: false // debug helper, keep hidden in normal use
}
}
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)
// Text {
// anchors.centerIn: parent
// font.family: Config.font
// font.pixelSize: bigFontSize
// color: textColor
// wrapMode: Text.Wrap
// text: NotificationS.trackedNotifications.values[0]
// }
// Component.onCompleted: {
// for (var notification in NotificationS.trackedNotifications.values) {
// console.log("[NotificationHistory]: " + notification.appName + " Notifications")
// }
// for (var i =0; i < NotificationS.notificationNum; i++) {
// console.log("[NotificationHistory]: " + NotificationS.trackedNotifications.values[i].appName + " Notifications")
// }
// }
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: backgroundCenter
// anchors.centerIn: parent
// spacing: Config.screen_small_bar_spacing_fun(screen.height)
// }
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
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
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()
// closeNHistory()
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - topBar.height - bgColumn.spacing //- bgColumn.anchors.margins*2
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
// ── Grouped view (only groups with >1 notification are grouped) ──
// Uses root.grouped (sorted by appName). Each app group with
// more than one notification is rendered as a collapsible
// card with a header (appName + count + expand/collapse +
// clear-group). Singletons are rendered directly as a
// NotificationBody without a header.
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
// height adapts to which component is visible
implicitHeight: isGrouped ? groupedCard.implicitHeight : singleBody.implicitHeight
height: implicitHeight
// ── Singleton: plain NotificationBody ──
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()
}
}
// ── Grouped card (count > 1) ──
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
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
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
}
}
}
}
}
}
}
}
}
}
}