kcm.cpp 11.3 KB
Newer Older
David Rosca's avatar
David Rosca committed
1 2 3 4
/* This file is part of the KDE Project
   Copyright (c) 2014 Marco Martin <mart@kde.org>
   Copyright (c) 2014 Vishesh Handa <me@vhanda.in>
   Copyright (c) 2016 David Rosca <nowrep@gmail.com>
5
   Copyright (c) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
David Rosca's avatar
David Rosca committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

   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 <KPluginFactory>
#include <KAboutData>
#include <KSharedConfig>
#include <KLocalizedString>
#include <KDesktopFile>

#include <Plasma/Theme>
#include <Plasma/Svg>

#include <QDebug>
#include <QProcess>
#include <QQuickItem>
36
#include <QQuickWindow>
David Rosca's avatar
David Rosca committed
37 38 39 40 41 42 43 44 45 46 47 48
#include <QStandardPaths>
#include <QStandardItemModel>

#include <KNewStuff3/KNS3/DownloadDialog>

Q_LOGGING_CATEGORY(KCM_DESKTOP_THEME, "kcm_desktoptheme")

K_PLUGIN_FACTORY_WITH_JSON(KCMDesktopThemeFactory, "kcm_desktoptheme.json", registerPlugin<KCMDesktopTheme>();)

KCMDesktopTheme::KCMDesktopTheme(QObject *parent, const QVariantList &args)
    : KQuickAddons::ConfigModule(parent, args)
    , m_defaultTheme(new Plasma::Theme(this))
49
    , m_haveThemeExplorerInstalled(false)
David Rosca's avatar
David Rosca committed
50 51 52 53 54 55 56 57 58 59 60
{
    //This flag seems to be needed in order for QQuickWidget to work
    //see https://bugreports.qt-project.org/browse/QTBUG-40765
    //also, it seems to work only if set in the kcm, not in the systemsettings' main
    qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
    qmlRegisterType<QStandardItemModel>();

    KAboutData* about = new KAboutData(QStringLiteral("kcm_desktoptheme"), i18n("Configure Desktop Theme"),
                                       QStringLiteral("0.1"), QString(), KAboutLicense::LGPL);
    about->addAuthor(i18n("David Rosca"), QString(), QStringLiteral("nowrep@gmail.com"));
    setAboutData(about);
61
    setButtons(Apply | Default | Help);
David Rosca's avatar
David Rosca committed
62 63 64 65 66

    m_model = new QStandardItemModel(this);
    QHash<int, QByteArray> roles = m_model->roleNames();
    roles[PluginNameRole] = QByteArrayLiteral("pluginName");
    roles[ThemeNameRole] = QByteArrayLiteral("themeName");
67
    roles[DescriptionRole] = QByteArrayLiteral("description");
David Rosca's avatar
David Rosca committed
68
    roles[IsLocalRole] = QByteArrayLiteral("isLocal");
69
    roles[PendingDeletionRole] = QByteArrayLiteral("pendingDeletion");
David Rosca's avatar
David Rosca committed
70
    m_model->setItemRoleNames(roles);
71 72

    m_haveThemeExplorerInstalled = !QStandardPaths::findExecutable(QStringLiteral("plasmathemeexplorer")).isEmpty();
David Rosca's avatar
David Rosca committed
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
}

KCMDesktopTheme::~KCMDesktopTheme()
{
    delete m_defaultTheme;
}

QStandardItemModel *KCMDesktopTheme::desktopThemeModel() const
{
    return m_model;
}

QString KCMDesktopTheme::selectedPlugin() const
{
    return m_selectedPlugin;
}

void KCMDesktopTheme::setSelectedPlugin(const QString &plugin)
{
    if (m_selectedPlugin == plugin) {
        return;
    }
    m_selectedPlugin = plugin;
96 97
    emit selectedPluginChanged(m_selectedPlugin);
    emit selectedPluginIndexChanged();
David Rosca's avatar
David Rosca committed
98 99 100
    updateNeedsSave();
}

101
int KCMDesktopTheme::selectedPluginIndex() const
David Rosca's avatar
David Rosca committed
102
{
103 104 105 106
    const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, m_selectedPlugin);
    if (results.count() == 1) {
        return results.first().row();
    }
David Rosca's avatar
David Rosca committed
107

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
    return -1;
}

void KCMDesktopTheme::setPendingDeletion(int index, bool pending)
{
    QModelIndex idx = m_model->index(index, 0);

    m_model->setData(idx, pending, PendingDeletionRole);

    if (pending && selectedPluginIndex() == index) {
        // move to the next non-pending theme
        const auto nonPending = m_model->match(idx, PendingDeletionRole, false);
        setSelectedPlugin(nonPending.first().data(PluginNameRole).toString());
    }

    updateNeedsSave();
}

void KCMDesktopTheme::getNewStuff(QQuickItem *ctx)
{
    if (!m_newStuffDialog) {
        m_newStuffDialog = new KNS3::DownloadDialog(QStringLiteral("plasma-themes.knsrc"));
        m_newStuffDialog.data()->setWindowTitle(i18n("Download New Desktop Themes"));
        m_newStuffDialog->setWindowModality(Qt::WindowModal);
        m_newStuffDialog->winId(); // so it creates the windowHandle();
        connect(m_newStuffDialog.data(), &KNS3::DownloadDialog::accepted, this, &KCMDesktopTheme::load);
    }

    if (ctx && ctx->window()) {
        m_newStuffDialog->windowHandle()->setTransientParent(ctx->window());
    }

    m_newStuffDialog.data()->show();
David Rosca's avatar
David Rosca committed
141 142 143 144 145 146
}

void KCMDesktopTheme::installThemeFromFile(const QUrl &file)
{
    qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << file;

147 148
    const QString program = QStringLiteral("kpackagetool5");
    const QStringList arguments = { QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), file.toLocalFile()};
David Rosca's avatar
David Rosca committed
149 150 151 152

    qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QStringLiteral(" "));
    QProcess *myProcess = new QProcess(this);
    connect(myProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
153
            this, [this, myProcess](int exitCode, QProcess::ExitStatus exitStatus) {
David Rosca's avatar
David Rosca committed
154 155
                Q_UNUSED(exitStatus);
                if (exitCode == 0) {
156
                    emit showSuccessMessage(i18n("Theme installed successfully."));
David Rosca's avatar
David Rosca committed
157 158
                    load();
                } else {
159
                    Q_EMIT showErrorMessage(i18n("Theme installation failed."));
160

David Rosca's avatar
David Rosca committed
161 162 163 164 165 166
                }
            });

    connect(myProcess, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error),
            this, [this](QProcess::ProcessError e) {
                qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed: " << e;
167
                Q_EMIT showErrorMessage(i18n("Theme installation failed."));
David Rosca's avatar
David Rosca committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
            });

    myProcess->start(program, arguments);
}

void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName)
{
    if (!item) {
        return;
    }

    Plasma::Theme *theme = m_themes[themeName];
    if (!theme) {
        theme = new Plasma::Theme(themeName, this);
        m_themes[themeName] = theme;
    }

    Q_FOREACH (Plasma::Svg *svg, item->findChildren<Plasma::Svg*>()) {
        svg->setTheme(theme);
        svg->setUsingRenderingCache(false);
    }
}

void KCMDesktopTheme::load()
{
    m_pendingRemoval.clear();

    // Get all desktop themes
    QStringList themes;
    const QStringList &packs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory);
    Q_FOREACH (const QString &ppath, packs) {
        const QDir cd(ppath);
        const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot);
        Q_FOREACH (const QString &pack, entries) {
            const QString _metadata = ppath + QLatin1Char('/') + pack + QStringLiteral("/metadata.desktop");
            if (QFile::exists(_metadata)) {
                themes << _metadata;
            }
        }
    }

    m_model->clear();

    Q_FOREACH (const QString &theme, themes) {
212
        int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1);
David Rosca's avatar
David Rosca committed
213
        const QString themeRoot = theme.left(themeSepIndex);
214
        int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1);
David Rosca's avatar
David Rosca committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1);

        KDesktopFile df(theme);

        if (df.noDisplay()) {
            continue;
        }

        QString name = df.readName();
        if (name.isEmpty()) {
            name = packageName;
        }
        const bool isLocal = QFileInfo(theme).isWritable();

        if (m_model->findItems(packageName).isEmpty()) {
            QStandardItem *item = new QStandardItem;
            item->setText(packageName);
            item->setData(packageName, PluginNameRole);
            item->setData(name, ThemeNameRole);
234
            item->setData(df.readComment(), DescriptionRole);
David Rosca's avatar
David Rosca committed
235
            item->setData(isLocal, IsLocalRole);
236
            item->setData(false, PendingDeletionRole);
David Rosca's avatar
David Rosca committed
237 238 239 240 241 242 243 244 245 246 247 248
            m_model->appendRow(item);
        }
    }

    KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("plasmarc")), "Theme");
    setSelectedPlugin(cg.readEntry("name", m_defaultTheme->themeName()));

    updateNeedsSave();
}

void KCMDesktopTheme::save()
{
249 250
    if (m_defaultTheme->themeName() != m_selectedPlugin) {
        m_defaultTheme->setThemeName(m_selectedPlugin);
David Rosca's avatar
David Rosca committed
251 252
    }

253
    processPendingDeletions();
David Rosca's avatar
David Rosca committed
254 255 256 257 258 259
    updateNeedsSave();
}

void KCMDesktopTheme::defaults()
{
    setSelectedPlugin(QStringLiteral("default"));
260 261 262 263 264 265

    // can this be done more elegantly?
    const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true);
    for (const QModelIndex &idx : pendingDeletions) {
        m_model->setData(idx, false, PendingDeletionRole);
    }
David Rosca's avatar
David Rosca committed
266 267
}

268 269 270 271 272 273 274 275 276 277
bool KCMDesktopTheme::canEditThemes() const
{
    return m_haveThemeExplorerInstalled;
}

void KCMDesktopTheme::editTheme(const QString &theme)
{
    QProcess::startDetached(QStringLiteral("plasmathemeexplorer -t ") % theme);
}

David Rosca's avatar
David Rosca committed
278 279
void KCMDesktopTheme::updateNeedsSave()
{
280 281
    setNeedsSave(!m_model->match(m_model->index(0, 0), PendingDeletionRole, true).isEmpty()
                    || m_selectedPlugin != m_defaultTheme->themeName());
David Rosca's avatar
David Rosca committed
282 283
}

284
void KCMDesktopTheme::processPendingDeletions()
David Rosca's avatar
David Rosca committed
285 286 287
{
    const QString program = QStringLiteral("plasmapkg2");

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true, -1 /*all*/);
    QVector<QPersistentModelIndex> persistentPendingDeletions;
    // turn into persistent model index so we can delete as we go
    std::transform(pendingDeletions.begin(), pendingDeletions.end(),
                   std::back_inserter(persistentPendingDeletions), [](const QModelIndex &idx) {
        return QPersistentModelIndex(idx);
    });

    for (const QPersistentModelIndex &idx : persistentPendingDeletions) {
        const QString pluginName = idx.data(PluginNameRole).toString();
        const QString displayName = idx.data(Qt::DisplayRole).toString();

        Q_ASSERT(pluginName != m_selectedPlugin);

        const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), pluginName};

David Rosca's avatar
David Rosca committed
304
        QProcess *process = new QProcess(this);
305 306 307 308 309 310 311 312 313 314 315 316
        connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
            [this, process, idx, pluginName, displayName](int exitCode, QProcess::ExitStatus exitStatus) {
                Q_UNUSED(exitStatus);
                if (exitCode == 0) {
                    m_model->removeRow(idx.row());
                } else {
                    emit showErrorMessage(i18n("Removing theme failed: %1",
                                               QString::fromLocal8Bit(process->readAllStandardOutput().trimmed())));
                    m_model->setData(idx, false, PendingDeletionRole);
                }
                process->deleteLater();
            });
David Rosca's avatar
David Rosca committed
317 318

        process->start(program, arguments);
319
        process->waitForFinished(); // needed so it deletes fine when "OK" is clicked and the dialog destroyed
David Rosca's avatar
David Rosca committed
320 321 322 323
    }
}

#include "kcm.moc"