File indexing completed on 2024-04-14 15:52:15

0001 /*
0002     SPDX-FileCopyrightText: 2018-2022 Nikita Melnichenko <nikita+kde@melnichenko.name>
0003     SPDX-FileCopyrightText: 2018-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "icon.h"
0009 
0010 #include "krglobal.h"
0011 
0012 // QtCore
0013 #include <QCache>
0014 #include <QDebug>
0015 #include <QDir>
0016 #include <QPair>
0017 // QtGui
0018 #include <QPainter>
0019 #include <QPalette>
0020 #include <QPixmap>
0021 
0022 #include <KConfigCore/KSharedConfig>
0023 #include <KIconThemes/KIconLoader>
0024 #include <utility>
0025 
0026 static const int cacheSize = 500;
0027 static const char *missingIconPath = ":/icons/icon-missing.svgz";
0028 
0029 static inline QStringList getThemeFallbackList()
0030 {
0031     QStringList themes;
0032 
0033     // add user fallback theme if set
0034     if (krConfig) {
0035         const KConfigGroup group(krConfig, QStringLiteral("Startup"));
0036         QString userFallbackTheme = group.readEntry("Fallback Icon Theme", QString());
0037         if (!userFallbackTheme.isEmpty()) {
0038             themes << userFallbackTheme;
0039         }
0040     }
0041 
0042     // Breeze and Oxygen are weak dependencies of Krusader,
0043     // i.e. each of the themes provide a complete set of icons used in the interface
0044     const QString breeze(Icon::isLightWindowThemeActive() ? "breeze" : "breeze-dark");
0045     themes << breeze << "oxygen";
0046 
0047     return themes;
0048 }
0049 
0050 class IconEngine : public QIconEngine
0051 {
0052 public:
0053     IconEngine(QString iconName, QIcon fallbackIcon, QStringList overlays = QStringList())
0054         : _iconName(std::move(iconName))
0055         , _fallbackIcon(std::move(fallbackIcon))
0056         , _overlays(std::move(overlays))
0057     {
0058         _themeFallbackList = getThemeFallbackList();
0059     }
0060 
0061     void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override;
0062     QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override;
0063 
0064     IconEngine *clone() const override
0065     {
0066         return new IconEngine(*this);
0067     }
0068 
0069 private:
0070     QString _iconName;
0071     QStringList _themeFallbackList;
0072     QIcon _fallbackIcon;
0073     QStringList _overlays;
0074 };
0075 
0076 Icon::Icon()
0077 {
0078 }
0079 
0080 Icon::Icon(QString name, QStringList overlays)
0081     : QIcon(new IconEngine(std::move(name), QIcon(missingIconPath), std::move(overlays)))
0082 {
0083 }
0084 
0085 Icon::Icon(QString name, QIcon fallbackIcon, QStringList overlays)
0086     : QIcon(new IconEngine(std::move(name), std::move(fallbackIcon), std::move(overlays)))
0087 {
0088 }
0089 
0090 struct IconSearchResult {
0091     QIcon icon; ///< icon returned by search; null icon if not found
0092     QString originalThemeName; ///< original theme name if theme is modified by search
0093 
0094     IconSearchResult(QIcon icon, QString originalThemeName)
0095         : icon(std::move(icon))
0096         , originalThemeName(std::move(originalThemeName))
0097     {
0098     }
0099 };
0100 
0101 // Search icon in the configured themes.
0102 // If this call modifies active theme, the original theme name will be specified in the result.
0103 static inline IconSearchResult searchIcon(const QString &iconName, QStringList themeFallbackList)
0104 {
0105     if (QDir::isAbsolutePath(iconName)) {
0106         // a path is used - directly load the icon
0107         return IconSearchResult(QIcon(iconName), QString());
0108     } else if (QIcon::hasThemeIcon(iconName)) {
0109         // current theme has the icon - load seamlessly
0110         return IconSearchResult(QIcon::fromTheme(iconName), QString());
0111     } else if (KIconLoader::global()->hasIcon(iconName)) {
0112         // KF icon loader does a wider search and helps with mime icons
0113         return IconSearchResult(KDE::icon(iconName), QString());
0114     } else {
0115         // search the icon in fallback themes
0116         auto currentTheme = QIcon::themeName();
0117         for (const auto &fallbackThemeName : themeFallbackList) {
0118             QIcon::setThemeName(fallbackThemeName);
0119             if (QIcon::hasThemeIcon(iconName)) {
0120                 return IconSearchResult(QIcon::fromTheme(iconName), currentTheme);
0121             }
0122         }
0123         QIcon::setThemeName(currentTheme);
0124 
0125         // not found
0126         return IconSearchResult(QIcon(), QString());
0127     }
0128 }
0129 
0130 bool Icon::exists(const QString &iconName)
0131 {
0132     static QCache<QString, bool> cache(cacheSize);
0133     static QString cachedTheme;
0134 
0135     // invalidate cache if system theme is changed
0136     if (cachedTheme != QIcon::themeName()) {
0137         cache.clear();
0138         cachedTheme = QIcon::themeName();
0139     }
0140 
0141     // return cached result when possible
0142     if (cache.contains(iconName)) {
0143         return *cache.object(iconName);
0144     }
0145 
0146     auto searchResult = searchIcon(iconName, getThemeFallbackList());
0147     if (!searchResult.originalThemeName.isNull()) {
0148         QIcon::setThemeName(searchResult.originalThemeName);
0149     }
0150 
0151     auto *result = new bool(!searchResult.icon.isNull());
0152 
0153     // update the cache; the cache takes ownership over the result
0154     cache.insert(iconName, result);
0155 
0156     return *result;
0157 }
0158 
0159 void Icon::applyOverlays(QPixmap *pixmap, QStringList overlays)
0160 {
0161     auto iconLoader = KIconLoader::global();
0162 
0163     // Since KIconLoader loadIcon is not virtual method, we can't redefine loadIcon
0164     // that is called by drawOverlays. The best we can do is to go over the overlays
0165     // and ensure they exist from the icon loader point of view.
0166     // If not, we replace the overlay with "emblem-unreadable" which should be available
0167     // per freedesktop icon name specification:
0168     // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
0169     QStringList fixedOverlays;
0170     for (const auto &overlay : overlays) {
0171         if (overlay.isEmpty() || iconLoader->hasIcon(overlay)) {
0172             fixedOverlays << overlay;
0173         } else {
0174             fixedOverlays << "emblem-unreadable";
0175         }
0176     }
0177 
0178     iconLoader->drawOverlays(fixedOverlays, *pixmap, KIconLoader::Desktop);
0179 }
0180 
0181 bool Icon::isLightWindowThemeActive()
0182 {
0183     const QColor textColor = QPalette().brush(QPalette::Text).color();
0184     return (textColor.red() + textColor.green() + textColor.blue()) / 3 < 128;
0185 }
0186 
0187 class IconCacheKey
0188 {
0189 public:
0190     IconCacheKey(const QString &name, const QStringList &overlays, const QSize &size, QIcon::Mode mode, QIcon::State state)
0191         : name(name)
0192         , overlays(overlays)
0193         , size(size)
0194         , mode(mode)
0195         , state(state)
0196     {
0197         auto repr = QString("%1 [%2] %3x%4 %5 %6").arg(name).arg(overlays.join(';')).arg(size.width()).arg(size.height()).arg((int)mode).arg((int)state);
0198         _hash = qHash(repr);
0199     }
0200 
0201     bool operator==(const IconCacheKey &x) const
0202     {
0203         return name == x.name && overlays == x.overlays && size == x.size && mode == x.mode && state == x.state;
0204     }
0205 
0206     uint hash() const
0207     {
0208         return _hash;
0209     }
0210 
0211     QString name;
0212     QStringList overlays;
0213     QSize size;
0214     QIcon::Mode mode;
0215     QIcon::State state;
0216 
0217 private:
0218     uint _hash;
0219 };
0220 
0221 uint qHash(const IconCacheKey &key) noexcept
0222 {
0223     return key.hash();
0224 }
0225 
0226 QPixmap IconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
0227 {
0228     static QCache<IconCacheKey, QPixmap> cache(cacheSize);
0229     static QString cachedTheme;
0230 
0231     QString systemTheme = QIcon::themeName();
0232 
0233     // [WORKAROUND] If system theme is Breeze, pick light or dark variant of the theme explicitly
0234     // This type of selection works implicitly when QIcon::fromTheme is used,
0235     // however after QIcon::setThemeName it stops working for unknown reason.
0236     if (systemTheme == "breeze" || systemTheme == "breeze-dark") {
0237         const QString pickedSystemTheme(Icon::isLightWindowThemeActive() ? "breeze" : "breeze-dark");
0238         if (systemTheme != pickedSystemTheme) {
0239             qDebug() << "System icon theme variant changed:" << systemTheme << "->" << pickedSystemTheme;
0240             systemTheme = pickedSystemTheme;
0241             QIcon::setThemeName(systemTheme);
0242         }
0243     }
0244 
0245     // invalidate cache if system theme is changed
0246     if (cachedTheme != systemTheme) {
0247         if (!cachedTheme.isEmpty()) {
0248             qDebug() << "System icon theme changed:" << cachedTheme << "->" << systemTheme;
0249         }
0250 
0251         cache.clear();
0252         cachedTheme = systemTheme;
0253     }
0254 
0255     // an empty icon name is a special case - we don't apply any fallback
0256     if (_iconName.isEmpty()) {
0257         return QPixmap();
0258     }
0259 
0260     auto key = IconCacheKey(_iconName, _overlays, size, mode, state);
0261 
0262     // return cached icon when possible
0263     if (cache.contains(key)) {
0264         return *cache.object(key);
0265     }
0266 
0267     // search icon and extract pixmap
0268     auto pixmap = new QPixmap;
0269     auto searchResult = searchIcon(_iconName, _themeFallbackList);
0270     if (!searchResult.icon.isNull()) {
0271         *pixmap = searchResult.icon.pixmap(size, mode, state);
0272     }
0273     if (!searchResult.originalThemeName.isNull()) {
0274         QIcon::setThemeName(searchResult.originalThemeName);
0275     }
0276 
0277     // can't find the icon neither in system theme nor in fallback themes - load fallback icon
0278     if (pixmap->isNull()) {
0279         qWarning() << "Unable to find icon" << _iconName << "of size" << size << "in any configured theme";
0280         *pixmap = _fallbackIcon.pixmap(size, mode, state);
0281     }
0282 
0283     // apply overlays in a safe manner
0284     Icon::applyOverlays(pixmap, _overlays);
0285 
0286     // update the cache; the cache takes ownership over the pixmap
0287     cache.insert(key, pixmap);
0288 
0289     return *pixmap;
0290 }
0291 
0292 void IconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state)
0293 {
0294     QSize pixmapSize = rect.size();
0295     QPixmap px = pixmap(pixmapSize, mode, state);
0296     painter->drawPixmap(rect, px);
0297 }