kcm.cpp 25.6 KB
Newer Older
Marco Martin's avatar
Marco Martin committed
1
2
3
/* This file is part of the KDE Project
   Copyright (c) 2014 Marco Martin <mart@kde.org>
   Copyright (c) 2014 Vishesh Handa <me@vhanda.in>
4
   Copyright (c) 2019 Cyril Rossi <cyril.rossi@enioka.com>
Marco Martin's avatar
Marco Martin committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License version 2 as published by the Free Software Foundation.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "kcm.h"
#include "../krdb/krdb.h"
#include "config-kcm.h"
24
#include "config-workspace.h"
Marco Martin's avatar
Marco Martin committed
25
26
27
28
29
#include <klauncher_iface.h>

#include <KAboutData>
#include <KSharedConfig>
#include <KGlobalSettings>
Marco Martin's avatar
Marco Martin committed
30
#include <KIconLoader>
31
32
#include <KAutostart>
#include <KRun>
33
#include <KService>
Marco Martin's avatar
Marco Martin committed
34

35
#include <QDebug>
36
37
#include <QQuickItem>
#include <QQuickWindow>
38
39
#include <QStandardPaths>
#include <QProcess>
Marco Martin's avatar
Marco Martin committed
40
41
#include <QStandardItemModel>
#include <QX11Info>
42
43
#include <QStyle>
#include <QStyleFactory>
Marco Martin's avatar
Marco Martin committed
44
45

#include <KLocalizedString>
46
#include <KPackage/PackageLoader>
Marco Martin's avatar
Marco Martin committed
47
48
49

#include <X11/Xlib.h>

50
51
#include "lookandfeelsettings.h"

52
53
54
55
56
#ifdef HAVE_XCURSOR
#   include "../cursortheme/xcursor/xcursortheme.h"
#   include <X11/Xcursor/Xcursor.h>
#endif

Marco Martin's avatar
Marco Martin committed
57
58
59
60
#ifdef HAVE_XFIXES
#  include <X11/extensions/Xfixes.h>
#endif

61
62
63
KCMLookandFeel::KCMLookandFeel(QObject *parent, const QVariantList &args)
    : KQuickAddons::ManagedConfigModule(parent, args)
    , m_settings(new LookAndFeelSettings(this))
64
    , m_config(QStringLiteral("kdeglobals"))
Marco Martin's avatar
Marco Martin committed
65
66
67
68
69
70
    , m_configGroup(m_config.group("KDE"))
    , m_applyColors(true)
    , m_applyWidgetStyle(true)
    , m_applyIcons(true)
    , m_applyPlasmaTheme(true)
    , m_applyCursors(true)
71
    , m_applyWindowSwitcher(true)
72
    , m_applyDesktopSwitcher(true)
73
    , m_resetDefaultLayout(false)
74
    , m_applyWindowDecoration(true)
Marco Martin's avatar
Marco Martin committed
75
{
76
    qmlRegisterType<LookAndFeelSettings>();
Marco Martin's avatar
Marco Martin committed
77
    qmlRegisterType<QStandardItemModel>();
Marco Martin's avatar
Marco Martin committed
78
    qmlRegisterType<KCMLookandFeel>();
79
80

    KAboutData *about = new KAboutData(QStringLiteral("kcm_lookandfeel"), i18n("Global Theme"),
81
82
                                       QStringLiteral("0.1"), QString(), KAboutLicense::LGPL);
    about->addAuthor(i18n("Marco Martin"), QString(), QStringLiteral("mart@kde.org"));
Marco Martin's avatar
Marco Martin committed
83
    setAboutData(about);
84
    setButtons(Apply | Default);
Marco Martin's avatar
Marco Martin committed
85
86
87
88

    m_model = new QStandardItemModel(this);
    QHash<int, QByteArray> roles = m_model->roleNames();
    roles[PluginNameRole] = "pluginName";
89
    roles[DescriptionRole] = "description";
90
    roles[ScreenshotRole] = "screenshot";
91
    roles[FullScreenPreviewRole] = "fullScreenPreview";
Marco Martin's avatar
Marco Martin committed
92
93
94
95
96
97
98
99
100
101
    roles[HasSplashRole] = "hasSplash";
    roles[HasLockScreenRole] = "hasLockScreen";
    roles[HasRunCommandRole] = "hasRunCommand";
    roles[HasLogoutRole] = "hasLogout";

    roles[HasColorsRole] = "hasColors";
    roles[HasWidgetStyleRole] = "hasWidgetStyle";
    roles[HasIconsRole] = "hasIcons";
    roles[HasPlasmaThemeRole] = "hasPlasmaTheme";
    roles[HasCursorsRole] = "hasCursors";
102
    roles[HasWindowSwitcherRole] = "hasWindowSwitcher";
103
    roles[HasDesktopSwitcherRole] = "hasDesktopSwitcher";
Marco Martin's avatar
Marco Martin committed
104
    m_model->setItemRoleNames(roles);
105
    loadModel();
Marco Martin's avatar
Marco Martin committed
106
107
}

Marco Martin's avatar
Marco Martin committed
108
109
110
111
KCMLookandFeel::~KCMLookandFeel()
{
}

112
void KCMLookandFeel::reloadModel()
Marco Martin's avatar
Marco Martin committed
113
{
114
    loadModel();
Marco Martin's avatar
Marco Martin committed
115
}
Marco Martin's avatar
Marco Martin committed
116

117
QStandardItemModel *KCMLookandFeel::lookAndFeelModel() const
Marco Martin's avatar
Marco Martin committed
118
119
120
121
{
    return m_model;
}

122
int KCMLookandFeel::pluginIndex(const QString &pluginName) const
Marco Martin's avatar
Marco Martin committed
123
{
124
125
126
    const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, pluginName);
    if (results.count() == 1) {
        return results.first().row();
127
    }
Marco Martin's avatar
Marco Martin committed
128

129
130
131
    return -1;
}

132
QList<KPackage::Package> KCMLookandFeel::availablePackages(const QStringList &components)
133
{
134
    QList<KPackage::Package> packages;
135
    QStringList paths;
136
    const QStringList dataPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
137

138
    paths.reserve(dataPaths.count());
139
    for (const QString &path : dataPaths) {
140
        QDir dir(path + QStringLiteral("/plasma/look-and-feel"));
141
142
143
144
        paths << dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
    }

    for (const QString &path : paths) {
145
        KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
146
        pkg.setPath(path);
147
        pkg.setFallbackPackage(KPackage::Package());
148
        if (components.isEmpty()) {
149
            packages << pkg;
150
151
152
153
154
155
156
        } else {
            for (const auto &component : components) {
                if (!pkg.filePath(component.toUtf8()).isEmpty()) {
                    packages << pkg;
                    break;
                }
            }
157
158
159
160
161
162
        }
    }

    return packages;
}

163
164
165
166
167
LookAndFeelSettings *KCMLookandFeel::lookAndFeelSettings() const
{
    return m_settings;
}

168
void KCMLookandFeel::loadModel()
Marco Martin's avatar
Marco Martin committed
169
170
171
{
    m_model->clear();

172
173
    const QList<KPackage::Package> pkgs = availablePackages({"defaults", "layouts"});
    for (const KPackage::Package &pkg : pkgs) {
Marco Martin's avatar
Marco Martin committed
174
175
176
        if (!pkg.metadata().isValid()) {
            continue;
        }
177
        QStandardItem *row = new QStandardItem(pkg.metadata().name());
178
179
        row->setData(pkg.metadata().pluginId(), PluginNameRole);
        row->setData(pkg.metadata().description(), DescriptionRole);
180
        row->setData(pkg.filePath("preview"), ScreenshotRole);
181
        row->setData(pkg.filePath("fullscreenpreview"), FullScreenPreviewRole);
Marco Martin's avatar
Marco Martin committed
182
183
184
185
186
187
188
189
190
191

        //What the package provides
        row->setData(!pkg.filePath("splashmainscript").isEmpty(), HasSplashRole);
        row->setData(!pkg.filePath("lockscreenmainscript").isEmpty(), HasLockScreenRole);
        row->setData(!pkg.filePath("runcommandmainscript").isEmpty(), HasRunCommandRole);
        row->setData(!pkg.filePath("logoutmainscript").isEmpty(), HasLogoutRole);

        if (!pkg.filePath("defaults").isEmpty()) {
            KSharedConfigPtr conf = KSharedConfig::openConfig(pkg.filePath("defaults"));
            KConfigGroup cg(conf, "kdeglobals");
192
            cg = KConfigGroup(&cg, "General");
Marco Martin's avatar
Marco Martin committed
193
194
195
196
            bool hasColors = !cg.readEntry("ColorScheme", QString()).isEmpty();
            if (!hasColors) {
                hasColors = !pkg.filePath("colors").isEmpty();
            }
197
            row->setData(hasColors, HasColorsRole);
198
            cg = KConfigGroup(&cg, "KDE");
Marco Martin's avatar
Marco Martin committed
199
200
201
202
203
204
205
206
207
208
209
210
            row->setData(!cg.readEntry("widgetStyle", QString()).isEmpty(), HasWidgetStyleRole);
            cg = KConfigGroup(conf, "kdeglobals");
            cg = KConfigGroup(&cg, "Icons");
            row->setData(!cg.readEntry("Theme", QString()).isEmpty(), HasIconsRole);

            cg = KConfigGroup(conf, "kdeglobals");
            cg = KConfigGroup(&cg, "Theme");
            row->setData(!cg.readEntry("name", QString()).isEmpty(), HasPlasmaThemeRole);

            cg = KConfigGroup(conf, "kcminputrc");
            cg = KConfigGroup(&cg, "Mouse");
            row->setData(!cg.readEntry("cursorTheme", QString()).isEmpty(), HasCursorsRole);
211
212
213
214

            cg = KConfigGroup(conf, "kwinrc");
            cg = KConfigGroup(&cg, "WindowSwitcher");
            row->setData(!cg.readEntry("LayoutName", QString()).isEmpty(), HasWindowSwitcherRole);
215
216
217
218

            cg = KConfigGroup(conf, "kwinrc");
            cg = KConfigGroup(&cg, "DesktopSwitcher");
            row->setData(!cg.readEntry("LayoutName", QString()).isEmpty(), HasDesktopSwitcherRole);
Marco Martin's avatar
Marco Martin committed
219
220
221
222
        }

        m_model->appendRow(row);
    }
223
    m_model->sort(0 /*column*/);
224
225
226

    //Model has been cleared so pretend the selected look and fell changed to force view update
    emit m_settings->lookAndFeelPackageChanged();
227
228
229
230
}

void KCMLookandFeel::load()
{
231
    ManagedConfigModule::load();
232

233
234
    m_package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
    m_package.setPath(m_settings->lookAndFeelPackage());
Marco Martin's avatar
Marco Martin committed
235
236
237
238
}

void KCMLookandFeel::save()
{
239
    KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
240
    package.setPath(m_settings->lookAndFeelPackage());
Marco Martin's avatar
Marco Martin committed
241
242
243
244
245

    if (!package.isValid()) {
        return;
    }

246
    ManagedConfigModule::save();
Marco Martin's avatar
Marco Martin committed
247

248
    if (m_resetDefaultLayout) {
249
        QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), QStringLiteral("/PlasmaShell"),
250
251
252
                                                   QStringLiteral("org.kde.PlasmaShell"), QStringLiteral("loadLookAndFeelDefaultLayout"));

        QList<QVariant> args;
253
        args << m_settings->lookAndFeelPackage();
254
255
256
257
258
        message.setArguments(args);

        QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
    }

Marco Martin's avatar
Marco Martin committed
259
260
261
262
263
    if (!package.filePath("defaults").isEmpty()) {
        KSharedConfigPtr conf = KSharedConfig::openConfig(package.filePath("defaults"));
        KConfigGroup cg(conf, "kdeglobals");
        cg = KConfigGroup(&cg, "KDE");
        if (m_applyWidgetStyle) {
264
265
266
267
268
269
            QString widgetStyle = cg.readEntry("widgetStyle", QString());
            // Some global themes refer to breeze's widgetStyle with a lowercase b.
            if (widgetStyle == QStringLiteral("breeze")) {
                widgetStyle = QStringLiteral("Breeze");
            }
            setWidgetStyle(widgetStyle);
Marco Martin's avatar
Marco Martin committed
270
271
272
273
        }

        if (m_applyColors) {
            QString colorsFile = package.filePath("colors");
Marco Martin's avatar
Marco Martin committed
274
275
            KConfigGroup cg(conf, "kdeglobals");
            cg = KConfigGroup(&cg, "General");
Marco Martin's avatar
Marco Martin committed
276
            QString colorScheme = cg.readEntry("ColorScheme", QString());
Marco Martin's avatar
Marco Martin committed
277

Marco Martin's avatar
Marco Martin committed
278
279
280
281
282
283
284
            if (!colorsFile.isEmpty()) {
                if (!colorScheme.isEmpty()) {
                    setColors(colorScheme, colorsFile);
                } else {
                    setColors(package.metadata().name(), colorsFile);
                }
            } else if (!colorScheme.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
285
                colorScheme.remove(QLatin1Char('\'')); // So Foo's does not become FooS
286
                QRegExp fixer(QStringLiteral("[\\W,.-]+(.?)"));
Marco Martin's avatar
Marco Martin committed
287
                int offset;
Marco Martin's avatar
Marco Martin committed
288
                while ((offset = fixer.indexIn(colorScheme)) >= 0) {
Marco Martin's avatar
Marco Martin committed
289
                    colorScheme.replace(offset, fixer.matchedLength(), fixer.cap(1).toUpper());
Marco Martin's avatar
Marco Martin committed
290
                }
Marco Martin's avatar
Marco Martin committed
291
                colorScheme.replace(0, 1, colorScheme.at(0).toUpper());
292
293
294
295
296
297
298
299
300
301
302

                //NOTE: why this loop trough all the scheme files?
                //the scheme theme name is an heuristic, there is no plugin metadata whatsoever.
                //is based on the file name stripped from weird characters or the
                //eventual id- prefix store.kde.org puts, so we can just find a
                //theme that ends as the specified name
                bool schemeFound = false;
                const QStringList schemeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes"), QStandardPaths::LocateDirectory);
                for (const QString &dir : schemeDirs) {
                    const QStringList fileNames = QDir(dir).entryList(QStringList()<<QStringLiteral("*.colors"));
                    for (const QString &file : fileNames) {
303
304
                        if (file.endsWith(colorScheme + QStringLiteral(".colors"))) {
                            setColors(colorScheme, dir + QLatin1Char('/') + file);
305
306
307
308
309
310
311
312
                            schemeFound = true;
                            break;
                        }
                    }
                    if (schemeFound) {
                        break;
                    }
                }
Marco Martin's avatar
Marco Martin committed
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
            }
        }

        if (m_applyIcons) {
            cg = KConfigGroup(conf, "kdeglobals");
            cg = KConfigGroup(&cg, "Icons");
            setIcons(cg.readEntry("Theme", QString()));
        }

        if (m_applyPlasmaTheme) {
            cg = KConfigGroup(conf, "plasmarc");
            cg = KConfigGroup(&cg, "Theme");
            setPlasmaTheme(cg.readEntry("name", QString()));
        }

        if (m_applyCursors) {
            cg = KConfigGroup(conf, "kcminputrc");
            cg = KConfigGroup(&cg, "Mouse");
            setCursorTheme(cg.readEntry("cursorTheme", QString()));
        }
333
334
335
336
337
338

        if (m_applyWindowSwitcher) {
            cg = KConfigGroup(conf, "kwinrc");
            cg = KConfigGroup(&cg, "WindowSwitcher");
            setWindowSwitcher(cg.readEntry("LayoutName", QString()));
        }
339
340
341
342
343
344

        if (m_applyDesktopSwitcher) {
            cg = KConfigGroup(conf, "kwinrc");
            cg = KConfigGroup(&cg, "DesktopSwitcher");
            setDesktopSwitcher(cg.readEntry("LayoutName", QString()));
        }
345
346
347
348

        if (m_applyWindowDecoration) {
            cg = KConfigGroup(conf, "kwinrc");
            cg = KConfigGroup(&cg, "org.kde.kdecoration2");
349
#ifdef HAVE_BREEZE_DECO
350
            setWindowDecoration(cg.readEntry("library", QStringLiteral(BREEZE_KDECORATION_PLUGIN_ID)), cg.readEntry("theme", QStringLiteral("Breeze")));
351
#else
352
            setWindowDecoration(cg.readEntry("library", QStringLiteral("org.kde.kwin.aurorae")), cg.readEntry("theme", QStringLiteral("kwin4_decoration_qml_plastik")));
353
354
355
356
357
358
359
360
361
362
#endif
        }

        // Reload KWin if something changed, but only once.
        if (m_applyWindowSwitcher || m_applyDesktopSwitcher || m_applyWindowDecoration) {
            QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"),
                                                    QStringLiteral("org.kde.KWin"),
                                                    QStringLiteral("reloadConfig"));
            QDBusConnection::sessionBus().send(message);
        }
363
364
365
366
367
368
369
370
371

        //autostart
        if (m_resetDefaultLayout) {
            //remove all the old package to autostart
            {
                KSharedConfigPtr oldConf = KSharedConfig::openConfig(m_package.filePath("defaults"));
                cg = KConfigGroup(oldConf, QStringLiteral("Autostart"));
                const QStringList autostartServices = cg.readEntry("Services", QStringList());

372
373
374
375
376
377
378
379
                if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) {
                    for (const QString &serviceFile : autostartServices) {
                        KService service(serviceFile + QStringLiteral(".desktop"));
                        KAutostart as(serviceFile);
                        as.setAutostarts(false);
                        //FIXME: quite ugly way to stop things, and what about non KDE things?
                        QProcess::startDetached(QStringLiteral("kquitapp5"), {QStringLiteral("--service"), service.property(QStringLiteral("X-DBUS-ServiceName")).toString()});
                    }
380
381
382
383
384
385
386
387
388
389
390
391
                }
            }
            //Set all the stuff in the new lnf to autostart
            {
                cg = KConfigGroup(conf, QStringLiteral("Autostart"));
                const QStringList autostartServices = cg.readEntry("Services", QStringList());

                for (const QString &serviceFile : autostartServices) {
                    KService service(serviceFile + QStringLiteral(".desktop"));
                    KAutostart as(serviceFile);
                    as.setCommand(service.exec());
                    as.setAutostarts(true);
392
393
394
                    if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) {
                        KRun::runApplication(service, {}, nullptr);
                    }
395
396
397
                }
            }
        }
Marco Martin's avatar
Marco Martin committed
398
399
    }

400
    //TODO: option to enable/disable apply? they don't seem required by UI design
401
402
    setSplashScreen(m_settings->lookAndFeelPackage());
    setLockScreen(m_settings->lookAndFeelPackage());
403

Marco Martin's avatar
Marco Martin committed
404
    m_configGroup.sync();
405
    m_package.setPath(m_settings->lookAndFeelPackage());
Marco Martin's avatar
Marco Martin committed
406
    runRdb(KRdbExportQtColors | KRdbExportGtkTheme | KRdbExportColors | KRdbExportQtSettings | KRdbExportXftSettings);
Marco Martin's avatar
Marco Martin committed
407
408
409
410
411
412
413
414
}

void KCMLookandFeel::setWidgetStyle(const QString &style)
{
    if (style.isEmpty()) {
        return;
    }

415
416
417
418
419
420
421
422
423
    // Some global themes use styles that may not be installed.
    // Test if style can be installed before updating the config.
    QScopedPointer<QStyle> newStyle(QStyleFactory::create(style));
    if (newStyle) {
        m_configGroup.writeEntry("widgetStyle", style);
        m_configGroup.sync();
        //FIXME: changing style on the fly breaks QQuickWidgets
        KGlobalSettings::self()->emitChange(KGlobalSettings::StyleChanged);
    }
Marco Martin's avatar
Marco Martin committed
424
425
426
427
}

void KCMLookandFeel::setColors(const QString &scheme, const QString &colorFile)
{
Marco Martin's avatar
Marco Martin committed
428
    if (scheme.isEmpty() && colorFile.isEmpty()) {
Marco Martin's avatar
Marco Martin committed
429
430
431
        return;
    }

432
    KSharedConfigPtr conf = KSharedConfig::openConfig(colorFile, KSharedConfig::CascadeConfig);
Marco Martin's avatar
Marco Martin committed
433
    foreach (const QString &grp, conf->groupList()) {
434
435
436
        KConfigGroup cg(conf, grp);
        KConfigGroup cg2(&m_config, grp);
        cg.copyTo(&cg2);
437
    }
438

439
440
441
    KConfigGroup configGroup(&m_config, "General");
    configGroup.writeEntry("ColorScheme", scheme);

442
    configGroup.sync();
443
    KGlobalSettings::self()->emitChange(KGlobalSettings::PaletteChanged);
Marco Martin's avatar
Marco Martin committed
444
445
446
447
448
449
450
451
452
453
}

void KCMLookandFeel::setIcons(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }

    KConfigGroup cg(&m_config, "Icons");
    cg.writeEntry("Theme", theme);
Marco Martin's avatar
Marco Martin committed
454
455
456
457
458
    cg.sync();

    for (int i=0; i < KIconLoader::LastGroup; i++) {
        KIconLoader::emitChange(KIconLoader::Group(i));
    }
Marco Martin's avatar
Marco Martin committed
459
460
461
462
463
464
465
466
}

void KCMLookandFeel::setPlasmaTheme(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }

467
    KConfig config(QStringLiteral("plasmarc"));
Marco Martin's avatar
Marco Martin committed
468
469
470
471
472
473
474
475
476
477
478
479
    KConfigGroup cg(&config, "Theme");
    cg.writeEntry("name", theme);
    cg.sync();
}

void KCMLookandFeel::setCursorTheme(const QString themeName)
{
    //TODO: use pieces of cursor kcm when moved to plasma-desktop
    if (themeName.isEmpty()) {
        return;
    }

480
    KConfig config(QStringLiteral("kcminputrc"));
Marco Martin's avatar
Marco Martin committed
481
482
483
484
    KConfigGroup cg(&config, "Mouse");
    cg.writeEntry("cursorTheme", themeName);
    cg.sync();

485
#ifdef HAVE_XCURSOR
Marco Martin's avatar
Marco Martin committed
486
487
488
    // Require the Xcursor version that shipped with X11R6.9 or greater, since
    // in previous versions the Xfixes code wasn't enabled due to a bug in the
    // build system (freedesktop bug #975).
489
#if defined(HAVE_XFIXES) && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105
490
491
    const int cursorSize = cg.readEntry("cursorSize", 0);

Marco Martin's avatar
Marco Martin committed
492
    QDir themeDir = cursorThemeDir(themeName, 0);
Marco Martin's avatar
debug--    
Marco Martin committed
493

Marco Martin's avatar
Marco Martin committed
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
    if (!themeDir.exists()) {
        return;
    }

    XCursorTheme theme(themeDir);

    if (!CursorTheme::haveXfixes()) {
        return;
    }

    // Set up the proper launch environment for newly started apps
    OrgKdeKLauncherInterface klauncher(QStringLiteral("org.kde.klauncher5"),
                                       QStringLiteral("/KLauncher"),
                                       QDBusConnection::sessionBus());
    klauncher.setLaunchEnv(QStringLiteral("XCURSOR_THEME"), themeName);

    // Update the Xcursor X resources
    runRdb(0);

    // Notify all applications that the cursor theme has changed
    KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged);

    // Reload the standard cursors
    QStringList names;

    // Qt cursors
520
521
522
523
524
    names << QStringLiteral("left_ptr")       << QStringLiteral("up_arrow")      << QStringLiteral("cross")      << QStringLiteral("wait")
          << QStringLiteral("left_ptr_watch") << QStringLiteral("ibeam")         << QStringLiteral("size_ver")   << QStringLiteral("size_hor")
          << QStringLiteral("size_bdiag")     << QStringLiteral("size_fdiag")    << QStringLiteral("size_all")   << QStringLiteral("split_v")
          << QStringLiteral("split_h")        << QStringLiteral("pointing_hand") << QStringLiteral("openhand")
          << QStringLiteral("closedhand")     << QStringLiteral("forbidden")     << QStringLiteral("whats_this") << QStringLiteral("copy") << QStringLiteral("move") << QStringLiteral("link");
Marco Martin's avatar
Marco Martin committed
525
526

    // X core cursors
527
528
529
530
531
532
533
534
    names << QStringLiteral("X_cursor")            << QStringLiteral("right_ptr")           << QStringLiteral("hand1")
          << QStringLiteral("hand2")               << QStringLiteral("watch")               << QStringLiteral("xterm")
          << QStringLiteral("crosshair")           << QStringLiteral("left_ptr_watch")      << QStringLiteral("center_ptr")
          << QStringLiteral("sb_h_double_arrow")   << QStringLiteral("sb_v_double_arrow")   << QStringLiteral("fleur")
          << QStringLiteral("top_left_corner")     << QStringLiteral("top_side")            << QStringLiteral("top_right_corner")
          << QStringLiteral("right_side")          << QStringLiteral("bottom_right_corner") << QStringLiteral("bottom_side")
          << QStringLiteral("bottom_left_corner")  << QStringLiteral("left_side")           << QStringLiteral("question_arrow")
          << QStringLiteral("pirate");
Marco Martin's avatar
Marco Martin committed
535
536

    foreach (const QString &name, names) {
537
        XFixesChangeCursorByName(QX11Info::display(), theme.loadCursor(name, cursorSize), QFile::encodeName(name));
Marco Martin's avatar
Marco Martin committed
538
539
540
541
    }

#else
    KMessageBox::information(this,
542
                                 i18n("You have to restart the Plasma session for these changes to take effect."),
Marco Martin's avatar
Marco Martin committed
543
544
                                 i18n("Cursor Settings Changed"), "CursorSettingsChanged");
#endif
545
#endif
Marco Martin's avatar
Marco Martin committed
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
}

QDir KCMLookandFeel::cursorThemeDir(const QString &theme, const int depth)
{
    // Prevent infinite recursion
    if (depth > 10) {
        return QDir();
    }

    // Search each icon theme directory for 'theme'
    foreach (const QString &baseDir, cursorSearchPaths()) {
        QDir dir(baseDir);
        if (!dir.exists() || !dir.cd(theme)) {
            continue;
        }

        // If there's a cursors subdir, we'll assume this is a cursor theme
563
        if (dir.exists(QStringLiteral("cursors"))) {
Marco Martin's avatar
Marco Martin committed
564
565
566
567
            return dir;
        }

        // If the theme doesn't have an index.theme file, it can't inherit any themes.
568
        if (!dir.exists(QStringLiteral("index.theme"))) {
Marco Martin's avatar
Marco Martin committed
569
570
571
572
            continue;
        }

        // Open the index.theme file, so we can get the list of inherited themes
Laurent Montel's avatar
Laurent Montel committed
573
        KConfig config(dir.path() + QStringLiteral("/index.theme"), KConfig::NoGlobals);
Marco Martin's avatar
Marco Martin committed
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
        KConfigGroup cg(&config, "Icon Theme");

        // Recurse through the list of inherited themes, to check if one of them
        // is a cursor theme.
        QStringList inherits = cg.readEntry("Inherits", QStringList());
        foreach (const QString &inherit, inherits) {
            // Avoid possible DoS
            if (inherit == theme) {
                continue;
            }

            if (cursorThemeDir(inherit, depth + 1).exists()) {
                return dir;
            }
        }
    }

    return QDir();
}

const QStringList KCMLookandFeel::cursorSearchPaths()
{
    if (!m_cursorSearchPaths.isEmpty())
        return m_cursorSearchPaths;

599
#ifdef HAVE_XCURSOR
Marco Martin's avatar
Marco Martin committed
600
601
602
603
604
605
606
607
608
609
610
611
612
613
#if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1
    // These are the default paths Xcursor will scan for cursor themes
    QString path("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons");

    // If XCURSOR_PATH is set, use that instead of the default path
    char *xcursorPath = std::getenv("XCURSOR_PATH");
    if (xcursorPath)
        path = xcursorPath;
#else
    // Get the search path from Xcursor
    QString path = XcursorLibraryPath();
#endif

    // Separate the paths
614
    m_cursorSearchPaths = path.split(QLatin1Char(':'), QString::SkipEmptyParts);
Marco Martin's avatar
Marco Martin committed
615
616
617
618
619
620
621
622
623
624
625
626
627

    // Remove duplicates
    QMutableStringListIterator i(m_cursorSearchPaths);
    while (i.hasNext())
    {
        const QString path = i.next();
        QMutableStringListIterator j(i);
        while (j.hasNext())
            if (j.next() == path)
                j.remove();
    }

    // Expand all occurrences of ~/ to the home dir
628
    m_cursorSearchPaths.replaceInStrings(QRegExp(QStringLiteral("^~\\/")), QDir::home().path() + QLatin1Char('/'));
Marco Martin's avatar
Marco Martin committed
629
    return m_cursorSearchPaths;
630
631
632
#else
    return QStringList();
#endif
Marco Martin's avatar
Marco Martin committed
633
634
}

635
636
637
638
639
640
void KCMLookandFeel::setSplashScreen(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }

641
    KConfig config(QStringLiteral("ksplashrc"));
642
643
644
645
646
647
648
649
650
651
652
653
654
    KConfigGroup cg(&config, "KSplash");
    cg.writeEntry("Theme", theme);
    //TODO: a way to set none as spash in the l&f
    cg.writeEntry("Engine", "KSplashQML");
    cg.sync();
}

void KCMLookandFeel::setLockScreen(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }

655
    KConfig config(QStringLiteral("kscreenlockerrc"));
656
657
658
659
660
    KConfigGroup cg(&config, "Greeter");
    cg.writeEntry("Theme", theme);
    cg.sync();
}

661
662
663
664
665
void KCMLookandFeel::setWindowSwitcher(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }
666

667
    KConfig config(QStringLiteral("kwinrc"));
668
669
670
671
    KConfigGroup cg(&config, "TabBox");
    cg.writeEntry("LayoutName", theme);
    cg.sync();
}
672

673
674
675
676
677
678
void KCMLookandFeel::setDesktopSwitcher(const QString &theme)
{
    if (theme.isEmpty()) {
        return;
    }

679
    KConfig config(QStringLiteral("kwinrc"));
680
681
682
683
    KConfigGroup cg(&config, "TabBox");
    cg.writeEntry("DesktopLayout", theme);
    cg.writeEntry("DesktopListLayout", theme);
    cg.sync();
684
685
686
687
688
689
690
691
692
693
694
695
}

void KCMLookandFeel::setWindowDecoration(const QString &library, const QString &theme)
{
    if (library.isEmpty()) {
        return;
    }

    KConfig config(QStringLiteral("kwinrc"));
    KConfigGroup cg(&config, "org.kde.kdecoration2");
    cg.writeEntry("library", library);
    cg.writeEntry("theme", theme);
696

697
    cg.sync();
698
699
700
701
702
703
704
    // Reload KWin.
    QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"),
                                                      QStringLiteral("org.kde.KWin"),
                                                      QStringLiteral("reloadConfig"));
    QDBusConnection::sessionBus().send(message);
}

705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
void KCMLookandFeel::setResetDefaultLayout(bool reset)
{
    if (m_resetDefaultLayout == reset) {
        return;
    }
    m_resetDefaultLayout = reset;
    emit resetDefaultLayoutChanged();
    if (reset) {
        setNeedsSave(true);
    }
}

bool KCMLookandFeel::resetDefaultLayout() const
{
    return m_resetDefaultLayout;
}