Files
Panama/config/dot/quickshell/modules/settings/WallpaperPicker.qml

219 lines
8.4 KiB
QML

// 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
cache: false
// Decode to roughly the size actually drawn. Without this a
// grid of 12MB photographs decodes at full resolution.
sourceSize.width: 400
sourceSize.height: 240
// 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 found. Looked in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}