diff --git a/modules/bar/widgets/NotificationButton.qml b/modules/bar/widgets/NotificationButton.qml index 9326049..e2c08be 100644 --- a/modules/bar/widgets/NotificationButton.qml +++ b/modules/bar/widgets/NotificationButton.qml @@ -13,22 +13,63 @@ MouseArea { cursorShape: Qt.PointingHandCursor onClicked: { - NotificationS.soundEnabled = !NotificationS.soundEnabled + NotificationS.toggleOverview() } - + Rectangle { anchors.fill: parent - color: "transparent" + color: notificationButton.containsMouse || NotificationS.overviewVisible ? Colors.tertiary : "transparent" radius: Config.radius_fun(notificationButton.height) + border.width: Config.border_width + border.color: notificationButton.containsMouse || NotificationS.overviewVisible ? Colors.tertiary_fg : "transparent" Text { id: notificationText anchors.centerIn: parent - // text: Quickshell.iconPath("discord") - text: NotificationS.soundEnabled ? "󰂚" : "󰂛" + text: "󰂚" font.pixelSize: Config.bigIconSize(bar.height) font.family: Config.font - color: Colors.secondaryContainer_fg + color: NotificationS.overviewVisible ? Colors.tertiary_fg : Colors.secondaryContainer_fg + } + + // count badge + Rectangle { + id: badge + visible: NotificationS.trackedNotifications.values.length > 0 + width: badgeText.implicitWidth + 6 + height: badgeText.implicitHeight + 4 + radius: height / 2 + color: Colors.errorContainer + border.width: 1 + border.color: Colors.error + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 2 + anchors.rightMargin: 2 + Text { + id: badgeText + anchors.centerIn: parent + text: NotificationS.trackedNotifications.values.length > 99 ? "99+" : NotificationS.trackedNotifications.values.length + font.family: Config.font + font.pixelSize: Config.labelSize(bar.height) * 0.6 + font.bold: true + color: Colors.error_fg + } + } + + // muted indicator dot + Rectangle { + visible: !NotificationS.soundEnabled + width: 8 + height: 8 + radius: 4 + color: Colors.error + border.width: 1 + border.color: Colors.error_fg + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 3 + anchors.rightMargin: 3 } // Image{ diff --git a/modules/notification/NotificationOverview.qml b/modules/notification/NotificationOverview.qml new file mode 100644 index 0000000..2f496cc --- /dev/null +++ b/modules/notification/NotificationOverview.qml @@ -0,0 +1,795 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import Quickshell.Services.Notifications +import QtQuick.Layouts +import QtQuick.Controls +import "../../config" +import "./notificationServer" + +PanelWindow { + id: root + + required property var screen + + anchors { + top: true + left: true + right: true + bottom: true + } + + exclusiveZone: 0 + color: "transparent" + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // state for expanded groups: appName -> bool + property var expandedGroups: ({}) + + // reactive grouped model: depends on trackedNotifications count + property var grouped: { + // establish binding dependency on trackedNotifications + const vals = NotificationS.trackedNotifications.values + // touch length to trigger re-evaluation + const len = vals.length + if (len < 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] }) + } + // sort alphabetically for stable order; could sort by most recent instead + 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) { + // dismiss tracked notification + 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) { + const n = vals[i] + if (n.tracked) n.dismiss() + } + // also collapse all + expandedGroups = ({}) + } + + // close handling + function closeOverview() { + NotificationS.overviewVisible = false + } + + // keyboard handling via contentItem + contentItem { + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + closeOverview() + event.accepted = true + } + } + } + + // background dim + click to close + MouseArea { + id: bgMouse + anchors.fill: parent + onClicked: function(mouse) { root.closeOverview() } + // eat wheel to prevent flickable behind? + } + + // main panel container + Rectangle { + id: panel + + // panel size: constrained by screen + width: Math.min(520, Math.max(380, root.screen.width * 0.32)) + property real maxPanelHeight: root.screen.height * 0.72 + height: maxPanelHeight + + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Config.barHeight(root.screen.height) + Config.spacing + anchors.rightMargin: Config.spacing + + radius: Config.radius + color: Qt.alpha(Colors.surfaceContainer, 0.96) + border.width: Config.border_width + border.color: Colors.outlineVariant + clip: true + + // prevent bg click when clicking panel + MouseArea { + anchors.fill: parent + hoverEnabled: false + onClicked: function(mouse) { mouse.accepted = true } + } + + Column { + id: panelColumn + anchors.fill: parent + anchors.margins: Config.spacing + spacing: Config.spacing + + // ───── Header ───── + Rectangle { + id: header + width: parent.width + implicitHeight: headerRow.implicitHeight + Config.spacing*2 + radius: Config.small_radius + color: Colors.surfaceContainerHigh + border.width: Config.border_width + border.color: Colors.outlineVariant + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Config.spacing + spacing: Config.spacing + + // title + count + Text { + id: titleText + Layout.alignment: Qt.AlignVCenter + text: "Notifications" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_small(root.screen.height) + font.bold: true + color: Colors.primary + } + Rectangle { + id: countBadge + Layout.alignment: Qt.AlignVCenter + visible: NotificationS.trackedNotifications.values.length > 0 + Layout.preferredWidth: countText.implicitWidth + Config.spacing + Layout.preferredHeight: countText.implicitHeight + Config.small_spacing + radius: height/2 + color: Colors.primaryContainer + border.width: Config.border_width + border.color: Colors.primary + Text { + id: countText + anchors.centerIn: parent + text: NotificationS.trackedNotifications.values.length + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + font.bold: true + color: Colors.primaryContainer_fg + } + } + + Item { Layout.preferredWidth: Config.spacing; Layout.preferredHeight: 1; Layout.alignment: Qt.AlignVCenter } + + // mute toggle (moved from NotificationButton) + Rectangle { + id: muteBtn + Layout.preferredWidth: muteText.implicitWidth + Config.spacing*2 + Layout.preferredHeight: muteText.implicitHeight + Config.spacing + Layout.alignment: Qt.AlignVCenter + radius: Config.radius_fun(height)*2 + color: muteArea.containsMouse ? Colors.tertiaryContainer : (NotificationS.soundEnabled ? Colors.secondaryContainer : Colors.errorContainer) + border.width: Config.border_width + border.color: muteArea.containsMouse ? Colors.tertiary : (NotificationS.soundEnabled ? Colors.secondary : Colors.error) + Text { + id: muteText + anchors.centerIn: parent + text: NotificationS.soundEnabled ? "󰂚" : "󰂛" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_small(root.screen.height) + color: muteArea.containsMouse ? Colors.tertiary_fg : (NotificationS.soundEnabled ? Colors.secondary : Colors.error_fg) + } + MouseArea { + id: muteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + NotificationS.soundEnabled = !NotificationS.soundEnabled + mouse.accepted = true + } + } + } + + // clear all + Rectangle { + id: clearAllBtn + Layout.preferredWidth: clearAllText.implicitWidth + Config.spacing*2 + Layout.preferredHeight: muteText.implicitHeight + Config.spacing + Layout.alignment: Qt.AlignVCenter + radius: Config.radius_fun(height)*2 + visible: NotificationS.trackedNotifications.values.length > 0 + color: clearAllArea.containsMouse ? Colors.errorContainer : Colors.surfaceContainerHighest + border.width: Config.border_width + border.color: clearAllArea.containsMouse ? Colors.error : Colors.outline + Text { + id: clearAllText + anchors.centerIn: parent + text: "Clear all" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: clearAllArea.containsMouse ? Colors.error_fg : Colors.primary + } + MouseArea { + id: clearAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + root.clearAll() + mouse.accepted = true + } + } + } + + Item { Layout.fillWidth: true; Layout.preferredWidth: 8; Layout.preferredHeight: 1 } + + // close X + Rectangle { + id: closeBtn + Layout.preferredWidth: closeText.implicitHeight + Config.spacing + Layout.preferredHeight: closeText.implicitHeight + Config.spacing + Layout.alignment: Qt.AlignVCenter + radius: Config.radius + color: closeArea.containsMouse ? Colors.errorContainer : "transparent" + border.width: Config.border_width + border.color: closeArea.containsMouse ? Colors.error : Colors.outlineVariant + Text { + id: closeText + anchors.centerIn: parent + text: "✕" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_small(root.screen.height) + color: closeArea.containsMouse ? Colors.error_fg : Colors.primary + } + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + root.closeOverview() + mouse.accepted = true + } + } + } + } + } + + // ───── Scrollable grouped list ───── + Flickable { + id: flick + width: parent.width + height: panel.height - header.height - panelColumn.spacing - Config.spacing*2 + 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 + + // empty placeholder + Rectangle { + width: parent.width + height: 80 + radius: Config.small_radius + color: Qt.alpha(Colors.surfaceContainerLow, 0.6) + border.width: Config.border_width + border.color: Colors.outlineVariant + visible: NotificationS.trackedNotifications.values.length === 0 + Text { + anchors.centerIn: parent + text: "No notifications" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_small(root.screen.height) + color: Colors.outline + } + } + + Repeater { + id: groupRepeater + model: root.grouped + + delegate: Column { + id: groupColumn + required property var modelData + required property int index + + property string appName: modelData.appName + property var notes: modelData.notes + property bool isExpanded: root.expandedGroups[appName] === true + property int count: notes.length + // pick representative icon from first notification + property var repNote: notes.length > 0 ? notes[0] : null + + width: contentColumn.width + spacing: Config.spacing / 2 + + // ── Group header ── + Rectangle { + id: groupHeader + width: parent.width + height: Math.max(headerContent.implicitHeight + Config.spacing*2, 48) + radius: Config.small_radius + color: groupHeaderArea.containsMouse ? Colors.secondaryContainer : Colors.surfaceContainerHigh + border.width: Config.border_width + border.color: groupColumn.isExpanded ? Colors.primary : Colors.outlineVariant + + Row { + id: headerContent + anchors.fill: parent + anchors.margins: Config.spacing + spacing: Config.spacing + anchors.verticalCenter: parent.verticalCenter + + // app icon + Image { + id: groupIcon + width: 28 + height: 28 + anchors.verticalCenter: parent.verticalCenter + asynchronous: true + fillMode: Image.PreserveAspectFit + source: { + if (!groupColumn.repNote) return Quickshell.iconPath("notifications") + if (groupColumn.repNote.image) return groupColumn.repNote.image + if (groupColumn.repNote.appIcon) return Quickshell.iconPath(groupColumn.repNote.appIcon) + const entry = groupColumn.repNote.desktopEntry ? DesktopEntries.byId(groupColumn.repNote.desktopEntry) : null + if (entry && entry.icon) { + const p = Quickshell.iconPath(entry.icon) + if (p && p !== "") return p + } + return Quickshell.iconPath("notifications") + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: groupHeader.width - groupIcon.width - expandArrow.width - clearGroupBtn.width - badge.width - headerContent.spacing*4 - Config.spacing*2 + + Text { + text: groupColumn.appName + width: parent.width + elide: Text.ElideRight + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_small(root.screen.height) + font.bold: true + color: Colors.primary + } + Text { + text: groupColumn.count + (groupColumn.count === 1 ? " notification" : " notifications") + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: Colors.outline + } + } + + Rectangle { + id: badge + width: badgeText.implicitWidth + Config.spacing + height: badgeText.implicitHeight + 4 + anchors.verticalCenter: parent.verticalCenter + radius: height/2 + color: groupColumn.isExpanded ? Colors.primary : Colors.secondaryContainer + border.width: Config.border_width + border.color: groupColumn.isExpanded ? Colors.primary_fg : Colors.outlineVariant + Text { + id: badgeText + anchors.centerIn: parent + text: groupColumn.count + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + font.bold: true + color: groupColumn.isExpanded ? Colors.primary_fg : Colors.secondary + } + } + + Text { + id: expandArrow + anchors.verticalCenter: parent.verticalCenter + text: groupColumn.isExpanded ? "▼" : "▶" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: Colors.primary + } + + // clear group button + Rectangle { + id: clearGroupBtn + width: clearGroupText.implicitWidth + Config.spacing + height: clearGroupText.implicitHeight + Config.spacing/2 + anchors.verticalCenter: parent.verticalCenter + radius: Config.radius_fun(height) + color: clearGroupArea.containsMouse ? Colors.errorContainer : Qt.alpha(Colors.surfaceContainerHighest, 0.8) + border.width: Config.border_width + border.color: clearGroupArea.containsMouse ? Colors.error : Colors.outlineVariant + Text { + id: clearGroupText + anchors.centerIn: parent + text: "✕" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: clearGroupArea.containsMouse ? Colors.error_fg : Colors.outline + } + MouseArea { + id: clearGroupArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: function(mouse) { + mouse.accepted = true + root.clearGroup(groupColumn.appName) + } + } + } + } + + MouseArea { + id: groupHeaderArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + // don't steal click when hitting clear button + onClicked: function(mouse) { + // if clear button contains mouse, ignore + if (clearGroupArea.containsMouse) return + mouse.accepted = true + root.toggleGroup(groupColumn.appName) + } + } + } + + // ── Group notifications (collapsed by default) ── + Column { + id: groupNotesColumn + width: parent.width + spacing: Config.spacing/2 + visible: groupColumn.isExpanded + // keep clipped and animate? simple visible + Repeater { + id: notesRepeater + model: groupColumn.notes + delegate: Rectangle { + id: histCard + required property var modelData + required property int index + property var notification: modelData + property int urgency: (notification.urgency ?? notification.hints["urgency"]) != null ? (notification.urgency ?? notification.hints["urgency"]) + 1 : 0 + property color textColor: urgency !== 3 ? Colors.primary : Colors.tertiary + property color textBGColor: urgency !== 3 ? Qt.alpha(Colors.primaryContainer, 0.92) : Qt.alpha(Colors.tertiaryContainer, 0.92) + + width: groupNotesColumn.width + // height determined by content + implicitHeight: cardInnerColumn.implicitHeight + Config.spacing*2 + radius: Config.small_radius + color: textBGColor + border.width: Config.border_width + border.color: textColor + clip: true + + Column { + id: cardInnerColumn + width: parent.width + spacing: 0 + + // content row + Rectangle { + id: cardContentRect + width: parent.width + // height based on max of icon vs text + implicitHeight: Math.max(iconCol.implicitHeight, bodyCol.implicitHeight) + Config.spacing*2 + color: "transparent" + + // close per-notification + MouseArea { + id: cardCloseArea + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Config.spacing/2 + width: cardCloseText.implicitHeight + Config.spacing + height: width + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + Rectangle { + anchors.fill: parent + radius: Config.small_radius + color: cardCloseArea.containsMouse ? Colors.errorContainer : "transparent" + border.width: Config.border_width + border.color: cardCloseArea.containsMouse ? Colors.error : histCard.textColor + Text { + id: cardCloseText + anchors.centerIn: parent + text: "✕" + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: cardCloseArea.containsMouse ? Colors.error_fg : histCard.textColor + } + } + onClicked: function(mouse) { + mouse.accepted = true + if (histCard.notification.tracked) histCard.notification.dismiss() + else histCard.notification.close() + } + } + + Row { + id: cardRow + anchors.fill: parent + anchors.margins: Config.spacing + anchors.rightMargin: cardCloseArea.width + Config.spacing + spacing: Config.spacing + + // icon side + Rectangle { + id: iconColWrapper + width: 1.25 * iconImg.width + height: Math.max(iconCol.implicitHeight, bodyCol.implicitHeight) + color: "transparent" + Column { + id: iconCol + width: parent.width + spacing: Config.spacing/2 + Image { + id: iconImg + width: root.screen.width / 32 + height: width + anchors.horizontalCenter: parent.horizontalCenter + asynchronous: true + fillMode: Image.PreserveAspectFit + source: { + if (histCard.notification.image) return histCard.notification.image + if (histCard.notification.appIcon) return Quickshell.iconPath(histCard.notification.appIcon) + const entry = histCard.notification.desktopEntry ? DesktopEntries.byId(histCard.notification.desktopEntry) : null + if (entry && entry.icon) { + const p = Quickshell.iconPath(entry.icon) + if (p && p !== "") return p + } + return Quickshell.iconPath("notifications") + } + } + Text { + id: histSummary + text: histCard.notification.summary + visible: histCard.notification.summary ? true : false + width: parent.width + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + font.bold: true + color: histCard.textColor + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + // body side + Rectangle { + id: bodyWrapper + width: parent.width - iconColWrapper.width - cardRow.spacing + height: Math.max(iconCol.implicitHeight, bodyCol.implicitHeight) + color: "transparent" + Column { + id: bodyCol + width: parent.width + spacing: Config.spacing/2 + Text { + id: bodyTitle + text: { + if (histCard.notification.image) return histCard.notification.appName.charAt(0).toUpperCase() + histCard.notification.appName.slice(1) + return histCard.notification.appName ? histCard.notification.appName : "Message:" + } + font.family: Config.font + font.bold: true + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: histCard.textColor + elide: Text.ElideRight + width: parent.width + } + Text { + id: bodyText + text: histCard.notification.body + visible: histCard.notification.body ? true : false + width: parent.width + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: histCard.textColor + wrapMode: Text.Wrap + textFormat: Text.RichText + } + Text { + text: { + // time hint if available, else empty + if (histCard.notification.hints["x-dunst-stack-tag"]) return "" + return "" + } + visible: false + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: Colors.outline + } + } + } + } + } + + // actions row + Rectangle { + id: histActionRowRect + width: parent.width + height: Config.screen_height_to_font_tiny(root.screen.height)+2*Config.spacing + color: "transparent" + border.width: Config.border_width + border.color: histCard.textColor + visible: histCard.notification.actions.length > 0 + Row { + id: histActionsRow + spacing: Config.spacing + anchors.fill: parent + anchors.margins: Config.spacing + property var cardRoot: histCard + Repeater { + id: histActionRepeater + model: histCard.notification.actions + delegate: MouseArea { + required property var modelData + id: histActionArea + width: histActionsRow.width / histActionsRow.cardRoot.notification.actions.length + height: histActionRect.height + anchors.verticalCenter: parent.verticalCenter + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + Rectangle { + id: histActionRect + width: parent.width + height: histActionRow.height + Config.spacing + color: histActionArea.containsMouse ? Colors.secondaryContainer_fg : Colors.secondaryContainer + border.color: histActionArea.containsMouse ? Colors.secondary_fg : Colors.secondary + border.width: Config.border_width + radius: Config.radius_fun(histActionRect.height)*2 + Row { + id: histActionRow + anchors.centerIn: parent + spacing: Config.spacing/2 + Text { + text: modelData.text + width: Math.min(histActionRect.width - Config.spacing*2, implicitWidth) + elide: Text.ElideRight + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: histActionArea.containsMouse ? Colors.secondary_fg : Colors.secondary + } + } + } + onClicked: function(mouse) { + mouse.accepted = true + modelData.invoke() + // after invoke, dismiss if not resident handled by server + if (histCard.notification.tracked && !histCard.notification.resident) { + // optionally keep in history; don't auto dismiss + } + } + } + } + } + } + + // inline reply + Rectangle { + id: histInlineRect + width: parent.width + height: Config.screen_height_to_font_small(root.screen.height)+2*Config.spacing + color: "transparent" + border.width: Config.border_width + border.color: histCard.textColor + visible: histCard.notification.hasInlineReply + function sendReply(message) { + histCard.notification.sendInlineReply(message) + inlineField.focus = false + // keep notification in history; optionally dismiss? + } + Row { + id: histInlineRow + spacing: Config.spacing + anchors.fill: parent + anchors.margins: Config.spacing + TextField { + id: inlineField + width: parent.width - inlineSendArea.width - Config.spacing + height: inlineSendText.height + Config.spacing + anchors.verticalCenter: parent.verticalCenter + hoverEnabled: true + activeFocusOnTab: true + placeholderText: histCard.notification.hints["x-kde-reply-placeholder-text"] != null ? histCard.notification.hints["x-kde-reply-placeholder-text"] : histCard.notification.inlineReplyPlaceholder + placeholderTextColor: inlineField.activeFocus ? Colors.secondary_fg : Colors.secondary + font.family: Config.font + leftPadding: Config.spacing + background: Rectangle { + radius: Config.radius + color: inlineField.activeFocus ? Colors.tertiary_fg : Colors.primary_fg + border.color: inlineField.activeFocus ? Colors.tertiary : Colors.primary + border.width: Config.border_width + } + onAccepted: { + histInlineRect.sendReply(text) + } + } + MouseArea { + id: inlineSendArea + width: inlineSendText.width + 2*Config.spacing + height: inlineSendText.height + Config.spacing + anchors.verticalCenter: parent.verticalCenter + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + Rectangle { + anchors.fill: parent + radius: height*0.5 + color: inlineSendArea.containsMouse ? Colors.secondaryContainer_fg : Colors.secondaryContainer + border.color: inlineSendArea.containsMouse ? Colors.secondary_fg : Colors.secondary + border.width: Config.border_width + Text { + id: inlineSendText + text: "SEND" + anchors.centerIn: parent + font.family: Config.font + font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) + color: inlineSendArea.containsMouse ? Colors.secondary_fg : Colors.secondary + } + } + onClicked: function(mouse) { + mouse.accepted = true + histInlineRect.sendReply(inlineField.text) + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + // debug + Component.onCompleted: { + Config.debug == "Notification" && console.log("[NotificationOverview " + Hyprland.monitorFor(screen).activeWorkspace.id + "] completed") + } +} \ No newline at end of file diff --git a/modules/notification/NotificationOverviewLoader.qml b/modules/notification/NotificationOverviewLoader.qml new file mode 100644 index 0000000..36f20ff --- /dev/null +++ b/modules/notification/NotificationOverviewLoader.qml @@ -0,0 +1,42 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import "./notificationServer" + +Item { + id: overviewLoaderRoot + + // Per-screen PanelWindows for the overview. + // Uses Loader so windows are only created when overviewVisible is true. + Repeater { + model: Quickshell.screens + delegate: Loader { + id: screenLoader + required property var modelData + active: NotificationS.overviewVisible + sourceComponent: NotificationOverview { + screen: modelData + } + // optional: hide on screen removed + onActiveChanged: { + if (active) { + console.log("[NotificationOverviewLoader] opening overview on screen", Hyprland.monitorFor(modelData).activeWorkspace.id) + } + } + } + } + + // Global shortcut to toggle overview (optional) + GlobalShortcut { + name: "NotificationOverview" + description: "Toggle notification history overview" + onPressed: { + NotificationS.toggleOverview() + console.log("[NotificationOverviewLoader] shortcut toggle, now", NotificationS.overviewVisible) + } + } + + Component.onCompleted: { + console.log("[NotificationOverviewLoader] completed, overviewVisible:", NotificationS.overviewVisible) + } +} \ No newline at end of file diff --git a/modules/notification/notificationServer/NotificationServer.qml b/modules/notification/notificationServer/NotificationServer.qml index e075665..362c14f 100644 --- a/modules/notification/notificationServer/NotificationServer.qml +++ b/modules/notification/notificationServer/NotificationServer.qml @@ -13,9 +13,15 @@ Item { property var focusedMonitor: null property var newestNotification: null property bool soundEnabled: true + property bool overviewVisible: false + property alias trackedNotifications: notificationServer.trackedNotifications signal notificationReceived(var notification) + function toggleOverview() { + overviewVisible = !overviewVisible + } + function correctMonitor(screen) { return (Hyprland.monitorFor(screen) === focusedMonitor) } diff --git a/shell.qml b/shell.qml index 065b69f..6191218 100644 --- a/shell.qml +++ b/shell.qml @@ -19,6 +19,7 @@ ShellRoot { // Notifications NotificationLoader {} + NotificationOverviewLoader {} //Wallpaper picker WallpaperPickerLoader {}