// The wallpaper grid. // // A thumbnail grid rather than a file path field: choosing a background is the // one setting where the value IS the picture, and typing a path to something // you cannot see is the worst possible version of it. // // Images are loaded asynchronously and at a fraction of their real size -- // several of the candidates here are 8-12 MB, and decoding them at full // resolution to draw a 150px tile would cost more memory than the rest of the // shell put together. import QtQuick import QtQuick.Effects import qs.config import qs.services Item { id: root implicitHeight: grid.implicitHeight // Sixty tiles is two screens of wallpaper on a page that also holds // typography, window geometry and effects -- everything below it becomes // unreachable without a long scroll past pictures. Two rows by default, // all of them on request. property bool expanded: false property string mode: DesktopPreferences.get("wallpaperMode") property string selectedOutput: Wallpaper.outputNames()[0] ?? "" property var activeByOutput: Wallpaper.activeByOutput property var slideshowPaths: DesktopPreferences.get("wallpaperSlideshowPaths") ?? [] property var assignments: DesktopPreferences.get("wallpaperPerMonitor") ?? ({}) property var setSingleAction: function(path) { Wallpaper.setSingle(path); } property var toggleSlideshowAction: function(path) { Wallpaper.toggleSlideshowPath(path); } property var setAssignmentAction: function(output, path) { Wallpaper.setAssignment(output, path); } readonly property int collapsedRows: 2 readonly property var shown: root.expanded ? Wallpaper.available : Wallpaper.available.slice(0, root.columns * root.collapsedRows) readonly property int hidden: Wallpaper.available.length - root.shown.length readonly property int columns: Math.max(2, Math.floor(width / 190)) readonly property real cellWidth: columns > 0 ? (width - (columns - 1) * 10) / columns : 160 function isCurrent(path: string): bool { return root.activeByOutput[root.selectedOutput] === path; } function selected(path: string): bool { if (root.mode === "slideshow") return root.slideshowPaths.includes(path); if (root.mode === "per-monitor") return root.assignments[root.selectedOutput] === path; return root.isCurrent(path); } function activate(path: string): void { if (root.mode === "slideshow") root.toggleSlideshowAction(path); else if (root.mode === "per-monitor") root.setAssignmentAction(root.selectedOutput, path); else root.setSingleAction(path); } Grid { id: grid width: parent.width columns: root.columns spacing: 10 Repeater { model: root.shown Rectangle { id: tile required property var modelData readonly property bool current: root.isCurrent(tile.modelData) readonly property bool member: root.mode === "slideshow" && root.selected(tile.modelData) width: root.cellWidth height: Math.round(root.cellWidth * 9 / 16) radius: Theme.cardRadius color: Theme.alpha(Theme.bgDark, 0.6) clip: true border.width: 0 Image { id: thumbnail anchors.fill: parent source: "file://" + tile.modelData fillMode: Image.PreserveAspectCrop asynchronous: true // Decode to roughly the size actually drawn. Without this a // grid of 12MB photographs decodes at full resolution. sourceSize.width: 400 sourceSize.height: 240 // Cached, because these are slow: the largest of these files // takes over two seconds to decode even at this size, and // without a cache scrolling back up pays that again for // every tile. What is held is the scaled thumbnail, not the // original, so the cost of keeping it is small. The tradeoff // is that a wallpaper replaced in place under the same name // shows its old thumbnail until the shell restarts -- worth // it for a directory of files that are added, not edited. cache: true // Several of these are 8-12MB originals, so a tile can sit // empty for a second or two. Fading in on ready makes that // read as loading rather than as a broken image, and the // fade runs once per tile rather than continuously. opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutQuad } } } Text { anchors.centerIn: parent visible: thumbnail.status === Image.Loading text: "Loading…" color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } Text { anchors.centerIn: parent width: parent.width - 20 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap visible: thumbnail.status === Image.Error text: "Could not read this image" color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } // The prism marks the active wallpaper, the same way it marks // the focused window and the selected sidebar entry. Rectangle { anchors.fill: parent radius: parent.radius visible: tile.current color: "transparent" border.width: 2 border.color: Theme.accent } Rectangle { anchors.top: parent.top anchors.right: parent.right anchors.margins: 8 width: 24 height: 24 radius: 12 visible: tile.member color: Theme.alpha(Theme.bgDark, 0.82) border.width: 1 border.color: Theme.alpha(Theme.fg, 0.16) Text { anchors.centerIn: parent text: "✓" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: 14 font.weight: Font.DemiBold } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 26 visible: tile.current || hover.hovered border.width: 0 gradient: Gradient { GradientStop { position: 0.0; color: Theme.alpha(Theme.bgDark, 0.0) } GradientStop { position: 1.0; color: Theme.alpha(Theme.bgDark, 0.88) } } Text { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 7 text: tile.current ? "Current wallpaper" : (tile.member ? "In slideshow" : Wallpaper.titleFor(tile.modelData)) color: tile.current ? Theme.accent : Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: tile.current ? Font.DemiBold : Font.Normal elide: Text.ElideRight } } HoverHandler { id: hover } TapHandler { onTapped: root.activate(tile.modelData) } } } } Text { anchors.centerIn: parent visible: Wallpaper.available.length === 0 && !Wallpaper.scanning width: parent.width - 40 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap text: "No images in " + Wallpaper.searchRoots[0] + ". Change the folder above, or put some pictures there." color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } }