aboutsummaryrefslogtreecommitdiffstats
path: root/notifications/Balloons.qml
blob: 3676e67077ce80643b5b57bb757001dc297fd5db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

import Quickshell
import Quickshell.Wayland
import QtQuick

// The balloon stack, bottom-right of DP-1, over conky.
Scope {
    id: root

    // A tick drives both expiry and the end of a snooze without waiting for a
    // file change. 250ms keeps ronema's -t 1 near-instant.
    property double now: Date.now()
    Timer {
        interval: 250
        running: true
        repeat: true
        onTriggered: root.now = Date.now()
    }

    // The Repeater model is Notify.queue itself, so its identity only changes
    // when the daemon rewrites the file, not on the 250ms tick. Expiry and
    // snooze are the balloon's own business: the tick reassigns `now`, each
    // balloon re-evaluates its visible binding and leaves the column at
    // expiry. Rebuilding the model on the tick would recreate every delegate
    // (reloading icons, resetting hover) four times a second.
    readonly property var live: Notify.queue || []

    PanelWindow {
        id: win

        // From the queue, never from layout: a hidden window stops polishing,
        // and Column computes implicitHeight during polish, so deriving
        // visibility from it would never turn true again after the first
        // hide. Queue membership is a plain property the binding re-reads
        // whether or not the window is mapped, so a later notification
        // re-maps it. Expired entries that stay in the queue keep a
        // zero-height window mapped, which is harmless.
        visible: root.live.length > 0
        screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0]
        anchors { bottom: true; right: true }
        margins { bottom: 12; right: 12 }
        implicitWidth: 340
        implicitHeight: column.implicitHeight
        color: "transparent"
        exclusionMode: ExclusionMode.Ignore
        WlrLayershell.layer: WlrLayer.Overlay
        WlrLayershell.namespace: "quickshell-notifications"
        WlrLayershell.keyboardFocus: WlrKeyboardFocus.None

        Column {
            id: column
            width: parent.width
            anchors { bottom: parent.bottom; right: parent.right }
            spacing: 8

            Repeater {
                model: root.live
                NotificationBalloon { notification: modelData; now: root.now }
            }
        }
    }
}