Screenshots and recordings offered three folders to choose between, and three
guesses cannot include the folder somebody's other software already writes to --
which is the only folder that matters. This machine has had ~/Pictures/Screenshots
and ~/Videos/Screencasts since long before Panama, and Panama was writing
recordings to a Videos/Recordings it invented. Both are free text now, and the
recording default is the folder that was already there.
Wallpapers were swept from four directories at once, so the distribution's stock
images arrived mixed in with the user's own and there was no way to ask for just
one. Where wallpapers live is something somebody knows about their own machine.
It is a setting, not a search.
All three accept an absolute path as well as one relative to home, which meant
fixing Capture: it prefixed $HOME unconditionally, so naming /mnt/captures would
have written screenshots to ~/mnt/captures and left nobody able to find them.
The generator turned out to skip any entry whose comment sits inside the braces
rather than above them -- it looks for `key:` immediately after `{`. Three
settings were invisible in the reference because of it, one of them dockScreens,
which has never appeared there at all. The staleness contract could not see it
either: regenerating reproduced the same omission, so the copy was current and
incomplete at once. It now counts what was declared against what it could read
and refuses rather than quietly documenting less than exists.
Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
227 lines
9.0 KiB
QML
227 lines
9.0 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
|
|
// 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
|
|
}
|
|
}
|