2025-12-24 23:00:08 +01:00
|
|
|
pragma ComponentBehavior: Bound
|
2025-12-23 20:21:18 +01:00
|
|
|
import QtQuick
|
|
|
|
|
import Quickshell
|
|
|
|
|
import Quickshell.Wayland
|
2025-12-27 20:47:50 +01:00
|
|
|
import Quickshell.Hyprland
|
2025-12-23 20:21:18 +01:00
|
|
|
import "."
|
2025-12-28 01:45:57 +01:00
|
|
|
import "../../"
|
2025-12-23 20:21:18 +01:00
|
|
|
import QtQuick.Layouts
|
2025-12-28 17:36:11 +01:00
|
|
|
import Quickshell.Widgets
|
|
|
|
|
import "../settings/"
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
WlrLayershell {
|
|
|
|
|
id: root
|
2025-12-31 01:37:27 +01:00
|
|
|
required property var modelData
|
2025-12-28 01:45:57 +01:00
|
|
|
screen: {
|
2025-12-27 20:47:50 +01:00
|
|
|
// Iterate through all connected Quickshell screens
|
|
|
|
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
|
|
|
let screenCandidate = Quickshell.screens[i];
|
2025-12-28 01:45:57 +01:00
|
|
|
|
2025-12-27 20:47:50 +01:00
|
|
|
// Ask: "Is this screen the one Hyprland is currently focusing?"
|
|
|
|
|
if (Hyprland.monitorFor(screenCandidate) === Hyprland.focusedMonitor) {
|
|
|
|
|
return screenCandidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null; // Fallback (should rarely happen)
|
|
|
|
|
}
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
// 1. Position: Top Right Corner, covering the full height
|
|
|
|
|
// We make it a fixed width (e.g., 400px) so it doesn't block the whole screen
|
|
|
|
|
anchors {
|
|
|
|
|
top: true
|
|
|
|
|
right: true
|
|
|
|
|
}
|
|
|
|
|
margins {
|
2025-12-29 23:41:32 +01:00
|
|
|
top: 36
|
|
|
|
|
right: 00
|
2025-12-23 20:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-27 21:07:52 +01:00
|
|
|
implicitWidth: 300
|
2025-12-28 01:45:57 +01:00
|
|
|
implicitHeight: notifList.contentHeight + 20
|
2025-12-29 23:41:32 +01:00
|
|
|
Behavior on implicitHeight {
|
|
|
|
|
NumberAnimation {
|
|
|
|
|
duration: 300
|
|
|
|
|
easing.type: Easing.OutQuad
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
// 2. Layer: Put it ABOVE normal windows
|
|
|
|
|
layer: WlrLayer.Overlay
|
|
|
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
|
|
|
|
|
|
// 3. CRITICAL: Make the window itself invisible!
|
2025-12-29 23:41:32 +01:00
|
|
|
// We only want to see the
|
|
|
|
|
// notifications, not the container.
|
2025-12-23 20:21:18 +01:00
|
|
|
color: "transparent"
|
|
|
|
|
|
|
|
|
|
// 4. Input: Let clicks pass through empty areas
|
|
|
|
|
// (This is default behavior if the background is transparent in some compositors,
|
|
|
|
|
// but usually you need to be careful with handling mouse events here)
|
|
|
|
|
|
|
|
|
|
// THE SPAWNER
|
|
|
|
|
ListView {
|
|
|
|
|
id: notifList
|
2025-12-29 23:41:32 +01:00
|
|
|
anchors.top: parent.top
|
|
|
|
|
anchors.left: parent.left
|
|
|
|
|
anchors.right: parent.right
|
2025-12-28 17:36:11 +01:00
|
|
|
anchors.margins: 0
|
2025-12-23 20:21:18 +01:00
|
|
|
// Use 'spacing' to put gaps between notifications
|
2025-12-29 23:41:32 +01:00
|
|
|
spacing: 00
|
|
|
|
|
height: contentHeight
|
2025-12-23 20:21:18 +01:00
|
|
|
|
2025-12-24 15:41:03 +01:00
|
|
|
model: NotifServer.trackedNotifications
|
2025-12-23 20:21:18 +01:00
|
|
|
delegate: Item {
|
2025-12-24 23:00:08 +01:00
|
|
|
id: notifyItem
|
2025-12-29 23:41:32 +01:00
|
|
|
required property var index
|
|
|
|
|
readonly property bool isLast: index === (ListView.view.count - 1)
|
2025-12-24 23:00:08 +01:00
|
|
|
implicitWidth: ListView.view.width
|
2025-12-28 17:36:11 +01:00
|
|
|
implicitHeight: 85 // Fixed height is usually better for icon layouts
|
2025-12-29 23:41:32 +01:00
|
|
|
height: implicitHeight
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
required property var modelData
|
2025-12-24 15:41:03 +01:00
|
|
|
Timer {
|
|
|
|
|
id: timout
|
2025-12-28 17:36:11 +01:00
|
|
|
interval: 3000
|
2025-12-24 15:41:03 +01:00
|
|
|
running: true
|
2025-12-29 23:41:32 +01:00
|
|
|
onTriggered: notifyItem.modelData.dismiss()
|
2025-12-24 15:41:03 +01:00
|
|
|
}
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
color: Colors.background
|
2025-12-29 23:41:32 +01:00
|
|
|
bottomLeftRadius: notifyItem.isLast ? 20 : 0
|
2025-12-23 20:21:18 +01:00
|
|
|
border.color: Colors.color5
|
2025-12-29 23:41:32 +01:00
|
|
|
border.width: 0
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
// 2. Use RowLayout to put Image | Text side-by-side
|
2025-12-27 21:07:52 +01:00
|
|
|
RowLayout {
|
2025-12-28 01:45:57 +01:00
|
|
|
id: fullLayout
|
2025-12-23 20:21:18 +01:00
|
|
|
anchors.margins: 10
|
2025-12-26 00:37:39 +01:00
|
|
|
anchors.fill: parent
|
2025-12-28 17:36:11 +01:00
|
|
|
spacing: 10
|
2025-12-23 20:21:18 +01:00
|
|
|
|
|
|
|
|
// 🖼️ THE IMAGE ON THE LEFT
|
2025-12-28 17:36:11 +01:00
|
|
|
ClippingWrapperRectangle {
|
|
|
|
|
radius: 10
|
|
|
|
|
implicitWidth: 64
|
|
|
|
|
implicitHeight: 64
|
|
|
|
|
visible: notifyItem.modelData.image !== ""
|
|
|
|
|
IconImage {
|
2025-12-26 00:37:39 +01:00
|
|
|
|
2025-12-28 17:36:11 +01:00
|
|
|
// Use the image if available, otherwise hide this space?
|
|
|
|
|
// Or you could use an icon fallback.
|
|
|
|
|
source: notifyItem.modelData.image
|
2025-12-23 20:21:18 +01:00
|
|
|
|
2025-12-28 17:36:11 +01:00
|
|
|
// Hide if no image exists so text takes full width
|
|
|
|
|
visible: notifyItem.modelData.image !== ""
|
2025-12-23 20:21:18 +01:00
|
|
|
|
2025-12-28 17:36:11 +01:00
|
|
|
// Fixed size for consistency
|
|
|
|
|
implicitSize: 30
|
2025-12-23 20:21:18 +01:00
|
|
|
|
2025-12-28 17:36:11 +01:00
|
|
|
// Crop it nicely so it doesn't stretch
|
2025-12-23 20:21:18 +01:00
|
|
|
|
2025-12-28 17:36:11 +01:00
|
|
|
// Optional: Cache it for performance
|
|
|
|
|
asynchronous: true
|
|
|
|
|
}
|
2025-12-23 20:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 📝 THE TEXT ON THE RIGHT
|
|
|
|
|
ColumnLayout {
|
2025-12-28 01:45:57 +01:00
|
|
|
id: textLayout
|
2025-12-23 20:21:18 +01:00
|
|
|
// Take up all remaining width
|
|
|
|
|
Layout.fillWidth: true
|
2025-12-28 17:36:11 +01:00
|
|
|
Layout.alignment: Qt.AlignVCenter // Center vertically
|
2025-12-23 20:21:18 +01:00
|
|
|
spacing: 2
|
|
|
|
|
|
|
|
|
|
Text {
|
2025-12-24 23:00:08 +01:00
|
|
|
text: notifyItem.modelData.summary
|
2025-12-23 20:21:18 +01:00
|
|
|
color: Colors.foreground
|
2025-12-28 17:36:11 +01:00
|
|
|
font.family: Settings.font
|
|
|
|
|
font.pixelSize: Settings.fontSize
|
2025-12-23 20:21:18 +01:00
|
|
|
font.bold: true
|
|
|
|
|
elide: Text.ElideRight
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
2025-12-24 23:00:08 +01:00
|
|
|
text: notifyItem.modelData.body
|
2025-12-23 20:21:18 +01:00
|
|
|
color: Colors.foreground
|
|
|
|
|
|
|
|
|
|
// Limit to 2 lines
|
2025-12-28 17:36:11 +01:00
|
|
|
font.family: Settings.font
|
|
|
|
|
font.pixelSize: Settings.fontSize - 2
|
|
|
|
|
maximumLineCount: 3
|
2025-12-28 01:45:57 +01:00
|
|
|
wrapMode: Text.WordWrap
|
2025-12-23 20:21:18 +01:00
|
|
|
elide: Text.ElideRight
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// (Your MouseArea for closing can still go here covering the whole thing)
|
|
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
2025-12-26 00:37:39 +01:00
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.fillHeight: true
|
2025-12-23 20:21:18 +01:00
|
|
|
acceptedButtons: Qt.LeftButton
|
2025-12-24 23:00:08 +01:00
|
|
|
onClicked: notifyItem.modelData.dismiss()
|
2025-12-23 20:21:18 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|