1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtGui module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 #ifndef QT_NO_ICON
40 #include <private/qiconloader_p.h>
41 
42 #include <private/qguiapplication_p.h>
43 #include <private/qicon_p.h>
44 
45 #include <QtGui/QIconEnginePlugin>
46 #include <QtGui/QPixmapCache>
47 #include <qpa/qplatformtheme.h>
48 #include <QtGui/QIconEngine>
49 #include <QtGui/QPalette>
50 #include <QtCore/qmath.h>
51 #include <QtCore/QList>
52 #include <QtCore/QDir>
53 #if QT_CONFIG(settings)
54 #include <QtCore/QSettings>
55 #endif
56 #include <QtGui/QPainter>
57 
58 #include <private/qhexstring_p.h>
59 
60 QT_BEGIN_NAMESPACE
61 
Q_GLOBAL_STATIC(QIconLoader,iconLoaderInstance)62 Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance)
63 
64 /* Theme to use in last resort, if the theme does not have the icon, neither the parents  */
65 static QString systemFallbackThemeName()
66 {
67     if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
68         const QVariant themeHint = theme->themeHint(QPlatformTheme::SystemIconFallbackThemeName);
69         if (themeHint.isValid())
70             return themeHint.toString();
71     }
72     return QString();
73 }
74 
QIconLoader()75 QIconLoader::QIconLoader() :
76         m_themeKey(1), m_supportsSvg(false), m_initialized(false)
77 {
78 }
79 
systemThemeName()80 static inline QString systemThemeName()
81 {
82     const auto override = qgetenv("QT_QPA_SYSTEM_ICON_THEME");
83     if (!override.isEmpty())
84         return QString::fromLocal8Bit(override);
85     if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
86         const QVariant themeHint = theme->themeHint(QPlatformTheme::SystemIconThemeName);
87         if (themeHint.isValid())
88             return themeHint.toString();
89     }
90     return QString();
91 }
92 
systemIconSearchPaths()93 static inline QStringList systemIconSearchPaths()
94 {
95     if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
96         const QVariant themeHint = theme->themeHint(QPlatformTheme::IconThemeSearchPaths);
97         if (themeHint.isValid())
98             return themeHint.toStringList();
99     }
100     return QStringList();
101 }
102 
systemFallbackSearchPaths()103 static inline QStringList systemFallbackSearchPaths()
104 {
105     if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
106         const QVariant themeHint = theme->themeHint(QPlatformTheme::IconFallbackSearchPaths);
107         if (themeHint.isValid())
108             return themeHint.toStringList();
109     }
110     return QStringList();
111 }
112 
113 extern QFactoryLoader *qt_iconEngineFactoryLoader(); // qicon.cpp
114 
ensureInitialized()115 void QIconLoader::ensureInitialized()
116 {
117     if (!m_initialized) {
118         if (!QGuiApplicationPrivate::platformTheme())
119             return; // it's too early: try again later (QTBUG-74252)
120         m_initialized = true;
121         m_systemTheme = systemThemeName();
122 
123         if (m_systemTheme.isEmpty())
124             m_systemTheme = systemFallbackThemeName();
125         if (qt_iconEngineFactoryLoader()->keyMap().key(QLatin1String("svg"), -1) != -1)
126             m_supportsSvg = true;
127     }
128 }
129 
130 /*!
131     \internal
132     Gets an instance.
133 
134     \l QIcon::setFallbackThemeName() should be called before QGuiApplication is
135     created, to avoid a race condition (QTBUG-74252). When this function is
136     called from there, ensureInitialized() does not succeed because there
137     is no QPlatformTheme yet, so systemThemeName() is empty, and we don't want
138     m_systemTheme to get intialized to the fallback theme instead of the normal one.
139 */
instance()140 QIconLoader *QIconLoader::instance()
141 {
142    iconLoaderInstance()->ensureInitialized();
143    return iconLoaderInstance();
144 }
145 
146 // Queries the system theme and invalidates existing
147 // icons if the theme has changed.
updateSystemTheme()148 void QIconLoader::updateSystemTheme()
149 {
150     // Only change if this is not explicitly set by the user
151     if (m_userTheme.isEmpty()) {
152         QString theme = systemThemeName();
153         if (theme.isEmpty())
154             theme = fallbackThemeName();
155         if (theme != m_systemTheme) {
156             m_systemTheme = theme;
157             invalidateKey();
158         }
159     }
160 }
161 
setThemeName(const QString & themeName)162 void QIconLoader::setThemeName(const QString &themeName)
163 {
164     m_userTheme = themeName;
165     invalidateKey();
166 }
167 
fallbackThemeName() const168 QString QIconLoader::fallbackThemeName() const
169 {
170     return m_userFallbackTheme.isEmpty() ? systemFallbackThemeName() : m_userFallbackTheme;
171 }
172 
setFallbackThemeName(const QString & themeName)173 void QIconLoader::setFallbackThemeName(const QString &themeName)
174 {
175     m_userFallbackTheme = themeName;
176 }
177 
setThemeSearchPath(const QStringList & searchPaths)178 void QIconLoader::setThemeSearchPath(const QStringList &searchPaths)
179 {
180     m_iconDirs = searchPaths;
181     themeList.clear();
182     invalidateKey();
183 }
184 
themeSearchPaths() const185 QStringList QIconLoader::themeSearchPaths() const
186 {
187     if (m_iconDirs.isEmpty()) {
188         m_iconDirs = systemIconSearchPaths();
189         // Always add resource directory as search path
190         m_iconDirs.append(QLatin1String(":/icons"));
191     }
192     return m_iconDirs;
193 }
194 
setFallbackSearchPaths(const QStringList & searchPaths)195 void QIconLoader::setFallbackSearchPaths(const QStringList &searchPaths)
196 {
197     m_fallbackDirs = searchPaths;
198     invalidateKey();
199 }
200 
fallbackSearchPaths() const201 QStringList QIconLoader::fallbackSearchPaths() const
202 {
203     if (m_fallbackDirs.isEmpty()) {
204         m_fallbackDirs = systemFallbackSearchPaths();
205     }
206     return m_fallbackDirs;
207 }
208 
209 /*!
210     \internal
211     Helper class that reads and looks up into the icon-theme.cache generated with
212     gtk-update-icon-cache. If at any point we detect a corruption in the file
213     (because the offsets point at wrong locations for example), the reader
214     is marked as invalid.
215 */
216 class QIconCacheGtkReader
217 {
218 public:
219     explicit QIconCacheGtkReader(const QString &themeDir);
220     QVector<const char *> lookup(const QStringRef &);
isValid() const221     bool isValid() const { return m_isValid; }
222 private:
223     QFile m_file;
224     const unsigned char *m_data;
225     quint64 m_size;
226     bool m_isValid;
227 
read16(uint offset)228     quint16 read16(uint offset)
229     {
230         if (offset > m_size - 2 || (offset & 0x1)) {
231             m_isValid = false;
232             return 0;
233         }
234         return m_data[offset+1] | m_data[offset] << 8;
235     }
read32(uint offset)236     quint32 read32(uint offset)
237     {
238         if (offset > m_size - 4 || (offset & 0x3)) {
239             m_isValid = false;
240             return 0;
241         }
242         return m_data[offset+3] | m_data[offset+2] << 8
243             | m_data[offset+1] << 16 | m_data[offset] << 24;
244     }
245 };
246 
247 
QIconCacheGtkReader(const QString & dirName)248 QIconCacheGtkReader::QIconCacheGtkReader(const QString &dirName)
249     : m_isValid(false)
250 {
251     QFileInfo info(dirName + QLatin1String("/icon-theme.cache"));
252     if (!info.exists() || info.lastModified() < QFileInfo(dirName).lastModified())
253         return;
254     m_file.setFileName(info.absoluteFilePath());
255     if (!m_file.open(QFile::ReadOnly))
256         return;
257     m_size = m_file.size();
258     m_data = m_file.map(0, m_size);
259     if (!m_data)
260         return;
261     if (read16(0) != 1) // VERSION_MAJOR
262         return;
263 
264     m_isValid = true;
265 
266     // Check that all the directories are older than the cache
267     auto lastModified = info.lastModified();
268     quint32 dirListOffset = read32(8);
269     quint32 dirListLen = read32(dirListOffset);
270     for (uint i = 0; i < dirListLen; ++i) {
271         quint32 offset = read32(dirListOffset + 4 + 4 * i);
272         if (!m_isValid || offset >= m_size || lastModified < QFileInfo(dirName + QLatin1Char('/')
273                 + QString::fromUtf8(reinterpret_cast<const char*>(m_data + offset))).lastModified()) {
274             m_isValid = false;
275             return;
276         }
277     }
278 }
279 
icon_name_hash(const char * p)280 static quint32 icon_name_hash(const char *p)
281 {
282     quint32 h = static_cast<signed char>(*p);
283     for (p += 1; *p != '\0'; p++)
284         h = (h << 5) - h + *p;
285     return h;
286 }
287 
288 /*! \internal
289     lookup the icon name and return the list of subdirectories in which an icon
290     with this name is present. The char* are pointers to the mapped data.
291     For example, this would return { "32x32/apps", "24x24/apps" , ... }
292  */
lookup(const QStringRef & name)293 QVector<const char *> QIconCacheGtkReader::lookup(const QStringRef &name)
294 {
295     QVector<const char *> ret;
296     if (!isValid() || name.isEmpty())
297         return ret;
298 
299     QByteArray nameUtf8 = name.toUtf8();
300     quint32 hash = icon_name_hash(nameUtf8);
301 
302     quint32 hashOffset = read32(4);
303     quint32 hashBucketCount = read32(hashOffset);
304 
305     if (!isValid() || hashBucketCount == 0) {
306         m_isValid = false;
307         return ret;
308     }
309 
310     quint32 bucketIndex = hash % hashBucketCount;
311     quint32 bucketOffset = read32(hashOffset + 4 + bucketIndex * 4);
312     while (bucketOffset > 0 && bucketOffset <= m_size - 12) {
313         quint32 nameOff = read32(bucketOffset + 4);
314         if (nameOff < m_size && strcmp(reinterpret_cast<const char*>(m_data + nameOff), nameUtf8) == 0) {
315             quint32 dirListOffset = read32(8);
316             quint32 dirListLen = read32(dirListOffset);
317 
318             quint32 listOffset = read32(bucketOffset+8);
319             quint32 listLen = read32(listOffset);
320 
321             if (!m_isValid || listOffset + 4 + 8 * listLen > m_size) {
322                 m_isValid = false;
323                 return ret;
324             }
325 
326             ret.reserve(listLen);
327             for (uint j = 0; j < listLen && m_isValid; ++j) {
328                 quint32 dirIndex = read16(listOffset + 4 + 8 * j);
329                 quint32 o = read32(dirListOffset + 4 + dirIndex*4);
330                 if (!m_isValid || dirIndex >= dirListLen || o >= m_size) {
331                     m_isValid = false;
332                     return ret;
333                 }
334                 ret.append(reinterpret_cast<const char*>(m_data) + o);
335             }
336             return ret;
337         }
338         bucketOffset = read32(bucketOffset);
339     }
340     return ret;
341 }
342 
QIconTheme(const QString & themeName)343 QIconTheme::QIconTheme(const QString &themeName)
344         : m_valid(false)
345 {
346     QFile themeIndex;
347 
348     const QStringList iconDirs = QIcon::themeSearchPaths();
349     for ( int i = 0 ; i < iconDirs.size() ; ++i) {
350         QDir iconDir(iconDirs[i]);
351         QString themeDir = iconDir.path() + QLatin1Char('/') + themeName;
352         QFileInfo themeDirInfo(themeDir);
353 
354         if (themeDirInfo.isDir()) {
355             m_contentDirs << themeDir;
356             m_gtkCaches << QSharedPointer<QIconCacheGtkReader>::create(themeDir);
357         }
358 
359         if (!m_valid) {
360             themeIndex.setFileName(themeDir + QLatin1String("/index.theme"));
361             if (themeIndex.exists())
362                 m_valid = true;
363         }
364     }
365 #if QT_CONFIG(settings)
366     if (themeIndex.exists()) {
367         const QSettings indexReader(themeIndex.fileName(), QSettings::IniFormat);
368         const QStringList keys = indexReader.allKeys();
369         for (const QString &key : keys) {
370             if (key.endsWith(QLatin1String("/Size"))) {
371                 // Note the QSettings ini-format does not accept
372                 // slashes in key names, hence we have to cheat
373                 if (int size = indexReader.value(key).toInt()) {
374                     QString directoryKey = key.left(key.size() - 5);
375                     QIconDirInfo dirInfo(directoryKey);
376                     dirInfo.size = size;
377                     QString type = indexReader.value(directoryKey +
378                                                      QLatin1String("/Type")
379                                                      ).toString();
380 
381                     if (type == QLatin1String("Fixed"))
382                         dirInfo.type = QIconDirInfo::Fixed;
383                     else if (type == QLatin1String("Scalable"))
384                         dirInfo.type = QIconDirInfo::Scalable;
385                     else
386                         dirInfo.type = QIconDirInfo::Threshold;
387 
388                     dirInfo.threshold = indexReader.value(directoryKey +
389                                                         QLatin1String("/Threshold"),
390                                                         2).toInt();
391 
392                     dirInfo.minSize = indexReader.value(directoryKey +
393                                                          QLatin1String("/MinSize"),
394                                                          size).toInt();
395 
396                     dirInfo.maxSize = indexReader.value(directoryKey +
397                                                         QLatin1String("/MaxSize"),
398                                                         size).toInt();
399 
400                     dirInfo.scale = indexReader.value(directoryKey +
401                                                       QLatin1String("/Scale"),
402                                                       1).toInt();
403                     m_keyList.append(dirInfo);
404                 }
405             }
406         }
407 
408         // Parent themes provide fallbacks for missing icons
409         m_parents = indexReader.value(
410                 QLatin1String("Icon Theme/Inherits")).toStringList();
411         m_parents.removeAll(QString());
412 
413         // Ensure a default platform fallback for all themes
414         if (m_parents.isEmpty()) {
415             const QString fallback = QIconLoader::instance()->fallbackThemeName();
416             if (!fallback.isEmpty())
417                 m_parents.append(fallback);
418         }
419 
420         // Ensure that all themes fall back to hicolor
421         if (!m_parents.contains(QLatin1String("hicolor")))
422             m_parents.append(QLatin1String("hicolor"));
423     }
424 #endif // settings
425 }
426 
findIconHelper(const QString & themeName,const QString & iconName,QStringList & visited) const427 QThemeIconInfo QIconLoader::findIconHelper(const QString &themeName,
428                                            const QString &iconName,
429                                            QStringList &visited) const
430 {
431     QThemeIconInfo info;
432     Q_ASSERT(!themeName.isEmpty());
433 
434     // Used to protect against potential recursions
435     visited << themeName;
436 
437     QIconTheme &theme = themeList[themeName];
438     if (!theme.isValid()) {
439         theme = QIconTheme(themeName);
440         if (!theme.isValid())
441             theme = QIconTheme(fallbackThemeName());
442     }
443 
444     const QStringList contentDirs = theme.contentDirs();
445 
446     QStringRef iconNameFallback(&iconName);
447 
448     // Iterate through all icon's fallbacks in current theme
449     while (info.entries.isEmpty()) {
450         const QString svgIconName = iconNameFallback + QLatin1String(".svg");
451         const QString pngIconName = iconNameFallback + QLatin1String(".png");
452 
453         // Add all relevant files
454         for (int i = 0; i < contentDirs.size(); ++i) {
455             QVector<QIconDirInfo> subDirs = theme.keyList();
456 
457             // Try to reduce the amount of subDirs by looking in the GTK+ cache in order to save
458             // a massive amount of file stat (especially if the icon is not there)
459             auto cache = theme.m_gtkCaches.at(i);
460             if (cache->isValid()) {
461                 const auto result = cache->lookup(iconNameFallback);
462                 if (cache->isValid()) {
463                     const QVector<QIconDirInfo> subDirsCopy = subDirs;
464                     subDirs.clear();
465                     subDirs.reserve(result.count());
466                     for (const char *s : result) {
467                         QString path = QString::fromUtf8(s);
468                         auto it = std::find_if(subDirsCopy.cbegin(), subDirsCopy.cend(),
469                                                [&](const QIconDirInfo &info) {
470                                                    return info.path == path; } );
471                         if (it != subDirsCopy.cend()) {
472                             subDirs.append(*it);
473                         }
474                     }
475                 }
476             }
477 
478             QString contentDir = contentDirs.at(i) + QLatin1Char('/');
479             for (int j = 0; j < subDirs.size() ; ++j) {
480                 const QIconDirInfo &dirInfo = subDirs.at(j);
481                 const QString subDir = contentDir + dirInfo.path + QLatin1Char('/');
482                 const QString pngPath = subDir + pngIconName;
483                 if (QFile::exists(pngPath)) {
484                     PixmapEntry *iconEntry = new PixmapEntry;
485                     iconEntry->dir = dirInfo;
486                     iconEntry->filename = pngPath;
487                     // Notice we ensure that pixmap entries always come before
488                     // scalable to preserve search order afterwards
489                     info.entries.prepend(iconEntry);
490                 } else if (m_supportsSvg) {
491                     const QString svgPath = subDir + svgIconName;
492                     if (QFile::exists(svgPath)) {
493                         ScalableEntry *iconEntry = new ScalableEntry;
494                         iconEntry->dir = dirInfo;
495                         iconEntry->filename = svgPath;
496                         info.entries.append(iconEntry);
497                     }
498                 }
499             }
500         }
501 
502         if (!info.entries.isEmpty()) {
503             info.iconName = iconNameFallback.toString();
504             break;
505         }
506 
507         // If it's possible - find next fallback for the icon
508         const int indexOfDash = iconNameFallback.lastIndexOf(QLatin1Char('-'));
509         if (indexOfDash == -1)
510             break;
511 
512         iconNameFallback.truncate(indexOfDash);
513     }
514 
515     if (info.entries.isEmpty()) {
516         const QStringList parents = theme.parents();
517         // Search recursively through inherited themes
518         for (int i = 0 ; i < parents.size() ; ++i) {
519 
520             const QString parentTheme = parents.at(i).trimmed();
521 
522             if (!visited.contains(parentTheme)) // guard against recursion
523                 info = findIconHelper(parentTheme, iconName, visited);
524 
525             if (!info.entries.isEmpty()) // success
526                 break;
527         }
528     }
529     return info;
530 }
531 
lookupFallbackIcon(const QString & iconName) const532 QThemeIconInfo QIconLoader::lookupFallbackIcon(const QString &iconName) const
533 {
534     QThemeIconInfo info;
535 
536     const QString pngIconName = iconName + QLatin1String(".png");
537     const QString xpmIconName = iconName + QLatin1String(".xpm");
538     const QString svgIconName = iconName + QLatin1String(".svg");
539 
540     const auto searchPaths = QIcon::fallbackSearchPaths();
541     for (const QString &iconDir: searchPaths) {
542         QDir currentDir(iconDir);
543         if (currentDir.exists(pngIconName)) {
544             PixmapEntry *iconEntry = new PixmapEntry;
545             iconEntry->dir.type = QIconDirInfo::Fallback;
546             iconEntry->filename = currentDir.filePath(pngIconName);
547             info.entries.append(iconEntry);
548             break;
549         } else if (currentDir.exists(xpmIconName)) {
550             PixmapEntry *iconEntry = new PixmapEntry;
551             iconEntry->dir.type = QIconDirInfo::Fallback;
552             iconEntry->filename = currentDir.filePath(xpmIconName);
553             info.entries.append(iconEntry);
554             break;
555         } else if (m_supportsSvg &&
556                    currentDir.exists(svgIconName)) {
557             ScalableEntry *iconEntry = new ScalableEntry;
558             iconEntry->dir.type = QIconDirInfo::Fallback;
559             iconEntry->filename = currentDir.filePath(svgIconName);
560             info.entries.append(iconEntry);
561             break;
562         }
563     }
564 
565     if (!info.entries.isEmpty())
566         info.iconName = iconName;
567 
568     return info;
569 }
570 
loadIcon(const QString & name) const571 QThemeIconInfo QIconLoader::loadIcon(const QString &name) const
572 {
573     if (!themeName().isEmpty()) {
574         QStringList visited;
575         const QThemeIconInfo iconInfo = findIconHelper(themeName(), name, visited);
576         if (!iconInfo.entries.isEmpty())
577             return iconInfo;
578 
579         return lookupFallbackIcon(name);
580     }
581 
582     return QThemeIconInfo();
583 }
584 
585 
586 // -------- Icon Loader Engine -------- //
587 
588 
QIconLoaderEngine(const QString & iconName)589 QIconLoaderEngine::QIconLoaderEngine(const QString& iconName)
590         : m_iconName(iconName), m_key(0)
591 {
592 }
593 
~QIconLoaderEngine()594 QIconLoaderEngine::~QIconLoaderEngine()
595 {
596     qDeleteAll(m_info.entries);
597 }
598 
QIconLoaderEngine(const QIconLoaderEngine & other)599 QIconLoaderEngine::QIconLoaderEngine(const QIconLoaderEngine &other)
600         : QIconEngine(other),
601         m_iconName(other.m_iconName),
602         m_key(0)
603 {
604 }
605 
clone() const606 QIconEngine *QIconLoaderEngine::clone() const
607 {
608     return new QIconLoaderEngine(*this);
609 }
610 
read(QDataStream & in)611 bool QIconLoaderEngine::read(QDataStream &in) {
612     in >> m_iconName;
613     return true;
614 }
615 
write(QDataStream & out) const616 bool QIconLoaderEngine::write(QDataStream &out) const
617 {
618     out << m_iconName;
619     return true;
620 }
621 
hasIcon() const622 bool QIconLoaderEngine::hasIcon() const
623 {
624     return !(m_info.entries.isEmpty());
625 }
626 
627 // Lazily load the icon
ensureLoaded()628 void QIconLoaderEngine::ensureLoaded()
629 {
630     if (!(QIconLoader::instance()->themeKey() == m_key)) {
631         qDeleteAll(m_info.entries);
632         m_info.entries.clear();
633         m_info.iconName.clear();
634 
635         Q_ASSERT(m_info.entries.size() == 0);
636         m_info = QIconLoader::instance()->loadIcon(m_iconName);
637         m_key = QIconLoader::instance()->themeKey();
638     }
639 }
640 
paint(QPainter * painter,const QRect & rect,QIcon::Mode mode,QIcon::State state)641 void QIconLoaderEngine::paint(QPainter *painter, const QRect &rect,
642                              QIcon::Mode mode, QIcon::State state)
643 {
644     const qreal dpr = !qApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ?
645                 qreal(1.0) : painter->device()->devicePixelRatioF();
646 
647     QSize pixmapSize = rect.size() * dpr;
648     painter->drawPixmap(rect, pixmap(pixmapSize, mode, state));
649 }
650 
651 /*
652  * This algorithm is defined by the freedesktop spec:
653  * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
654  */
directoryMatchesSize(const QIconDirInfo & dir,int iconsize,int iconscale)655 static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize, int iconscale)
656 {
657     if (dir.scale != iconscale)
658         return false;
659 
660     if (dir.type == QIconDirInfo::Fixed) {
661         return dir.size == iconsize;
662 
663     } else if (dir.type == QIconDirInfo::Scalable) {
664         return iconsize <= dir.maxSize &&
665                 iconsize >= dir.minSize;
666 
667     } else if (dir.type == QIconDirInfo::Threshold) {
668         return iconsize >= dir.size - dir.threshold &&
669                 iconsize <= dir.size + dir.threshold;
670     } else if (dir.type == QIconDirInfo::Fallback) {
671         return true;
672     }
673 
674     Q_ASSERT(1); // Not a valid value
675     return false;
676 }
677 
678 /*
679  * This algorithm is defined by the freedesktop spec:
680  * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
681  */
directorySizeDistance(const QIconDirInfo & dir,int iconsize,int iconscale)682 static int directorySizeDistance(const QIconDirInfo &dir, int iconsize, int iconscale)
683 {
684     const int scaledIconSize = iconsize * iconscale;
685     if (dir.type == QIconDirInfo::Fixed) {
686         return qAbs(dir.size * dir.scale - scaledIconSize);
687 
688     } else if (dir.type == QIconDirInfo::Scalable) {
689         if (scaledIconSize < dir.minSize * dir.scale)
690             return dir.minSize * dir.scale - scaledIconSize;
691         else if (scaledIconSize > dir.maxSize * dir.scale)
692             return scaledIconSize - dir.maxSize * dir.scale;
693         else
694             return 0;
695 
696     } else if (dir.type == QIconDirInfo::Threshold) {
697         if (scaledIconSize < (dir.size - dir.threshold) * dir.scale)
698             return dir.minSize * dir.scale - scaledIconSize;
699         else if (scaledIconSize > (dir.size + dir.threshold) * dir.scale)
700             return scaledIconSize - dir.maxSize * dir.scale;
701         else return 0;
702     } else if (dir.type == QIconDirInfo::Fallback) {
703         return 0;
704     }
705 
706     Q_ASSERT(1); // Not a valid value
707     return INT_MAX;
708 }
709 
entryForSize(const QThemeIconInfo & info,const QSize & size,int scale)710 QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QThemeIconInfo &info, const QSize &size, int scale)
711 {
712     int iconsize = qMin(size.width(), size.height());
713 
714     // Note that m_info.entries are sorted so that png-files
715     // come first
716 
717     const int numEntries = info.entries.size();
718 
719     // Search for exact matches first
720     for (int i = 0; i < numEntries; ++i) {
721         QIconLoaderEngineEntry *entry = info.entries.at(i);
722         if (directoryMatchesSize(entry->dir, iconsize, scale)) {
723             return entry;
724         }
725     }
726 
727     // Find the minimum distance icon
728     int minimalSize = INT_MAX;
729     QIconLoaderEngineEntry *closestMatch = nullptr;
730     for (int i = 0; i < numEntries; ++i) {
731         QIconLoaderEngineEntry *entry = info.entries.at(i);
732         int distance = directorySizeDistance(entry->dir, iconsize, scale);
733         if (distance < minimalSize) {
734             minimalSize  = distance;
735             closestMatch = entry;
736         }
737     }
738     return closestMatch;
739 }
740 
741 /*
742  * Returns the actual icon size. For scalable svg's this is equivalent
743  * to the requested size. Otherwise the closest match is returned but
744  * we can never return a bigger size than the requested size.
745  *
746  */
actualSize(const QSize & size,QIcon::Mode mode,QIcon::State state)747 QSize QIconLoaderEngine::actualSize(const QSize &size, QIcon::Mode mode,
748                                    QIcon::State state)
749 {
750     Q_UNUSED(mode);
751     Q_UNUSED(state);
752 
753     ensureLoaded();
754 
755     QIconLoaderEngineEntry *entry = entryForSize(m_info, size);
756     if (entry) {
757         const QIconDirInfo &dir = entry->dir;
758         if (dir.type == QIconDirInfo::Scalable) {
759             return size;
760         } else if (dir.type == QIconDirInfo::Fallback) {
761             return QIcon(entry->filename).actualSize(size, mode, state);
762         } else {
763             int result = qMin<int>(dir.size, qMin(size.width(), size.height()));
764             return QSize(result, result);
765         }
766     }
767     return QSize(0, 0);
768 }
769 
pixmap(const QSize & size,QIcon::Mode mode,QIcon::State state)770 QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
771 {
772     Q_UNUSED(state);
773 
774     // Ensure that basePixmap is lazily initialized before generating the
775     // key, otherwise the cache key is not unique
776     if (basePixmap.isNull())
777         basePixmap.load(filename);
778 
779     QSize actualSize = basePixmap.size();
780     // If the size of the best match we have (basePixmap) is larger than the
781     // requested size, we downscale it to match.
782     if (!actualSize.isNull() && (actualSize.width() > size.width() || actualSize.height() > size.height()))
783         actualSize.scale(size, Qt::KeepAspectRatio);
784 
785     QString key = QLatin1String("$qt_theme_")
786                   % HexString<qint64>(basePixmap.cacheKey())
787                   % HexString<int>(mode)
788                   % HexString<qint64>(QGuiApplication::palette().cacheKey())
789                   % HexString<int>(actualSize.width())
790                   % HexString<int>(actualSize.height());
791 
792     QPixmap cachedPixmap;
793     if (QPixmapCache::find(key, &cachedPixmap)) {
794         return cachedPixmap;
795     } else {
796         if (basePixmap.size() != actualSize)
797             cachedPixmap = basePixmap.scaled(actualSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
798         else
799             cachedPixmap = basePixmap;
800         if (QGuiApplication *guiApp = qobject_cast<QGuiApplication *>(qApp))
801             cachedPixmap = static_cast<QGuiApplicationPrivate*>(QObjectPrivate::get(guiApp))->applyQIconStyleHelper(mode, cachedPixmap);
802         QPixmapCache::insert(key, cachedPixmap);
803     }
804     return cachedPixmap;
805 }
806 
pixmap(const QSize & size,QIcon::Mode mode,QIcon::State state)807 QPixmap ScalableEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
808 {
809     if (svgIcon.isNull())
810         svgIcon = QIcon(filename);
811 
812     // Bypass QIcon API, as that will scale by device pixel ratio of the
813     // highest DPR screen since we're not passing on any QWindow.
814     if (QIconEngine *engine = svgIcon.data_ptr() ? svgIcon.data_ptr()->engine : nullptr)
815         return engine->pixmap(size, mode, state);
816 
817     return QPixmap();
818 }
819 
pixmap(const QSize & size,QIcon::Mode mode,QIcon::State state)820 QPixmap QIconLoaderEngine::pixmap(const QSize &size, QIcon::Mode mode,
821                                  QIcon::State state)
822 {
823     ensureLoaded();
824 
825     QIconLoaderEngineEntry *entry = entryForSize(m_info, size);
826     if (entry)
827         return entry->pixmap(size, mode, state);
828 
829     return QPixmap();
830 }
831 
key() const832 QString QIconLoaderEngine::key() const
833 {
834     return QLatin1String("QIconLoaderEngine");
835 }
836 
virtual_hook(int id,void * data)837 void QIconLoaderEngine::virtual_hook(int id, void *data)
838 {
839     ensureLoaded();
840 
841     switch (id) {
842     case QIconEngine::AvailableSizesHook:
843         {
844             QIconEngine::AvailableSizesArgument &arg
845                     = *reinterpret_cast<QIconEngine::AvailableSizesArgument*>(data);
846             const int N = m_info.entries.size();
847             QList<QSize> sizes;
848             sizes.reserve(N);
849 
850             // Gets all sizes from the DirectoryInfo entries
851             for (int i = 0; i < N; ++i) {
852                 const QIconLoaderEngineEntry *entry = m_info.entries.at(i);
853                 if (entry->dir.type == QIconDirInfo::Fallback) {
854                     sizes.append(QIcon(entry->filename).availableSizes());
855                 } else {
856                     int size = entry->dir.size;
857                     sizes.append(QSize(size, size));
858                 }
859             }
860             arg.sizes.swap(sizes); // commit
861         }
862         break;
863     case QIconEngine::IconNameHook:
864         {
865             QString &name = *reinterpret_cast<QString*>(data);
866             name = m_info.iconName;
867         }
868         break;
869     case QIconEngine::IsNullHook:
870         {
871             *reinterpret_cast<bool*>(data) = m_info.entries.isEmpty();
872         }
873         break;
874     case QIconEngine::ScaledPixmapHook:
875         {
876             QIconEngine::ScaledPixmapArgument &arg = *reinterpret_cast<QIconEngine::ScaledPixmapArgument*>(data);
877             // QIcon::pixmap() multiplies size by the device pixel ratio.
878             const int integerScale = qCeil(arg.scale);
879             QIconLoaderEngineEntry *entry = entryForSize(m_info, arg.size / integerScale, integerScale);
880             arg.pixmap = entry ? entry->pixmap(arg.size, arg.mode, arg.state) : QPixmap();
881         }
882         break;
883     default:
884         QIconEngine::virtual_hook(id, data);
885     }
886 }
887 
888 QT_END_NAMESPACE
889 
890 #endif //QT_NO_ICON
891