CLI command changing wallpaper affects all screens, GUI only one?

Hi there,

a quick question: with 2 external monitors attached to my book, it seems if i choose to change the wallpaper per:

  • rightclick → Desktop and Wallpaper → Desktop Folder Settings

i have to change the wallpaper individually per screen, so in my case 3 times. do i maybe miss something?

so i was thinking about writing a script or something that maybe could integrated in the GUI there, and stumbled across this:

  • plasma-apply-wallpaperimage /path/to/image

and to my surprise, this changes the wallpaper to all attached screens at once. so, if the function is already there, again, maybe i’m missing something, but, wouldn’t that be worth to have a look together how this could be implemented in the GUI?

best regards to all of you, me (-:

it could be that GPT already found the relevant source:

and adjusted the code in a first step:

#include <QGuiApplication>
#include <QScreen>
#include <QJsonDocument>
#include <QJsonObject>
#include <Plasma/Containment>
#include <Plasma/Corona>
#include <Plasma/Wallpaper>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    
    if (argc < 2) {
        qWarning() << "Usage: plasma-apply-wallpaperimage <path-to-image>";
        return 1;
    }
    
    QString wallpaperPath = argv[1];
    
    // Zugriff auf Plasma-Shell
    Plasma::Corona *plasma = Plasma::Corona::instance();
    if (!plasma) {
        qWarning() << "Could not access Plasma shell";
        return 1;
    }
    
    // Über alle Bildschirme iterieren
    const auto screens = QGuiApplication::screens();
    for (QScreen *screen : screens) {
        for (Plasma::Containment *containment : plasma->containments()) {
            if (containment->screen() == screen->name()) { // Für alle Bildschirme
                QJsonObject config;
                config["Image"] = wallpaperPath;
                
                containment->setWallpaper("org.kde.image", QJsonDocument(config).toJson());
                qInfo() << "Wallpaper set for screen:" << screen->name();
            }
        }
    }
    
    return 0;
}

so, sadly GPT4 functionality is “empty” now, maybe i try grok or so. if i’m somewhere in the rright direction, then we’d “only” need to make this function (containment to all screens) bound to a checkbox “apply to all screens”…

edit:
it seems GPT3 can handle this as well. i’ll locally mirror the relevant GIT tonight (i don’t really know if i’m saying this correctly) to test if this could work - and if, i’ll let you know. This is, btw, what GPT3 says:

1. Add Checkbox to the GUI

QCheckBox *applyToAllCheckBox = new QCheckBox("Apply to all screens", this);
applyToAllCheckBox->setChecked(false); // Default is unchecked
// Add the checkbox to your layout
layout->addWidget(applyToAllCheckBox);

Step 2: Handle the Checkbox State

bool applyToAll = applyToAllCheckBox->isChecked();

if (applyToAll) {
    // Apply wallpaper to all screens
    for (const auto &screen : screens) {
        applyWallpaperToScreen(screen);
    }
} else {
    // Apply wallpaper to the current screen
    applyWallpaperToScreen(currentScreen);
}

Step 3: Modify the applyWallpaperToScreen Function

void applyWallpaperToScreen(const Screen &screen) {
    // Code to apply the wallpaper to a specific screen
    // Use KScreen or other relevant APIs to set wallpaper for a specific screen
}

Step 4: Integrate with KDE’s Wallpaper Configuration Logic

KScreen::applyWallpaperToAllScreens(const QString &wallpaperPath) {
    // Iterate through all connected screens and apply wallpaper
    for (auto screen : KScreen::getScreens()) {
        screen->applyWallpaper(wallpaperPath);
    }
}

it maybe a complete wrong attempt, if so, i’d be happy if you could let me know :slight_smile: but it’s fun and worth a try

In the commandline tool you don’t have a GUI so it can’t have a checkbox.

You would need something like a commandline option to specify the screen name(s) or screen index(es).

  1. GUI only changes wallpaper of one screen (i have 3)
  2. CLI command changes wallpaper of all screens (3)

Ergo: I want to add a checkbox to the GUI, to make the GUI change the wallpaper on all screens, not just one. (why optional? because smbdy may want to change wallpapers individual per screen )

:slight_smile:

(e.g. like this:)

Ah, my bad, I thought you wanted the CLI tool to behave similar to the GUI.

no problem - it’s the other way around. so two features would be great:

  1. Add the option to apply the selected wallpaper to alle xisting screens in the GUI
  2. Add a function to apply widescreen wallpapers to the GUI

But the scond one maybe more complicated, so i think the goal to modify the existing code in a way that it allows to set wallpapers to all screens would be great enough.

i thought an “inverted” checkbox may make sense, because i guess the most users only use one screen (?)

If there is only one screen then the checkbox doesn’t need to be shown at all :slight_smile:

great thought - although i came from a “make it as simple as possible” point of view, but, of course, the appearance of the checkbox could be bound to a kind of a “xrandr -are there other screens” condition. would that be easy in QML?

so, as a developer, how would you judge/estemate the needed efforts to have this improvement implemented?

as a not coder, i always hope that it may be just a few lines, and regarding the GPT output, could that be the right way, to “just” extend the “containment command” to all screens and put this as a condition to a checkbox?

Step 1. Add Checkbox to the GUI
Step 2: Handle the Checkbox State
Step 3: Modify the applyWallpaperToScreen Function
Step 4: Integrate with KDE’s Wallpaper Configuration Logic

Further questions coming to my mind as a amateur:

if i’d mirror the git to play around and try to let local LLMs help me, if i’d compile the testing changes then, would that replace my current “wallpaper management function?”

That should be fairly easy.

The C++ part of the screen could for example export the number of screens as a property or make it retrievable via a function.
The QML side can easily use those to decide on the visibility of elements.

I’ve only seen your snippets, not the actual code.
However those snippets make it look rather easy.

That is a good question.

My guess is that the System Settings app or the desktop itself call open these settings via a lookup and that could potentially be made to find one installed locally for the user before the one installed in the system.

Someone more familiar with Plasma and/or System Settings might know more details

1 Like

just to give you an actual state:

i may have found a way ( not i did, but grok3 did) to get the functionality by only modifying the QML source - but i couldn’t test it yet, since i have problems compiling.

i’ll post the modification here:

this is the source:

And here the modded source:

/*
    SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

import QtQuick 2.1
import QtQuick.Controls 2.15 as QtControls
import QtQuick.Layouts 1.0
import QtQuick.Dialogs
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kquickcontrols 2.0 as KQuickControls
import org.kde.plasma.plasma5support 2.0 as Plasma5Support

ColumnLayout {
    id: root

    // This attempts to follow the interface of the wallpaper configuration
    // in the wallpaper settings KCM for consistency
    //property int formAlignment: wallpaper.configuration.FormAlignment
    property alias cfg_Image: imageUrlField.text
    property alias cfg_FillMode: fillModeCombo.currentIndex
    property alias cfg_Color: backgroundColor.color
    property alias cfg_Slideshow: slideshowCheckBox.checked
    property alias cfg_SlideInterval: slideIntervalSpin.value
    property alias cfg_SlidePaths: slidePathsDialog.folders

    Kirigami.FormLayout {
        Layout.fillWidth: true

        QtControls.TextField {
            id: imageUrlField
            Kirigami.FormData.label: i18n("Image:")
            Layout.fillWidth: true
            //placeholderText: i18n("No image selected")
            onTextChanged: applyWallpaper() // Trigger on manual edit
        }

        RowLayout {
            QtControls.Button {
                text: i18n("Open...")
                icon.name: "document-open"
                onClicked: fileDialog.open()
            }
            QtControls.Button {
                text: i18n("Get New Wallpapers...")
                icon.name: "get-hot-new-stuff"
                visible: typeof Plasmoid !== "undefined"
                onClicked: {
                    Plasmoid.internalAction("get_new_wallpaper").trigger()
                }
            }
        }

        QtControls.ComboBox {
            id: fillModeCombo
            Kirigami.FormData.label: i18n("Positioning:")
            model: [
                i18n("Scaled and Cropped"),
                i18n("Scaled"),
                i18n("Scaled, Keep Proportions"),
                i18n("Centered"),
                i18n("Tiled")
            ]
        }

        KQuickControls.ColorButton {
            id: backgroundColor
            Kirigami.FormData.label: i18n("Background:")
            dialogTitle: i18n("Select Background Color")
        }

        QtControls.CheckBox {
            id: slideshowCheckBox
            Kirigami.FormData.label: i18n("Slideshow:")
        }

        QtControls.SpinBox {
            id: slideIntervalSpin
            Kirigami.FormData.label: i18n("Change every:")
            enabled: slideshowCheckBox.checked
            stepSize: 1
            from: 1
            to: 99999999
            value: 3600 // in seconds, display as minutes below
            // If you change the / 60 and * 60 below to another value,
            // make sure to change the tooltip text as well
            textFromValue: function(value) { return Math.round(value / 60) }
            valueFromText: function(text) { return parseInt(text) * 60 }
            QtControls.ToolTip {
                text: i18n("%1 minutes", Math.round(slideIntervalSpin.value / 60))
                visible: slideIntervalSpin.hovered
                delay: Kirigami.Units.toolTipDelay
            }
        }

        QtControls.CheckBox {
            id: applyToAllScreens
            Kirigami.FormData.label: i18n("Apply to:")
            text: i18n("All screens")
            checked: false
        }
    }

    FileDialog {
        id: fileDialog
        title: i18n("Select Image")
        // Seems Folder works better than the File option here?
        //folder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
        nameFilters: [i18n("Image Files (*.png *.jpg *.jpeg *.bmp *.svg *.svgz *.webp)")]
        onAccepted: {
            imageUrlField.text = fileDialog.fileUrl
            applyWallpaper() // Trigger on file selection
        }
    }

    // TODO: Is there a cleaner way to define this?
    // Attempting to mimic the FolderListModel setup used in the
    // Wallpapers KCM configuration, but that's not available in QML
    ListModel {
        id: slidePathsModel
    }

    // Dialog to allow selecting multiple folder URLs
    // Based on https://github.com/KDE/plasma-workspace/blob/master/wallpapers/image/imagepackage/contents/ui/UrlListDialog.qml
    Window {
        id: slidePathsDialog
        title: i18n("Slideshow Folders")
        width: Kirigami.Units.gridUnit * 20
        height: Kirigami.Units.gridUnit * 20
        modality: Qt.WindowModal
        flags: Qt.Dialog
        visible: false

        property alias folders: slidePathsModel

        onVisibleChanged: {
            if (visible) {
                slidePathsModel.clear()
                var paths = wallpaper.configuration.SlidePaths
                if (paths) {
                    paths.forEach(function (path) {
                        slidePathsModel.append({"fileUrl": path})
                    })
                }
            }
        }

        ColumnLayout {
            anchors.fill: parent
            anchors.margins: Kirigami.Units.smallSpacing

            ListView {
                id: slidePathsView
                Layout.fillWidth: true
                Layout.fillHeight: true
                clip: true
                model: slidePathsModel

                delegate: Kirigami.BasicListItem {
                    width: slidePathsView.width
                    label: model.fileUrl
                    icon: "folder"

                    onClicked: slidePathsView.currentIndex = index

                    QtControls.Button {
                        icon.name: "edit-delete"
                        onClicked: slidePathsModel.remove(index)
                    }
                }
            }

            RowLayout {
                QtControls.Button {
                    text: i18n("Add Folder...")
                    icon.name: "list-add"
                    onClicked: {
                        folderDialog.folder = ""
                        folderDialog.open()
                    }
                }

                QtControls.Button {
                    Layout.alignment: Qt.AlignRight
                    text: i18n("Close")
                    icon.name: "dialog-close"
                    onClicked: {
                        var paths = []
                        for (var i = 0; i < slidePathsModel.count; i++) {
                            paths.push(slidePathsModel.get(i).fileUrl)
                        }
                        wallpaper.configuration.SlidePaths = paths
                        slidePathsDialog.visible = false
                    }
                }
            }
        }
    }

    FileDialog {
        id: folderDialog
        title: i18n("Choose a Folder")
        selectFolder: true

        onAccepted: slidePathsModel.append({"fileUrl": folderDialog.fileUrl})
    }

    QtControls.Button {
        Layout.alignment: Qt.AlignRight
        text: i18n("Folders...")
        icon.name: "folder-open"
        visible: slideshowCheckBox.checked
        onClicked: slidePathsDialog.visible = true
    }

    Item {
        Layout.fillHeight: true
    }

    Plasma5Support.DataSource {
        id: executable
        engine: "executable"
        onNewData: disconnectSource(sourceName)
    }

    function applyWallpaper() {
        if (applyToAllScreens.checked) {
            let escapedImagePath = cfg_Image.replace(/'/g, "\\'").replace(/"/g, '\\"');
            let script = 'var allDesktops = desktops(); for (i in allDesktops) { allDesktops[i].wallpaperPlugin = "org.kde.image"; allDesktops[i].currentConfigGroup = ["Wallpaper", "org.kde.image", "General"]; allDesktops[i].writeConfig("Image", "' + escapedImagePath + '"); allDesktops[i].reloadConfig(); }';
            executable.connectSource("qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript '" + script + "'");
        } else {
            wallpaper.configuration.Image = cfg_Image;
        }
    }
}

And here the changes plus the idea behind them:

Here’s a brief explanation of each change and how it contributes to adding the “Apply to all screens” functionality to config.qml:

  • Line 15: Added import org.kde.plasma.plasma5support 2.0 as Plasma5Support

    • Purpose: Imports the Plasma5Support module, which provides the DataSource component. This is necessary to execute the DBus command that applies the wallpaper to all screens via plasmashell.
  • Line 73: Added onTextChanged: applyWallpaper() to imageUrlField

    • Purpose: Triggers the applyWallpaper() function whenever the user manually edits the image URL in the text field. This ensures the “all screens” logic runs immediately, maintaining the live-update behavior.
  • Line 134: Added QtControls.CheckBox { id: applyToAllScreens … }

    • Purpose: Adds the “Apply to all screens” checkbox to the UI. When checked, it signals that the wallpaper should be applied to all desktops instead of just the current one.
  • Line 172: Added applyWallpaper() call in fileDialog.onAccepted

    • Purpose: Calls applyWallpaper() when a file is selected via the “Open…” dialog. This ensures the “all screens” logic is applied right after picking an image, not just on manual text changes.
  • Line 233: Added Plasma5Support.DataSource { id: executable … }

    • Purpose: Defines a DataSource component to run external commands (specifically qdbus). It’s the mechanism that lets us send the JavaScript script to plasmashell to update all desktops.
  • Line 238-250: Added applyWallpaper() function with DBus logic

    • Purpose: Implements the core logic:

      • Checks if applyToAllScreens is checked.

      • If true, escapes the image path and builds a JavaScript script to set the wallpaper on all desktops using desktops(), then executes it via DBus.

      • If false, applies the wallpaper only to the current desktop (default behavior). This is the key to achieving your target of applying the wallpaper across all screens.

How They Work Together:
These changes add a checkbox to the GUI and tie it to a function that uses DBus to update all desktops when checked, while preserving the default single-screen behavior when unchecked. Each piece builds on the others to extend the wallpaper-setting process seamlessly.

Still a fun journey, maybe a coder see’s this and immideatly say it makes sense or do not at all. Hope to be able to get the posibility soon to test it by compiling…

best, me (-:

p.s. i am, as so often, not the first with this idea:

w o r k a r o u n d

after having fun with compiling and trying to directly work with just the .qml, which could have led me around the compiling, both wasn’t really succesfull. so i thought of a workaround, to add the functionality of plasma-apply-wallpaperimage to the right click context menu in dolphin. realizing, that’s already there :grinning:

This option sets the chosen picture to all screens, instead of just one (-:

1 Like