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

0001 /*
0002     main.cpp
0003 
0004     SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
0005     SPDX-FileCopyrightText: 2000 Antonio Larrosa <larrosa@kde.org>
0006     SPDX-FileCopyrightText: 2000 Geert Jansen <jansen@kde.org>
0007     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0008     SPDX-FileCopyrightText: 2019 Benjamin Port <benjamin.port@enioka.com>
0009 
0010     KDE Frameworks 5 port:
0011     SPDX-FileCopyrightText: 2013 Jonathan Riddell <jr@jriddell.org>
0012 
0013     SPDX-License-Identifier: GPL-2.0-or-later
0014 */
0015 
0016 #include "main.h"
0017 #include "../kcms-common_p.h"
0018 
0019 #include <QDBusConnection>
0020 #include <QDBusMessage>
0021 #include <QGuiApplication>
0022 #include <QPainter>
0023 #include <QPixmapCache>
0024 #include <QProcess>
0025 #include <QQuickItem>
0026 #include <QQuickWindow>
0027 #include <QStringList>
0028 #include <QSvgRenderer>
0029 
0030 #include <KConfigGroup>
0031 #include <KIconLoader>
0032 #include <KIconTheme>
0033 #include <KJobUiDelegate>
0034 #include <KLocalizedString>
0035 #include <KPluginFactory>
0036 #include <KSharedConfig>
0037 #include <KTar>
0038 
0039 #include <KIO/DeleteJob>
0040 #include <KIO/FileCopyJob>
0041 
0042 #include <algorithm>
0043 #include <unistd.h> // for unlink
0044 
0045 #include "iconsdata.h"
0046 #include "iconsmodel.h"
0047 #include "iconssettings.h"
0048 
0049 #include "config.h" // for CMAKE_INSTALL_FULL_LIBEXECDIR
0050 
0051 K_PLUGIN_FACTORY_WITH_JSON(IconsFactory, "kcm_icons.json", registerPlugin<IconModule>(); registerPlugin<IconsData>();)
0052 
0053 IconModule::IconModule(QObject *parent, const KPluginMetaData &data)
0054     : KQuickManagedConfigModule(parent, data)
0055     , m_data(new IconsData(this))
0056     , m_model(new IconsModel(m_data->settings(), this))
0057 {
0058     auto uri = "org.kde.private.kcms.icons";
0059     qmlRegisterAnonymousType<IconsSettings>(uri, 1);
0060     qmlRegisterAnonymousType<IconsModel>(uri, 1);
0061 
0062     // to be able to access its enums
0063     qmlRegisterUncreatableType<KIconLoader>(uri, 1, 0, "KIconLoader", QString());
0064 
0065     setButtons(Apply | Default | Help);
0066 
0067     connect(m_model, &IconsModel::pendingDeletionsChanged, this, &IconModule::settingsChanged);
0068 
0069     // When user has a lot of themes installed, preview pixmaps might get evicted prematurely
0070     QPixmapCache::setCacheLimit(50 * 1024); // 50 MiB
0071 }
0072 
0073 IconModule::~IconModule()
0074 {
0075 }
0076 
0077 IconsSettings *IconModule::iconsSettings() const
0078 {
0079     return m_data->settings();
0080 }
0081 
0082 IconsModel *IconModule::iconsModel() const
0083 {
0084     return m_model;
0085 }
0086 
0087 bool IconModule::downloadingFile() const
0088 {
0089     return m_tempCopyJob;
0090 }
0091 
0092 void IconModule::load()
0093 {
0094     KQuickManagedConfigModule::load();
0095     m_model->load();
0096     // Model has been cleared so pretend the theme name changed to force view update
0097     Q_EMIT iconsSettings()->ThemeChanged();
0098 }
0099 
0100 void IconModule::save()
0101 {
0102     KQuickManagedConfigModule::save();
0103 
0104     processPendingDeletions();
0105 }
0106 
0107 bool IconModule::isSaveNeeded() const
0108 {
0109     return !m_model->pendingDeletions().isEmpty();
0110 }
0111 
0112 void IconModule::processPendingDeletions()
0113 {
0114     const QStringList pendingDeletions = m_model->pendingDeletions();
0115 
0116     for (const QString &themeName : pendingDeletions) {
0117         Q_ASSERT(themeName != iconsSettings()->theme());
0118 
0119         KIconTheme theme(themeName);
0120         auto *job = KIO::del(QUrl::fromLocalFile(theme.dir()), KIO::HideProgressInfo);
0121         // needs to block for it to work on "OK" where the dialog (kcmshell) closes
0122         job->exec();
0123     }
0124 
0125     m_model->removeItemsPendingDeletion();
0126 }
0127 
0128 void IconModule::ghnsEntriesChanged()
0129 {
0130     // reload the display icontheme items
0131     KIconTheme::reconfigure();
0132     KIconLoader::global()->newIconLoader();
0133     load();
0134     QPixmapCache::clear();
0135 }
0136 
0137 void IconModule::installThemeFromFile(const QUrl &url)
0138 {
0139     if (url.isLocalFile()) {
0140         installThemeFile(url.toLocalFile());
0141         return;
0142     }
0143 
0144     if (m_tempCopyJob) {
0145         return;
0146     }
0147 
0148     m_tempInstallFile.reset(new QTemporaryFile());
0149     if (!m_tempInstallFile->open()) {
0150         Q_EMIT showErrorMessage(i18n("Unable to create a temporary file."));
0151         m_tempInstallFile.reset();
0152         return;
0153     }
0154 
0155     m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite);
0156     m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
0157     Q_EMIT downloadingFileChanged();
0158 
0159     connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) {
0160         if (job->error() != KJob::NoError) {
0161             Q_EMIT showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText()));
0162             return;
0163         }
0164 
0165         installThemeFile(m_tempInstallFile->fileName());
0166         m_tempInstallFile.reset();
0167     });
0168     connect(m_tempCopyJob, &QObject::destroyed, this, &IconModule::downloadingFileChanged);
0169 }
0170 
0171 void IconModule::installThemeFile(const QString &path)
0172 {
0173     const QStringList themesNames = findThemeDirs(path);
0174     if (themesNames.isEmpty()) {
0175         Q_EMIT showErrorMessage(i18n("The file is not a valid icon theme archive."));
0176         return;
0177     }
0178 
0179     if (!installThemes(themesNames, path)) {
0180         Q_EMIT showErrorMessage(i18n("A problem occurred during the installation process; however, most of the themes in the archive have been installed"));
0181         return;
0182     }
0183 
0184     Q_EMIT showSuccessMessage(i18n("Theme installed successfully."));
0185 
0186     KIconLoader::global()->newIconLoader();
0187     m_model->load();
0188 }
0189 
0190 QStringList IconModule::findThemeDirs(const QString &archiveName)
0191 {
0192     QStringList foundThemes;
0193 
0194     KTar archive(archiveName);
0195     archive.open(QIODevice::ReadOnly);
0196     const KArchiveDirectory *themeDir = archive.directory();
0197 
0198     KArchiveEntry *possibleDir = nullptr;
0199     KArchiveDirectory *subDir = nullptr;
0200 
0201     // iterate all the dirs looking for an index.theme or index.desktop file
0202     const QStringList entries = themeDir->entries();
0203     for (const QString &entry : entries) {
0204         possibleDir = const_cast<KArchiveEntry *>(themeDir->entry(entry));
0205         if (!possibleDir->isDirectory()) {
0206             continue;
0207         }
0208 
0209         subDir = dynamic_cast<KArchiveDirectory *>(possibleDir);
0210         if (!subDir) {
0211             continue;
0212         }
0213 
0214         if (subDir->entry(QStringLiteral("index.theme")) || subDir->entry(QStringLiteral("index.desktop"))) {
0215             foundThemes.append(subDir->name());
0216         }
0217     }
0218 
0219     archive.close();
0220     return foundThemes;
0221 }
0222 
0223 bool IconModule::installThemes(const QStringList &themes, const QString &archiveName)
0224 {
0225     bool everythingOk = true;
0226     const QString localThemesDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/icons/./"));
0227 
0228     Q_EMIT showProgress(i18n("Installing icon themes…"));
0229 
0230     KTar archive(archiveName);
0231     archive.open(QIODevice::ReadOnly);
0232     qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
0233 
0234     const KArchiveDirectory *rootDir = archive.directory();
0235 
0236     KArchiveDirectory *currentTheme = nullptr;
0237     for (const QString &theme : themes) {
0238         Q_EMIT showProgress(i18n("Installing %1 theme…", theme));
0239 
0240         qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
0241 
0242         currentTheme = dynamic_cast<KArchiveDirectory *>(const_cast<KArchiveEntry *>(rootDir->entry(theme)));
0243         if (!currentTheme) {
0244             // we tell back that something went wrong, but try to install as much
0245             // as possible
0246             everythingOk = false;
0247             continue;
0248         }
0249 
0250         currentTheme->copyTo(localThemesDir + theme);
0251     }
0252 
0253     archive.close();
0254 
0255     Q_EMIT hideProgress();
0256     return everythingOk;
0257 }
0258 
0259 QVariantList IconModule::previewIcons(const QString &themeName, int size, qreal dpr, int limit)
0260 {
0261     static QList<QStringList> s_previewIcons{
0262         {QStringLiteral("system-run"), QStringLiteral("exec")},
0263         {QStringLiteral("folder")},
0264         {QStringLiteral("document"), QStringLiteral("text-x-generic")},
0265         {QStringLiteral("user-trash"), QStringLiteral("user-trash-empty")},
0266         {QStringLiteral("help-browser"), QStringLiteral("system-help"), QStringLiteral("help-about"), QStringLiteral("help-contents")},
0267         {QStringLiteral("preferences-system"), QStringLiteral("systemsettings"), QStringLiteral("configure")},
0268 
0269         {QStringLiteral("text-html")},
0270         {QStringLiteral("image-x-generic"), QStringLiteral("image-png"), QStringLiteral("image-jpeg")},
0271         {QStringLiteral("video-x-generic"), QStringLiteral("video-x-theora+ogg"), QStringLiteral("video-mp4")},
0272         {QStringLiteral("x-office-document")},
0273         {QStringLiteral("x-office-spreadsheet")},
0274         {QStringLiteral("x-office-presentation"), QStringLiteral("application-presentation")},
0275 
0276         {QStringLiteral("user-home")},
0277         {QStringLiteral("user-desktop"), QStringLiteral("desktop")},
0278         {QStringLiteral("folder-image"), QStringLiteral("folder-images"), QStringLiteral("folder-pictures"), QStringLiteral("folder-picture")},
0279         {QStringLiteral("folder-documents")},
0280         {QStringLiteral("folder-download"), QStringLiteral("folder-downloads")},
0281         {QStringLiteral("folder-video"), QStringLiteral("folder-videos")}};
0282 
0283     // created on-demand as it is quite expensive to do and we don't want to do it every loop iteration either
0284     std::unique_ptr<KIconTheme> theme;
0285 
0286     QVariantList pixmaps;
0287 
0288     for (const QStringList &iconNames : s_previewIcons) {
0289         const QString cacheKey = themeName + QLatin1Char('@') + QString::number(size) + QLatin1Char('@') + QString::number(dpr, 'f', 1) + QLatin1Char('@')
0290             + iconNames.join(QLatin1Char(','));
0291 
0292         QPixmap pix;
0293         if (!QPixmapCache::find(cacheKey, &pix)) {
0294             if (!theme) {
0295                 theme.reset(new KIconTheme(themeName));
0296             }
0297 
0298             pix = getBestIcon(*theme.get(), iconNames, size, dpr);
0299 
0300             // Inserting a pixmap even if null so we know whether we searched for it already
0301             QPixmapCache::insert(cacheKey, pix);
0302         }
0303 
0304         if (pix.isNull()) {
0305             continue;
0306         }
0307 
0308         pixmaps.append(pix);
0309 
0310         if (limit > -1 && pixmaps.count() >= limit) {
0311             break;
0312         }
0313     }
0314 
0315     return pixmaps;
0316 }
0317 
0318 QPixmap IconModule::getBestIcon(KIconTheme &theme, const QStringList &iconNames, int size, qreal dpr)
0319 {
0320     QSvgRenderer renderer;
0321 
0322     const int iconSize = size * dpr;
0323 
0324     // not using initializer list as we want to unwrap inherits()
0325     const QStringList themes = QStringList() << theme.internalName() << theme.inherits();
0326     for (const QString &themeName : themes) {
0327         KIconTheme theme(themeName);
0328 
0329         for (const QString &iconName : iconNames) {
0330             const QString pixmapPath = theme.iconPath(QStringLiteral("%1.png").arg(iconName), iconSize, KIconLoader::MatchBest);
0331             QPixmap pixmap(pixmapPath);
0332             if (!pixmap.isNull()) {
0333                 pixmap.setDevicePixelRatio(dpr);
0334                 if (pixmap.width() >= iconSize && pixmap.height() >= iconSize) {
0335                     return pixmap;
0336                 }
0337             }
0338 
0339             // could not find the .png, try loading the .svg or .svgz
0340             QString scalablePath = theme.iconPath(QStringLiteral("%1.svg").arg(iconName), iconSize, KIconLoader::MatchBest);
0341             if (scalablePath.isEmpty()) {
0342                 scalablePath = theme.iconPath(QStringLiteral("%1.svgz").arg(iconName), iconSize, KIconLoader::MatchBest);
0343             }
0344 
0345             if (scalablePath.isEmpty()) {
0346                 if (!pixmap.isNull()) {
0347                     return pixmap;
0348                 }
0349 
0350                 continue;
0351             }
0352 
0353             if (!renderer.load(scalablePath)) {
0354                 continue;
0355             }
0356 
0357             QPixmap svgPixmap(iconSize, iconSize);
0358             svgPixmap.setDevicePixelRatio(dpr);
0359             svgPixmap.fill(QColor(Qt::transparent));
0360             QPainter p(&svgPixmap);
0361             p.setViewport(0, 0, size, size);
0362             renderer.render(&p);
0363             return svgPixmap;
0364         }
0365     }
0366 
0367     return QPixmap();
0368 }
0369 
0370 int IconModule::pluginIndex(const QString &themeName) const
0371 {
0372     const auto results = m_model->match(m_model->index(0, 0), ThemeNameRole, themeName, 1, Qt::MatchExactly);
0373     if (results.count() == 1) {
0374         return results.first().row();
0375     }
0376     return -1;
0377 }
0378 
0379 void IconModule::defaults()
0380 {
0381     for (int i = 0, count = m_model->rowCount(QModelIndex()); i < count; ++i) {
0382         m_model->setData(m_model->index(i), false, IconsModel::Roles::PendingDeletionRole);
0383     }
0384     KQuickManagedConfigModule::defaults();
0385 }
0386 
0387 #include "main.moc"