File indexing completed on 2024-05-19 05:38:05

0001 /*
0002     SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org>
0003     SPDX-FileCopyrightText: 2014 Vishesh Handa <me@vhanda.in>
0004     SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
0005     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0006     SPDX-FileCopyrightText: 2019 Kevin Ottens <kevin.ottens@enioka.com>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-only
0009 */
0010 
0011 #include "kcm.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KPluginFactory>
0015 
0016 #include <KIO/FileCopyJob>
0017 #include <KIO/JobUiDelegate>
0018 
0019 #include <KSvg/FrameSvg>
0020 #include <KSvg/ImageSet>
0021 #include <KSvg/Svg>
0022 
0023 #include <QDBusConnection>
0024 #include <QDBusMessage>
0025 #include <QDebug>
0026 #include <QProcess>
0027 #include <QQuickItem>
0028 #include <QQuickWindow>
0029 #include <QStandardItemModel>
0030 #include <QStandardPaths>
0031 #include <QTemporaryFile>
0032 
0033 #include "desktopthemedata.h"
0034 #include "filterproxymodel.h"
0035 
0036 Q_LOGGING_CATEGORY(KCM_DESKTOP_THEME, "kcm_desktoptheme")
0037 
0038 K_PLUGIN_FACTORY_WITH_JSON(KCMDesktopThemeFactory, "kcm_desktoptheme.json", registerPlugin<KCMDesktopTheme>(); registerPlugin<DesktopThemeData>();)
0039 
0040 KCMDesktopTheme::KCMDesktopTheme(QObject *parent, const KPluginMetaData &data)
0041     : KQuickManagedConfigModule(parent, data)
0042     , m_data(new DesktopThemeData(this))
0043     , m_model(new ThemesModel(this))
0044     , m_filteredModel(new FilterProxyModel(this))
0045     , m_haveThemeExplorerInstalled(false)
0046 {
0047     qmlRegisterAnonymousType<DesktopThemeSettings>("org.kde.private.kcms.desktoptheme", 1);
0048     qmlRegisterUncreatableType<ThemesModel>("org.kde.private.kcms.desktoptheme", 1, 0, "ThemesModel", QStringLiteral("Cannot create ThemesModel"));
0049     qmlRegisterUncreatableType<FilterProxyModel>("org.kde.private.kcms.desktoptheme",
0050                                                  1,
0051                                                  0,
0052                                                  "FilterProxyModel",
0053                                                  QStringLiteral("Cannot create FilterProxyModel"));
0054 
0055     setButtons(Apply | Default | Help);
0056 
0057     m_haveThemeExplorerInstalled = !QStandardPaths::findExecutable(QStringLiteral("plasmathemeexplorer")).isEmpty();
0058 
0059     connect(m_model, &ThemesModel::pendingDeletionsChanged, this, &KCMDesktopTheme::settingsChanged);
0060 
0061     connect(m_model, &ThemesModel::selectedThemeChanged, this, [this](const QString &pluginName) {
0062         desktopThemeSettings()->setName(pluginName);
0063     });
0064 
0065     connect(desktopThemeSettings(), &DesktopThemeSettings::nameChanged, this, [this] {
0066         m_model->setSelectedTheme(desktopThemeSettings()->name());
0067     });
0068 
0069     connect(m_model, &ThemesModel::selectedThemeChanged, m_filteredModel, &FilterProxyModel::setSelectedTheme);
0070 
0071     m_filteredModel->setSourceModel(m_model);
0072 }
0073 
0074 KCMDesktopTheme::~KCMDesktopTheme()
0075 {
0076 }
0077 
0078 DesktopThemeSettings *KCMDesktopTheme::desktopThemeSettings() const
0079 {
0080     return m_data->settings();
0081 }
0082 
0083 ThemesModel *KCMDesktopTheme::desktopThemeModel() const
0084 {
0085     return m_model;
0086 }
0087 
0088 FilterProxyModel *KCMDesktopTheme::filteredModel() const
0089 {
0090     return m_filteredModel;
0091 }
0092 
0093 bool KCMDesktopTheme::downloadingFile() const
0094 {
0095     return m_tempCopyJob;
0096 }
0097 
0098 void KCMDesktopTheme::installThemeFromFile(const QUrl &url)
0099 {
0100     if (url.isLocalFile()) {
0101         installTheme(url.toLocalFile());
0102         return;
0103     }
0104 
0105     if (m_tempCopyJob) {
0106         return;
0107     }
0108 
0109     m_tempInstallFile.reset(new QTemporaryFile());
0110     if (!m_tempInstallFile->open()) {
0111         Q_EMIT showErrorMessage(i18n("Unable to create a temporary file."));
0112         m_tempInstallFile.reset();
0113         return;
0114     }
0115 
0116     m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite);
0117     m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
0118     Q_EMIT downloadingFileChanged();
0119 
0120     connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) {
0121         if (job->error() != KJob::NoError) {
0122             Q_EMIT showErrorMessage(i18n("Unable to download the theme: %1", job->errorText()));
0123             return;
0124         }
0125 
0126         installTheme(m_tempInstallFile->fileName());
0127         m_tempInstallFile.reset();
0128     });
0129     connect(m_tempCopyJob, &QObject::destroyed, this, &KCMDesktopTheme::downloadingFileChanged);
0130 }
0131 
0132 void KCMDesktopTheme::installTheme(const QString &path)
0133 {
0134     qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << path;
0135 
0136     const QString program = QStringLiteral("kpackagetool6");
0137     const QStringList arguments = {QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), path};
0138 
0139     qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QLatin1Char(' '));
0140     QProcess *myProcess = new QProcess(this);
0141     connect(myProcess,
0142             static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
0143             this,
0144             [this](int exitCode, QProcess::ExitStatus exitStatus) {
0145                 Q_UNUSED(exitStatus)
0146                 if (exitCode == 0) {
0147                     Q_EMIT showSuccessMessage(i18n("Theme installed successfully."));
0148                     load();
0149                 } else {
0150                     Q_EMIT showErrorMessage(i18n("Theme installation failed."));
0151                 }
0152             });
0153 
0154     connect(myProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError e) {
0155         qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed: " << e;
0156         Q_EMIT showErrorMessage(i18n("Theme installation failed."));
0157     });
0158 
0159     myProcess->start(program, arguments);
0160 }
0161 
0162 void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName)
0163 {
0164     if (!item) {
0165         return;
0166     }
0167 
0168     KSvg::ImageSet *imageSet = m_themes[themeName];
0169     if (!imageSet) {
0170         imageSet = new KSvg::ImageSet(themeName, QStringLiteral("plasma/desktoptheme"), this);
0171         m_themes[themeName] = imageSet;
0172     }
0173 
0174     Q_FOREACH (KSvg::Svg *svg, item->findChildren<KSvg::Svg *>()) {
0175         auto *frame = qobject_cast<KSvg::FrameSvg *>(svg);
0176         svg->setUsingRenderingCache(false);
0177         if (frame) {
0178             frame->setCacheAllRenderedFrames(false);
0179         }
0180         svg->setImageSet(imageSet);
0181     }
0182 }
0183 
0184 void KCMDesktopTheme::load()
0185 {
0186     KQuickManagedConfigModule::load();
0187     m_model->load();
0188     m_model->setSelectedTheme(desktopThemeSettings()->name());
0189 }
0190 
0191 void KCMDesktopTheme::save()
0192 {
0193     auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
0194                                               QStringLiteral("/org/kde/KWin/BlendChanges"),
0195                                               QStringLiteral("org.kde.KWin.BlendChanges"),
0196                                               QStringLiteral("start"));
0197     // Plasma theme changes are known to be slow, so make the blend take a while
0198     msg << 1000;
0199     // This is deliberately blocking so that we ensure Kwin has processed the
0200     // animation start event before we potentially trigger client side changes
0201     QDBusConnection::sessionBus().call(msg);
0202 
0203     KQuickManagedConfigModule::save();
0204     KSvg::ImageSet().setImageSetName(desktopThemeSettings()->name());
0205     processPendingDeletions();
0206 }
0207 
0208 void KCMDesktopTheme::defaults()
0209 {
0210     KQuickManagedConfigModule::defaults();
0211 
0212     // can this be done more elegantly?
0213     const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true);
0214     for (const QModelIndex &idx : pendingDeletions) {
0215         m_model->setData(idx, false, ThemesModel::PendingDeletionRole);
0216     }
0217 }
0218 
0219 bool KCMDesktopTheme::canEditThemes() const
0220 {
0221     return m_haveThemeExplorerInstalled;
0222 }
0223 
0224 void KCMDesktopTheme::editTheme(const QString &theme)
0225 {
0226     QProcess::startDetached(QStringLiteral("plasmathemeexplorer"), {QStringLiteral("-t"), theme});
0227 }
0228 
0229 bool KCMDesktopTheme::isSaveNeeded() const
0230 {
0231     return !m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true).isEmpty();
0232 }
0233 
0234 void KCMDesktopTheme::processPendingDeletions()
0235 {
0236     const QString program = QStringLiteral("plasmapkg2");
0237 
0238     const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true, -1 /*all*/);
0239     QList<QPersistentModelIndex> persistentPendingDeletions;
0240     // turn into persistent model index so we can delete as we go
0241     std::transform(pendingDeletions.begin(), pendingDeletions.end(), std::back_inserter(persistentPendingDeletions), [](const QModelIndex &idx) {
0242         return QPersistentModelIndex(idx);
0243     });
0244 
0245     for (const QPersistentModelIndex &idx : persistentPendingDeletions) {
0246         const QString pluginName = idx.data(ThemesModel::PluginNameRole).toString();
0247         const QString displayName = idx.data(Qt::DisplayRole).toString();
0248 
0249         Q_ASSERT(pluginName != desktopThemeSettings()->name());
0250 
0251         const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), pluginName};
0252 
0253         QProcess *process = new QProcess(this);
0254         connect(process,
0255                 static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
0256                 this,
0257                 [this, process, idx, pluginName, displayName](int exitCode, QProcess::ExitStatus exitStatus) {
0258                     Q_UNUSED(exitStatus)
0259                     if (exitCode == 0) {
0260                         m_model->removeRow(idx.row());
0261                     } else {
0262                         Q_EMIT showErrorMessage(i18n("Removing theme failed: %1", QString::fromLocal8Bit(process->readAllStandardOutput().trimmed())));
0263                         m_model->setData(idx, false, ThemesModel::PendingDeletionRole);
0264                     }
0265                     process->deleteLater();
0266                 });
0267 
0268         process->start(program, arguments);
0269         process->waitForFinished(); // needed so it deletes fine when "OK" is clicked and the dialog destroyed
0270     }
0271 }
0272 
0273 #include "kcm.moc"