Warning, file /plasma/plasma-workspace/kcms/icons/main.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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