From 70bbc970c75221c902c515efd124c47fd702e3b7 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 17 Oct 2026 10:16:40 +0200 Subject: appearance moves into the settings window The appearance shell existed because a wallpaper picker needed more room than the 600px drawer, so it ran as its own process with a full-screen overlay. The settings window now gives it that room, and its five tabs become five settings pages in the desktop process: Wallpaper, Theme, Icons & cursor, Sunset and Idle. appearance/ is gone, and with it a process, a keepalive window and a layer blur rule. The files move to desktop/modules/appearance/ unchanged in what they do. Each tab gets a small *Module.qml; the window's refresh-on-open calls the singletons' refresh(), which the panel's show() used to. The two tabs that were inline Components in AppearancePanel become ThemeTab and WallpaperTab, owning the notices the panel printed. Sunset, Idle and Icons drop their own Flickable so the page scrolls them and search can reveal a row; the wallpaper list virtualises its rows and scrolls itself, so it takes the page's new viewHeight instead. Search finds a colour scheme and an icon or cursor theme by name. The appearance IPC target moves to desktop/shell.qml with the same verbs, so udt-steps and any keybind change only the path. Git now follows the appearance pages and autostart in the sidebar. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/appearance/WallpaperTab.qml | 357 ++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 desktop/modules/appearance/WallpaperTab.qml (limited to 'desktop/modules/appearance/WallpaperTab.qml') diff --git a/desktop/modules/appearance/WallpaperTab.qml b/desktop/modules/appearance/WallpaperTab.qml new file mode 100644 index 0000000..72d4dfe --- /dev/null +++ b/desktop/modules/appearance/WallpaperTab.qml @@ -0,0 +1,357 @@ +// Copyright (C) 2026 Danilo M. +// +// 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 QtQuick +import "../.." + +// Wallpapers by folder, staged per screen and set together with Apply, beside +// a mock of the two screens. Sized by its host: the thumbnail list scrolls +// itself, so it needs a height rather than the page's scroll. +Row { + id: tab + spacing: 20 + + // What the mock screens show. Hovering a thumbnail previews it on the + // targeted screen; the other keeps whatever is currently set. + property string hovering: "" + property string notice: "" + // Close the window: after a wallpaper is set, so the desk shows, and when + // wallhaven-dl opens. + signal dismiss + + Connections { + target: Wallpapers + function onApplied(ok, message) { + tab.notice = ok ? "" : `wallpaper failed: ${message}`; + if (ok) tab.dismiss(); + } + } + + Column { + width: parent.width - mockPane.width - parent.spacing + height: parent.height + spacing: 12 + + Text { + visible: tab.notice !== "" + width: parent.width + wrapMode: Text.Wrap + text: tab.notice + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.red + } + + Row { + spacing: 8 + Text { + anchors.verticalCenter: parent.verticalCenter + text: "Set on" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + Tab { + text: "Horizontal" + selected: Wallpapers.target === "H" + onClicked: Wallpapers.target = "H" + } + Tab { + text: "Vertical" + selected: Wallpapers.target === "V" + onClicked: Wallpapers.target = "V" + } + Text { + anchors.verticalCenter: parent.verticalCenter + text: Wallpapers.scanning ? "scanning…" + : Wallpapers.applying ? "applying…" + : Wallpapers.files.length + " wallpapers" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + // A folder is a header row followed, while open, by its + // thumbnails a row at a time, all in one flat ListView, so only + // the rows in view (and cacheBuffer around them) exist and + // decode: a large folder loads as it scrolls. Each row holds a + // group key and the index of its first file, start -1 for the + // header. A toggle inserts or removes just that folder's rows, + // so the rows around it, their decoded thumbnails and the + // scroll position all survive it. + ListView { + id: wallList + width: parent.width + height: parent.height - y + clip: true + readonly property int cols: Math.max(1, Math.floor(width / 300)) + readonly property real cell: Math.floor(width / cols) + readonly property real thumbHeight: cell * 9 / 16 + 26 + // The column is laid out after creation and reads negative + // until then, which the buffer refuses with a warning. + cacheBuffer: Math.max(0, thumbHeight * 2) + model: ListModel { id: rows } + + function group(key) { + return Wallpapers.groups.find(g => g.key === key) ?? { key: key, name: "", files: [] }; + } + function rowsOf(g) { + const out = []; + for (let i = 0; i < g.files.length; i += cols) out.push({ key: g.key, start: i }); + return out; + } + function rebuild() { + rows.clear(); + for (const g of Wallpapers.groups) { + rows.append({ key: g.key, start: -1 }); + if (Wallpapers.expanded[g.key]) rows.append(rowsOf(g)); + } + } + function toggle(index) { + const g = group(rows.get(index).key); + const body = rowsOf(g); + if (Wallpapers.expanded[g.key]) rows.remove(index + 1, body.length); + else body.forEach((r, i) => rows.insert(index + 1 + i, r)); + Wallpapers.toggle(g.key); + } + + // The width settles after creation, and a new column count + // regroups every row, so both rebuild. + Component.onCompleted: rebuild() + onColsChanged: rebuild() + Connections { + target: Wallpapers + function onGroupsChanged() { wallList.rebuild(); } + } + + delegate: Item { + id: row + required property int index + required property string key + required property int start + readonly property var group: wallList.group(key) + readonly property bool open: !!Wallpapers.expanded[key] + width: ListView.view.width + height: start < 0 ? 40 : wallList.thumbHeight + + Rectangle { + visible: row.start < 0 + anchors { fill: parent; topMargin: 6 } + radius: 8 + color: Qt.alpha(Theme.surface, headerHover.hovered ? 0.8 : 0.4) + + HoverHandler { id: headerHover } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: wallList.toggle(row.index) + } + + Text { + id: chevron + anchors { left: parent.left; leftMargin: 12; verticalCenter: parent.verticalCenter } + text: row.open ? "\uf077" : "\uf078" + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + } + Text { + anchors { left: chevron.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: row.group.name + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.text + } + Text { + anchors { right: parent.right; rightMargin: 12; verticalCenter: parent.verticalCenter } + text: row.group.files.length + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + Row { + visible: row.start >= 0 + + Repeater { + model: row.start < 0 ? [] : row.group.files.slice(row.start, row.start + wallList.cols) + + delegate: Item { + required property string modelData + width: wallList.cell + height: wallList.thumbHeight + + Rectangle { + readonly property bool staged: + Wallpapers.pendingH === modelData || Wallpapers.pendingV === modelData + + anchors.fill: parent + anchors.margins: 6 + radius: 8 + color: Qt.alpha(Theme.surface, thumbHover.hovered ? 0.8 : 0.3) + // Staged wins the border; otherwise it + // says how well the image fills the + // screen it would go to. + border.width: 2 + border.color: staged ? Theme.accent + : Wallpapers.qualityColor(modelData, Wallpapers.target) + clip: true + + HoverHandler { + id: thumbHover + // Hovering previews this image on the mock + // screen it would actually land on. + onHoveredChanged: + tab.hovering = hovered ? modelData + : (tab.hovering === modelData ? "" : tab.hovering) + } + MouseArea { + anchors.fill: parent + enabled: !Wallpapers.applying + cursorShape: Qt.PointingHandCursor + onClicked: Wallpapers.pick(modelData) + } + + Column { + anchors.fill: parent + anchors.margins: 6 + spacing: 4 + + Image { + width: parent.width + height: parent.height - 18 + source: "file://" + modelData + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop + // Decode at display size: full-resolution + // wallpapers would be megabytes each in memory. + sourceSize.width: wallList.cell + } + + Text { + width: parent.width + elide: Text.ElideMiddle + text: Wallpapers.label(modelData) + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + color: Theme.subtext + } + } + } + } + } + } + } + } + } + + // The two screens as they sit on the desk: DP-3 rotated upright on + // the left, DP-1 wide on the right, centred against each other the + // way Hyprland has them. Hovering a thumbnail fills the screen it + // would be set on; the other keeps what is on it now. + Item { + id: mockPane + // A third of the panel rather than a fixed width, so the + // preview keeps its share when the panel is resized. + width: Math.round(parent.width * 0.33) + height: parent.height + + // Divisor is the two panels side by side plus the gap; the + // bezels and stands are drawn outside that, hence the margin. + readonly property real scale: (width * 0.92) / (1080 + 2560 + 160) + + Column { + anchors.centerIn: parent + spacing: 14 + + Row { + spacing: Math.round(160 * mockPane.scale) + + MockScreen { + anchors.verticalCenter: parent.verticalCenter + label: "DP-3" + targeted: Wallpapers.target === "V" + source: (targeted && tab.hovering !== "") + ? tab.hovering : Wallpapers.shown("V") + // DP-3 has transform=1, so it stands upright. + panelWidth: Math.round(1080 * mockPane.scale) + panelHeight: Math.round(1920 * mockPane.scale) + } + + MockScreen { + anchors.verticalCenter: parent.verticalCenter + label: "DP-1" + targeted: Wallpapers.target === "H" + source: (targeted && tab.hovering !== "") + ? tab.hovering : Wallpapers.shown("H") + panelWidth: Math.round(2560 * mockPane.scale) + panelHeight: Math.round(1080 * mockPane.scale) + } + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + width: mockPane.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: tab.hovering !== "" ? Wallpapers.label(tab.hovering) + : Wallpapers.dirty ? "click Apply to set" + : "click a wallpaper to stage it" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + + // Why the image under consideration, hovered or else the + // one staged for the target screen, will look worse than + // its thumbnail. + Text { + readonly property string subject: tab.hovering !== "" ? tab.hovering + : Wallpapers.target === "V" ? Wallpapers.pendingV : Wallpapers.pendingH + anchors.horizontalCenter: parent.horizontalCenter + width: mockPane.width + visible: text !== "" + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: subject !== "" ? Wallpapers.qualityNote(subject, Wallpapers.target) : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Wallpapers.qualityColor(subject, Wallpapers.target) + } + + // wallhaven-dl searches wallhaven.cc and finds larger + // copies of low-res wallpapers. Launched through its + // desktop entry, which needs no ~/bin on quickshell's PATH + // and keeps the app's own taskbar icon. The panel closes + // so the app window is not opened under the overlay. + Tab { + anchors.horizontalCenter: parent.horizontalCenter + text: "Open wallhaven-dl" + onClicked: { + Quickshell.execDetached(["gtk-launch", "wallhaven-dl"]); + tab.dismiss(); + } + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 8 + + Tab { + text: Wallpapers.applying ? "applying…" : "Apply" + selected: Wallpapers.dirty && !Wallpapers.applying + enabled: Wallpapers.dirty && !Wallpapers.applying + onClicked: Wallpapers.apply() + } + Tab { + text: "Reset" + enabled: Wallpapers.dirty && !Wallpapers.applying + onClicked: Wallpapers.clearPending() + } + } + } + } +} -- cgit v1.2.3