Wallpaper rework

- removed swww to the code is easier to maintain
- basic multi monitor wallpaper support
This commit is contained in:
LemmyCook 2025-08-29 08:33:40 -04:00
parent 3cc8c8fb03
commit e79c163dd9
12 changed files with 330 additions and 552 deletions

View file

@ -94,10 +94,10 @@ Singleton {
// And not on every reload // And not on every reload
if (!isLoaded) { if (!isLoaded) {
Logger.log("Settings", "JSON completed loading") Logger.log("Settings", "JSON completed loading")
if (adapter.wallpaper.current !== "") { // if (adapter.wallpaper.current !== "") {
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) // Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) // WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
} // }
// Validate monitor configurations, only once // Validate monitor configurations, only once
// if none of the configured monitors exist, clear the lists // if none of the configured monitors exist, clear the lists
@ -171,22 +171,13 @@ Singleton {
// wallpaper // wallpaper
property JsonObject wallpaper: JsonObject { property JsonObject wallpaper: JsonObject {
property string directory: "/usr/share/wallpapers" property string directory: "/usr/share/wallpapers"
property string current: "" property bool enableMultiMonitorDirectories: false
property bool isRandom: false property bool setWallpaperOnAllMonitors: true
property int randomInterval: 300 property bool randomEnabled: false
property JsonObject swww property int randomIntervalSec: 300 // 5 min
property int transitionDuration: 1500 // 1500 ms
onDirectoryChanged: WallpaperService.listWallpapers() property string transitionType: "fade"
onIsRandomChanged: WallpaperService.toggleRandomWallpaper() property list<var> monitors: []
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
swww: JsonObject {
property bool enabled: false
property string resizeMethod: "crop"
property int transitionFps: 60
property string transitionType: "random"
property real transitionDuration: 1.1
}
} }
// applauncher // applauncher
@ -234,19 +225,10 @@ Singleton {
property string fontDefault: "Roboto" // Default font for all text property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
property list<var> monitorsScaling: []
// Legacy compatibility
property string fontFamily: fontDefault // Keep for backward compatibility
// Idle inhibitor state
property bool idleInhibitorEnabled: false property bool idleInhibitorEnabled: false
} }
// Scaling (not stored inside JsonObject, or it crashes)
property var monitorsScaling: {
}
// brightness // brightness
property JsonObject brightness: JsonObject { property JsonObject brightness: JsonObject {
property int brightnessStep: 5 property int brightnessStep: 5

View file

@ -5,26 +5,16 @@ import qs.Commons
import qs.Services import qs.Services
Loader { Loader {
active: !Settings.data.wallpaper.swww.enabled active: true
sourceComponent: Variants { sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
delegate: PanelWindow { delegate: PanelWindow {
required property ShellScreen modelData required property ShellScreen modelData
property string wallpaperSource: WallpaperService.currentWallpaper !== "" property string wallpaperSource: WallpaperService.getWallpaper(modelData.name)
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled visible: wallpaperSource !== ""
// Force update when SWWW setting changes
onVisibleChanged: {
if (visible) {
} else {
}
}
color: Color.transparent color: Color.transparent
screen: modelData screen: modelData
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background

View file

@ -20,10 +20,9 @@ Loader {
delegate: PanelWindow { delegate: PanelWindow {
required property ShellScreen modelData required property ShellScreen modelData
property string wallpaperSource: WallpaperService.currentWallpaper !== "" property string wallpaperSource: WallpaperService.getWallpaper(modelData.name)
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled visible: wallpaperSource !== ""
color: Color.transparent color: Color.transparent
screen: modelData screen: modelData
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background

View file

@ -188,7 +188,7 @@ ColumnLayout {
} }
NText { NText {
text: `${Math.round(ScalingService.scaleByName(modelData.name) * 100)}%` text: `${Math.round(ScalingService.getMonitorScale(modelData.name) * 100)}%`
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 50 * scaling Layout.minimumWidth: 50 * scaling
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
@ -204,12 +204,8 @@ ColumnLayout {
from: 0.7 from: 0.7
to: 1.8 to: 1.8
stepSize: 0.01 stepSize: 0.01
value: ScalingService.scaleByName(modelData.name) value: ScalingService.getMonitorScale(modelData.name)
onPressedChanged: { onPressedChanged: ScalingService.setMonitorScale(modelData.name, value)
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = value
Settings.data.monitorsScaling = data
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling Layout.minimumWidth: 150 * scaling
} }
@ -217,11 +213,7 @@ ColumnLayout {
NIconButton { NIconButton {
icon: "refresh" icon: "refresh"
tooltipText: "Reset Scaling" tooltipText: "Reset Scaling"
onClicked: { onClicked: ScalingService.setMonitorScale(modelData.name, 1.0)
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = 1.0
Settings.data.monitorsScaling = data
}
} }
} }
} }

View file

@ -29,7 +29,7 @@ ColumnLayout {
id: currentWallpaperImage id: currentWallpaperImage
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
imagePath: WallpaperService.currentWallpaper imagePath: WallpaperService.getWallpaper(screen.name)
fallbackIcon: "image" fallbackIcon: "image"
imageRadius: Style.radiusM * scaling imageRadius: Style.radiusM * scaling
} }
@ -62,14 +62,6 @@ ColumnLayout {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Layout.fillWidth: true Layout.fillWidth: true
} }
NText {
text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType
+ " transition." : "Wallpapers will change instantly."
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
visible: Settings.data.wallpaper.swww.enabled
}
} }
NIconButton { NIconButton {
@ -78,10 +70,17 @@ ColumnLayout {
onClicked: { onClicked: {
WallpaperService.listWallpapers() WallpaperService.listWallpapers()
} }
Layout.alignment: Qt.AlignTop | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
} }
} }
NToggle {
label: "Assign selection to all monitors"
description: "Set selected wallpaper on all monitors at once."
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
}
// Wallpaper grid container // Wallpaper grid container
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
@ -179,7 +178,11 @@ ColumnLayout {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
WallpaperService.changeWallpaper(wallpaperPath) if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(undefined, wallpaperPath)
} else {
WallpaperService.changeWallpaper(screen.name, wallpaperPath)
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Services import qs.Services
@ -9,36 +10,59 @@ import qs.Widgets
ColumnLayout { ColumnLayout {
id: root id: root
// Process to check if swww is installed ColumnLayout {
Process { spacing: Style.marginL * scaling
id: swwwCheck Layout.fillWidth: true
command: ["which", "swww"] NTextInput {
running: false label: "Wallpaper Directory"
description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
}
onExited: function (exitCode) { // Monitor-specific directories
if (exitCode === 0) { NToggle {
// SWWW exists, enable it label: "Monitor-specific directories"
Settings.data.wallpaper.swww.enabled = true description: "Enable multi-monitor wallpaper directory management."
WallpaperService.startSWWWDaemon() checked: Settings.data.wallpaper.enableMultiMonitorDirectories
ToastService.showNotice("Swww", "Enabled") onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
} else { }
// SWWW not found
ToastService.showWarning("Swww", "Not installed") ColumnLayout {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
spacing: Style.marginL * scaling
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: 550 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginXXS * scaling
NTextInput {
label: (modelData.name || "Unknown")
description: `Path to your wallpaper directory for "${(modelData.name || "Unknown")}" monitor`
text: WallpaperService.getMonitorWallpaperDirectory(modelData.name)
labelColor: Color.mSecondary
onEditingFinished: WallpaperService.setMonitorWallpaperDirectory(modelData.name, text)
Layout.maximumWidth: 420 * scaling
}
}
}
} }
} }
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NTextInput {
label: "Wallpaper Directory"
description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
} }
NDivider { NDivider {
@ -62,10 +86,42 @@ ColumnLayout {
NToggle { NToggle {
label: "Random Wallpaper" label: "Random Wallpaper"
description: "Automatically select random wallpapers from the folder." description: "Automatically select random wallpapers from the folder."
checked: Settings.data.wallpaper.isRandom checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => { onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
Settings.data.wallpaper.isRandom = checked }
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
onSelected: key => Settings.data.wallpaper.transitionType = key
}
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 100
to: 10000
stepSize: 100
value: Settings.data.wallpaper.transitionDuration
onMoved: Settings.data.wallpaper.transitionDuration = value
cutoutColor: Color.mSurface
}
NText {
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
} }
// Interval (slider + H:M inputs) // Interval (slider + H:M inputs)
@ -79,25 +135,37 @@ ColumnLayout {
NText { NText {
// Show friendly H:MM format from current settings // Show friendly H:MM format from current settings
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval) text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight Layout.alignment: Qt.AlignBottom | Qt.AlignRight
} }
} }
// Preset chips // Preset chips using Repeater
RowLayout { RowLayout {
id: presetRow id: presetRow
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Preset seconds list // Factorized presets data
property var presets: [15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60] property var intervalPresets: [
5 * 60,
10 * 60,
15 * 60,
30 * 60,
45 * 60,
60 * 60,
90 * 60,
120 * 60,
]
// Whether current interval equals one of the presets // Whether current interval equals one of the presets
property bool isCurrentPreset: presets.indexOf(Settings.data.wallpaper.randomInterval) !== -1 property bool isCurrentPreset: {
return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec)
}
// Allow user to force open the custom input; otherwise it's auto-open when not a preset // Allow user to force open the custom input; otherwise it's auto-open when not a preset
property bool customForcedVisible: false property bool customForcedVisible: false
function setIntervalSeconds(sec) { function setIntervalSeconds(sec) {
Settings.data.wallpaper.randomInterval = sec Settings.data.wallpaper.randomIntervalSec = sec
WallpaperService.restartRandomWallpaperTimer() WallpaperService.restartRandomWallpaperTimer()
// Hide custom when selecting a preset // Hide custom when selecting a preset
customForcedVisible = false customForcedVisible = false
@ -105,168 +173,25 @@ ColumnLayout {
// Helper to color selected chip // Helper to color selected chip
function isSelected(sec) { function isSelected(sec) {
return Settings.data.wallpaper.randomInterval === sec return Settings.data.wallpaper.randomIntervalSec === sec
} }
// 15m // Repeater for preset chips
Rectangle { Repeater {
radius: height * 0.5 model: presetRow.intervalPresets
color: presetRow.isSelected(15 * 60) ? Color.mPrimary : Color.mSurfaceVariant delegate: IntervalPresetChip {
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) seconds: modelData
implicitWidth: label15.implicitWidth + Style.marginM * 1.5 * scaling label: Time.formatVagueHumanReadableDuration(modelData)
border.width: 1 selected: presetRow.isSelected(modelData)
border.color: presetRow.isSelected(15 * 60) ? Color.transparent : Color.mOutline onClicked: presetRow.setIntervalSeconds(modelData)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(15 * 60)
}
NText {
id: label15
anchors.centerIn: parent
text: "15m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(15 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 30m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(30 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label30.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(30 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(30 * 60)
}
NText {
id: label30
anchors.centerIn: parent
text: "30m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(30 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 45m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(45 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label45.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(45 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(45 * 60)
}
NText {
id: label45
anchors.centerIn: parent
text: "45m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(45 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 1h
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(60 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label1h.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(60 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(60 * 60)
}
NText {
id: label1h
anchors.centerIn: parent
text: "1h"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(60 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 1h 30m
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(90 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label90.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(90 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(90 * 60)
}
NText {
id: label90
anchors.centerIn: parent
text: "1h 30m"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(90 * 60) ? Color.mOnPrimary : Color.mOnSurface
}
}
// 2h
Rectangle {
radius: height * 0.5
color: presetRow.isSelected(120 * 60) ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: label2h.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: presetRow.isSelected(120 * 60) ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.setIntervalSeconds(120 * 60)
}
NText {
id: label2h
anchors.centerIn: parent
text: "2h"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: presetRow.isSelected(120 * 60) ? Color.mOnPrimary : Color.mOnSurface
} }
} }
// Custom opens inline input // Custom opens inline input
Rectangle { IntervalPresetChip {
radius: height * 0.5 label: customRow.visible ? "Custom" : "Custom…"
color: customRow.visible ? Color.mPrimary : Color.mSurfaceVariant selected: customRow.visible
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
implicitWidth: labelCustom.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: customRow.visible ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
}
NText {
id: labelCustom
anchors.centerIn: parent
text: customRow.visible ? "Custom" : "Custom…"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
color: customRow.visible ? Color.mOnPrimary : Color.mOnSurface
}
} }
} }
@ -282,12 +207,11 @@ ColumnLayout {
description: "Enter time as HH:MM (e.g., 01:30)." description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling inputMaxWidth: 100 * scaling
text: { text: {
const s = Settings.data.wallpaper.randomInterval const s = Settings.data.wallpaper.randomIntervalSec
const h = Math.floor(s / 3600) const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60) const m = Math.floor((s % 3600) / 60)
return h + ":" + (m < 10 ? ("0" + m) : m) return h + ":" + (m < 10 ? ("0" + m) : m)
} }
onEditingFinished: { onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
if (m) { if (m) {
@ -297,7 +221,7 @@ ColumnLayout {
return return
h = Math.max(0, Math.min(24, h)) h = Math.max(0, Math.min(24, h))
min = Math.max(0, Math.min(59, min)) min = Math.max(0, Math.min(59, min))
Settings.data.wallpaper.randomInterval = (h * 3600) + (min * 60) Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60)
WallpaperService.restartRandomWallpaperTimer() WallpaperService.restartRandomWallpaperTimer()
// Keep custom visible after manual entry // Keep custom visible after manual entry
presetRow.customForcedVisible = true presetRow.customForcedVisible = true
@ -308,193 +232,32 @@ ColumnLayout {
} }
} }
NDivider { // Reusable component for interval preset chips
Layout.fillWidth: true component IntervalPresetChip: Rectangle {
Layout.topMargin: Style.marginXL * scaling property int seconds: 0
Layout.bottomMargin: Style.marginXL * scaling property string label: ""
} property bool selected: false
signal clicked()
// ------------------------------- radius: height * 0.5
// Swww color: selected ? Color.mPrimary : Color.mSurfaceVariant
ColumnLayout { implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
spacing: Style.marginL * scaling implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5 * scaling
Layout.fillWidth: true border.width: 1
border.color: selected ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.clicked()
}
NText { NText {
text: "Swww" id: chipLabel
font.pointSize: Style.fontSizeXXL * scaling anchors.centerIn: parent
font.weight: Style.fontWeightBold text: parent.label
color: Color.mSecondary font.pointSize: Style.fontSizeS * scaling
} color: parent.selected ? Color.mOnPrimary : Color.mOnSurface
// Use SWWW
NToggle {
label: "Use Swww"
description: "Use Swww daemon for advanced wallpaper management."
checked: Settings.data.wallpaper.swww.enabled
onToggled: checked => {
if (checked) {
// Check if swww is installed
swwwCheck.running = true
} else {
Settings.data.wallpaper.swww.enabled = false
ToastService.showNotice("Swww", "Disabled")
}
}
}
// SWWW Settings (only visible when useSWWW is enabled)
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
visible: Settings.data.wallpaper.swww.enabled
// Resize Mode
NComboBox {
label: "Resize Mode"
description: "How Swww should resize wallpapers to fit the screen."
model: ListModel {
ListElement {
key: "no"
name: "No"
}
ListElement {
key: "crop"
name: "Crop"
}
ListElement {
key: "fit"
name: "Fit"
}
ListElement {
key: "stretch"
name: "Stretch"
}
}
currentKey: Settings.data.wallpaper.swww.resizeMethod
onSelected: key => {
Settings.data.wallpaper.swww.resizeMethod = key
}
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "simple"
name: "Simple"
}
ListElement {
key: "fade"
name: "Fade"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "wipe"
name: "Wipe"
}
ListElement {
key: "wave"
name: "Wave"
}
ListElement {
key: "grow"
name: "Grow"
}
ListElement {
key: "center"
name: "Center"
}
ListElement {
key: "any"
name: "Any"
}
ListElement {
key: "outer"
name: "Outer"
}
ListElement {
key: "random"
name: "Random"
}
}
currentKey: Settings.data.wallpaper.swww.transitionType
onSelected: key => {
Settings.data.wallpaper.swww.transitionType = key
}
}
// Transition FPS
ColumnLayout {
NLabel {
label: "Transition FPS"
description: "Frames per second for transition animations."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 30
to: 500
stepSize: 5
value: Settings.data.wallpaper.swww.transitionFps
onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value)
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.wallpaper.swww.transitionFps + " FPS"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 0.25
to: 10
stepSize: 0.05
value: Settings.data.wallpaper.swww.transitionDuration
onMoved: Settings.data.wallpaper.swww.transitionDuration = value
cutoutColor: Color.mSurface
}
NText {
text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + "s"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
} }
} }
@ -503,4 +266,4 @@ ColumnLayout {
Layout.topMargin: Style.marginXL * scaling Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling Layout.bottomMargin: Style.marginXL * scaling
} }
} }

View file

@ -75,7 +75,6 @@ Features a modern modular architecture with a status bar, notification system, c
### Optional ### Optional
- `cliphist` - For clipboard history support - `cliphist` - For clipboard history support
- `swww` - Wallpaper animations and effects
- `matugen` - Material You color scheme generation - `matugen` - Material You color scheme generation
- `cava` - Audio visualizer component - `cava` - Audio visualizer component
- `wlsunset` - To be able to use NightLight - `wlsunset` - To be able to use NightLight
@ -270,14 +269,6 @@ The launcher supports special commands for enhanced functionality:
## Advanced Configuration ## Advanced Configuration
### Niri Configuration
Add this to your `layout` section for proper swww integration:
```
background-color "transparent"
```
### Recommended Compositor Settings ### Recommended Compositor Settings
For Niri: For Niri:
@ -288,11 +279,6 @@ window-rule {
clip-to-geometry true clip-to-geometry true
} }
layer-rule {
match namespace="^swww-daemon$"
place-within-backdrop true
}
layer-rule { layer-rule {
match namespace="^quickshell-wallpaper$" match namespace="^quickshell-wallpaper$"
} }

View file

@ -11,7 +11,7 @@ Singleton {
function scale(aScreen) { function scale(aScreen) {
try { try {
if (aScreen !== undefined && aScreen.name !== undefined) { if (aScreen !== undefined && aScreen.name !== undefined) {
return scaleByName(aScreen.name) return getMonitorScale(aScreen.name)
} }
} catch (e) { } catch (e) {
@ -20,21 +20,46 @@ Singleton {
return 1.0 return 1.0
} }
function scaleByName(aScreenName) { // -------------------------------------------
function getMonitorScale(aScreenName) {
try { try {
if (Settings.data.monitorsScaling !== undefined) { var monitors = Settings.data.ui.monitorsScaling
if (Settings.data.monitorsScaling[aScreenName] !== undefined) { if (monitors !== undefined) {
return Settings.data.monitorsScaling[aScreenName] for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === aScreenName) {
return monitors[i].scale
}
} }
} }
} catch (e) { } catch (e) {
//Logger.warn(e) //Logger.warn(e)
} }
return 1.0 return 1.0
} }
// -------------------------------------------
function setMonitorScale(aScreenName, scale) {
try {
var monitors = Settings.data.ui.monitorsScaling
if (monitors !== undefined) {
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === aScreenName) {
monitors[i].scale = scale
return
}
}
}
monitors.push({
"name": aScreenName,
"scale": scale
})
} catch (e) {
//Logger.warn(e)
}
}
// ------------------------------------------- // -------------------------------------------
// Dynamic scaling based on resolution // Dynamic scaling based on resolution

View file

@ -10,66 +10,138 @@ Singleton {
id: root id: root
Component.onCompleted: { Component.onCompleted: {
Logger.log("Wallpapers", "Service started") Logger.log("Wallpaper", "Service started")
listWallpapers() listWallpapers()
// Wallpaper is set when the settings are loaded. // Wallpaper is set when the settings are loaded.
// Don't start random wallpaper during initialization // Don't start random wallpaper during initialization
} }
readonly property ListModel transitionsModel: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "fade"
name: "Fade"
}
}
property var wallpaperList: [] property var wallpaperList: []
property string currentWallpaper: Settings.data.wallpaper.current
property bool scanning: false property bool scanning: false
// SWWW Connections {
property string transitionType: Settings.data.wallpaper.swww.transitionType target: Settings.data.wallpaper
property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] onDirectoryChanged: WallpaperService.listWallpapers()
onRandomEnabledChanged: WallpaperService.toggleRandomWallpaper()
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
}
// -------------------------------------------------------------------
function geMonitorDefinition(screenName) {
var monitors = Settings.data.wallpaper.monitors
if (monitors !== undefined) {
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === screenName) {
return monitors[i]
}
}
}
}
// -------------------------------------------------------------------
function getMonitorWallpaperDirectory(screenName) {
var monitor = geMonitorDefinition(screenName)
if (monitor !== undefined) {
return monitor.directory
}
return Settings.data.wallpaper.directory
}
// -------------------------------------------------------------------
function setMonitorWallpaperDirectory(screenName, directory) {
var monitor = geMonitorDefinition(screenName)
if (monitor !== undefined) {
monitor.directory = directory
return
}
Settings.data.wallpaper.monitors.push({
"name": screenName,
"directory": directory,
"wallpaper": ""
})
}
// -------------------------------------------------------------------
function listWallpapers() { function listWallpapers() {
Logger.log("Wallpapers", "Listing wallpapers") Logger.log("Wallpaper", "Listing wallpapers")
scanning = true scanning = true
wallpaperList = [] wallpaperList = []
// Set the folder directly to avoid model reset issues // Set the folder directly to avoid model reset issues
folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "")
} }
function changeWallpaper(path) { // -------------------------------------------------------------------
Logger.log("Wallpapers", "Changing to:", path) function getWallpaper(screenName) {
setCurrentWallpaper(path, false) // Logger.log("Wallpaper", "getWallpaper on", screenName)
var monitor = geMonitorDefinition(screenName)
if (monitor !== undefined) {
return monitor["wallpaper"]
}
return ""
} }
function setCurrentWallpaper(path, isInitial) { // -------------------------------------------------------------------
// Only regenerate colors if the wallpaper actually changed function changeWallpaper(screenName, path) {
var wallpaperChanged = currentWallpaper !== path if (screenName !== undefined) {
setCurrentWallpaper(screenName, path, false)
currentWallpaper = path
if (!isInitial) {
Settings.data.wallpaper.current = path
}
if (Settings.data.wallpaper.swww.enabled) {
if (Settings.data.wallpaper.swww.transitionType === "random") {
transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)]
} else {
transitionType = Settings.data.wallpaper.swww.transitionType
}
changeWallpaperProcess.running = true
} else { } else {
for (var i = 0; i < Quickshell.screens.length; i++) {
setCurrentWallpaper(Quickshell.screens[i].name, path, false)
}
}
}
// Fallback: update the settings directly for non-SWWW mode // -------------------------------------------------------------------
//Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") function setCurrentWallpaper(screenName, path, isInitial) {
if (screenName === undefined) {
Logger.warn("Wallpaper", "setCurrentWallpaper", "no screen specified")
return
} }
Logger.log("Wallpaper", "setCurrentWallpaper on", screenName, ": ", path)
var monitor = geMonitorDefinition(screenName)
if (monitor !== undefined) {
monitor["wallpaper"] = path
} else {
Settings.data.wallpaper.monitors.push({
"name": screenName,
"directory": Settings.data.wallpaper.directory,
"wallpaper": path
})
}
// // Only regenerate colors if the wallpaper actually changed
// var wallpaperChanged = currentWallpaper !== path
// currentWallpaper = path
// if (!isInitial) {
// Settings.data.wallpaper.current = path
// }
if (randomWallpaperTimer.running) { if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart() randomWallpaperTimer.restart()
} }
// Only notify ColorScheme service if the wallpaper actually changed // Only notify ColorScheme service if the wallpaper actually changed
if (wallpaperChanged) { // if (wallpaperChanged) {
ColorSchemeService.changedWallpaper() // ColorSchemeService.changedWallpaper()
} // }
} }
// -------------------------------------------------------------------
function setRandomWallpaper() { function setRandomWallpaper() {
var randomIndex = Math.floor(Math.random() * wallpaperList.length) var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex] var randomPath = wallpaperList[randomIndex]
@ -79,6 +151,7 @@ Singleton {
setCurrentWallpaper(randomPath, false) setCurrentWallpaper(randomPath, false)
} }
// -------------------------------------------------------------------
function toggleRandomWallpaper() { function toggleRandomWallpaper() {
if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) {
randomWallpaperTimer.start() randomWallpaperTimer.start()
@ -88,6 +161,7 @@ Singleton {
} }
} }
// -------------------------------------------------------------------
function restartRandomWallpaperTimer() { function restartRandomWallpaperTimer() {
if (Settings.data.wallpaper.isRandom) { if (Settings.data.wallpaper.isRandom) {
randomWallpaperTimer.stop() randomWallpaperTimer.stop()
@ -95,16 +169,12 @@ Singleton {
} }
} }
function startSWWWDaemon() { // -------------------------------------------------------------------
if (Settings.data.wallpaper.swww.enabled) { // -------------------------------------------------------------------
Logger.log("Swww", "Requesting swww-daemon") // -------------------------------------------------------------------
startDaemonProcess.running = true
}
}
Timer { Timer {
id: randomWallpaperTimer id: randomWallpaperTimer
interval: Settings.data.wallpaper.randomInterval * 1000 interval: Settings.data.wallpaper.randomIntervalSec * 1000
running: false running: false
repeat: true repeat: true
onTriggered: setRandomWallpaper() onTriggered: setRandomWallpaper()
@ -113,7 +183,6 @@ Singleton {
FolderListModel { FolderListModel {
id: folderModel id: folderModel
// Swww supports many images format but Quickshell only support a subset of those.
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
showDirs: false showDirs: false
sortField: FolderListModel.Name sortField: FolderListModel.Name
@ -127,46 +196,7 @@ Singleton {
} }
wallpaperList = files wallpaperList = files
scanning = false scanning = false
Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length) Logger.log("Wallpaper", "List refreshed, count:", wallpaperList.length)
}
}
}
Process {
id: changeWallpaperProcess
command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString(
), currentWallpaper]
running: false
onStarted: {
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus)
if (exitCode !== 0) {
Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon")
Logger.log("Swww", "You can start it with: swww-daemon --format xrgb")
}
}
}
Process {
id: startDaemonProcess
command: ["swww-daemon", "--format", "xrgb"]
running: false
onStarted: {
Logger.log("Swww", "Daemon start process initiated")
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Daemon start process finished with exit code:", exitCode)
if (exitCode === 0) {
Logger.log("Swww", "Daemon started successfully")
} else {
Logger.log("Swww", "Failed to start daemon, may already be running")
} }
} }
} }

View file

@ -3,8 +3,12 @@ import QtQuick.Layouts
import qs.Commons import qs.Commons
ColumnLayout { ColumnLayout {
id: root
property string label: "" property string label: ""
property string description: "" property string description: ""
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
@ -13,14 +17,14 @@ ColumnLayout {
text: label text: label
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnSurface color: labelColor
visible: label !== "" visible: label !== ""
} }
NText { NText {
text: description text: description
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant color: descriptionColor
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
visible: description !== "" visible: description !== ""
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -12,6 +12,8 @@ ColumnLayout {
property bool readOnly: false property bool readOnly: false
property bool enabled: true property bool enabled: true
property int inputMaxWidth: 420 * scaling property int inputMaxWidth: 420 * scaling
property color labelColor: Color.mOnSurface
property color descriptionColor: Color.mOnSurfaceVariant
property alias text: input.text property alias text: input.text
property alias placeholderText: input.placeholderText property alias placeholderText: input.placeholderText
@ -25,6 +27,8 @@ ColumnLayout {
NLabel { NLabel {
label: root.label label: root.label
description: root.description description: root.description
labelColor: root.labelColor
descriptionColor: root.descriptionColor
visible: root.label !== "" || root.description !== "" visible: root.label !== "" || root.description !== ""
} }

View file

@ -35,7 +35,7 @@ Item {
} }
} }
} }
Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name) //Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name)
} }
} }