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