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 }