aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/appearance/WallEngine.qml
blob: fe45f7b51e180997138821e21715148947de2901 (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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// 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.

pragma Singleton

import Quickshell
import QtQuick
import "../.."
import "Shuffle.js" as Shuffle

// The wallpaper engine: both wallpapers on a timer, from a list per screen
// picked with the heart in the picker. Ticks go through Wallpapers.run like
// Apply, so the burn plays, but as the light run: udt-accent leaves GRUB, the
// homepage and SDDM alone. Each screen has a home, the wallpaper last chosen
// for it by hand, and switching off goes back to it with a full run, so those
// three match the desktop again. See the spec,
// docs/superpowers/specs/2026-10-29-wallpaper-engine-design.md.
Singleton {
    id: root

    readonly property var cfg: Prefs.wallEngine
    readonly property bool on: cfg.on

    function list(which) { return which === "V" ? cfg.v : cfg.h; }
    function listed(path, which) { return list(which).indexOf(path) >= 0; }
    // A gallery folder by its Wallpapers.groups key, "" for the hearted list.
    function folder(which) { return which === "V" ? cfg.folderV : cfg.folderH; }
    function setFolder(which, key) { Prefs.setWallEngine(which === "V" ? { folderV: key } : { folderH: key }); }
    // What a tick draws from. A folder that is no longer in the gallery
    // gives nothing, so that screen is left alone rather than falling back
    // to the list behind the user's back.
    function pool(which) {
        const key = folder(which);
        if (key === "") return list(which).filter(present);
        const g = Wallpapers.groups.find(g => g.key === key);
        return g ? g.files : [];
    }
    // ponytail: present means in the gallery's last scan, so a file deleted
    // since stays drawable until a rescan, and wallp then skips it.
    function present(path) { return Wallpapers.files.indexOf(path) >= 0; }

    function toggle(path, which) {
        const l = list(which).slice();
        const i = l.indexOf(path);
        if (i >= 0) l.splice(i, 1);
        else l.push(path);
        Prefs.setWallEngine(which === "V" ? { v: l } : { h: l });
    }

    function setOn(v) {
        if (v === on) return;
        if (v) {
            Prefs.setWallEngine({ on: true, homeH: Wallpapers.currentH, homeV: Wallpapers.currentV });
        } else {
            Prefs.setWallEngine({ on: false });
            goHome();
        }
    }

    function setMinutes(m) {
        if (Number.isInteger(m) && m >= 1 && m !== cfg.minutes) Prefs.setWallEngine({ minutes: m });
    }

    // ponytail: in memory, so a restart starts a new round.
    property var bagH: []
    property var bagV: []

    // A screen whose list is empty, or whose pick is already up, is left out.
    function tick() {
        if (Wallpapers.applying) return;
        const h = Shuffle.draw(pool("H"), bagH, Wallpapers.currentH);
        const v = Shuffle.draw(pool("V"), bagV, Wallpapers.currentV);
        const ph = h.pick !== Wallpapers.currentH ? h.pick : "";
        const pv = v.pick !== Wallpapers.currentV ? v.pick : "";
        if (Wallpapers.run(ph, pv, "tick")) {
            bagH = h.bag;
            bagV = v.bag;
        }
    }

    function next() {
        tick();
        timer.restart();
    }

    // A switch-off during a run cannot start its own; it goes when that run
    // finishes.
    property bool homeWaiting: false
    function goHome() {
        const h = cfg.homeH !== Wallpapers.currentH ? cfg.homeH : "";
        const v = cfg.homeV !== Wallpapers.currentV ? cfg.homeV : "";
        if (h === "" && v === "") return;
        homeWaiting = !Wallpapers.run(h, v, "home");
    }

    Timer {
        id: timer
        interval: root.cfg.minutes * 60000
        repeat: true
        running: root.on
        onTriggered: root.tick()
    }

    Connections {
        target: Wallpapers
        // A pick by hand is the new home, and stays up a full interval.
        function onManualApplied(h, v) {
            if (!root.on) return;
            Prefs.setWallEngine({ homeH: h !== "" ? h : root.cfg.homeH,
                                  homeV: v !== "" ? v : root.cfg.homeV });
            timer.restart();
        }
        function onFinished() {
            if (!root.homeWaiting || root.on) return;
            root.homeWaiting = false;
            root.goHome();
        }
    }
}