1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3#ifndef QT_NO_ICON
4#include <private/qiconloader_p.h>
5
6#include <private/qguiapplication_p.h>
7#include <private/qicon_p.h>
8
9#include <QtGui/QIconEnginePlugin>
10#include <QtGui/QPixmapCache>
11#include <qpa/qplatformtheme.h>
12#include <QtGui/QIconEngine>
13#include <QtGui/QPalette>
14#include <QtCore/qmath.h>
15#include <QtCore/QList>
16#include <QtCore/QDir>
17#include <QtCore/qloggingcategory.h>
18#if QT_CONFIG(settings)
19#include <QtCore/QSettings>
20#endif
21#include <QtGui/QPainter>
22
23#include <private/qhexstring_p.h>
24
25QT_BEGIN_NAMESPACE
26
27Q_LOGGING_CATEGORY(lcIconLoader, "qt.gui.icon.loader")
28
29using namespace Qt::StringLiterals;
30
31Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance)
32
33/* Theme to use in last resort, if the theme does not have the icon, neither the parents */
34static QString systemFallbackThemeName()
35{
36 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
37 const QVariant themeHint = theme->themeHint(hint: QPlatformTheme::SystemIconFallbackThemeName);
38 if (themeHint.isValid())
39 return themeHint.toString();
40 }
41 return QString();
42}
43
44QIconLoader::QIconLoader() :
45 m_themeKey(1), m_supportsSvg(false), m_initialized(false)
46{
47}
48
49static inline QString systemThemeName()
50{
51 const auto override = qgetenv(varName: "QT_QPA_SYSTEM_ICON_THEME");
52 if (!override.isEmpty())
53 return QString::fromLocal8Bit(ba: override);
54 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
55 const QVariant themeHint = theme->themeHint(hint: QPlatformTheme::SystemIconThemeName);
56 if (themeHint.isValid())
57 return themeHint.toString();
58 }
59 return QString();
60}
61
62static inline QStringList systemIconSearchPaths()
63{
64 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
65 const QVariant themeHint = theme->themeHint(hint: QPlatformTheme::IconThemeSearchPaths);
66 if (themeHint.isValid())
67 return themeHint.toStringList();
68 }
69 return QStringList();
70}
71
72static inline QStringList systemFallbackSearchPaths()
73{
74 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
75 const QVariant themeHint = theme->themeHint(hint: QPlatformTheme::IconFallbackSearchPaths);
76 if (themeHint.isValid())
77 return themeHint.toStringList();
78 }
79 return QStringList();
80}
81
82extern QFactoryLoader *qt_iconEngineFactoryLoader(); // qicon.cpp
83
84void QIconLoader::ensureInitialized()
85{
86 if (!m_initialized) {
87 if (!QGuiApplicationPrivate::platformTheme())
88 return; // it's too early: try again later (QTBUG-74252)
89 m_initialized = true;
90 m_systemTheme = systemThemeName();
91
92 if (m_systemTheme.isEmpty())
93 m_systemTheme = systemFallbackThemeName();
94 if (qt_iconEngineFactoryLoader()->keyMap().key(value: "svg"_L1, defaultKey: -1) != -1)
95 m_supportsSvg = true;
96
97 qCDebug(lcIconLoader) << "Initialized icon loader with system theme"
98 << m_systemTheme << "and SVG support" << m_supportsSvg;
99 }
100}
101
102/*!
103 \internal
104 Gets an instance.
105
106 \l QIcon::setFallbackThemeName() should be called before QGuiApplication is
107 created, to avoid a race condition (QTBUG-74252). When this function is
108 called from there, ensureInitialized() does not succeed because there
109 is no QPlatformTheme yet, so systemThemeName() is empty, and we don't want
110 m_systemTheme to get initialized to the fallback theme instead of the normal one.
111*/
112QIconLoader *QIconLoader::instance()
113{
114 iconLoaderInstance()->ensureInitialized();
115 return iconLoaderInstance();
116}
117
118// Queries the system theme and invalidates existing
119// icons if the theme has changed.
120void QIconLoader::updateSystemTheme()
121{
122 const QString currentSystemTheme = m_systemTheme;
123 m_systemTheme = systemThemeName();
124 if (m_systemTheme.isEmpty())
125 m_systemTheme = systemFallbackThemeName();
126 if (m_systemTheme != currentSystemTheme)
127 qCDebug(lcIconLoader) << "Updated system theme to" << m_systemTheme;
128 // Invalidate even if the system theme name hasn't changed, as the
129 // theme itself may have changed its underlying icon lookup logic.
130 if (!hasUserTheme())
131 invalidateKey();
132}
133
134void QIconLoader::invalidateKey()
135{
136 // Invalidating the key here will result in QThemeIconEngine
137 // recreating the actual engine the next time the icon is used.
138 // We don't need to clear the QIcon cache itself.
139 m_themeKey++;
140}
141
142QString QIconLoader::themeName() const
143{
144 return m_userTheme.isEmpty() ? m_systemTheme : m_userTheme;
145}
146
147void QIconLoader::setThemeName(const QString &themeName)
148{
149 if (m_userTheme == themeName)
150 return;
151
152 qCDebug(lcIconLoader) << "Setting user theme name to" << themeName;
153
154 const bool hadUserTheme = hasUserTheme();
155 m_userTheme = themeName;
156 // if we cleared the user theme, then reset search paths as well,
157 // otherwise we'll keep looking in the user-defined search paths for
158 // a system-provide theme, which will never work.
159 if (!hasUserTheme() && hadUserTheme)
160 setThemeSearchPath(systemIconSearchPaths());
161 invalidateKey();
162}
163
164QString QIconLoader::fallbackThemeName() const
165{
166 return m_userFallbackTheme.isEmpty() ? systemFallbackThemeName() : m_userFallbackTheme;
167}
168
169void QIconLoader::setFallbackThemeName(const QString &themeName)
170{
171 qCDebug(lcIconLoader) << "Setting fallback theme name to" << themeName;
172 m_userFallbackTheme = themeName;
173 invalidateKey();
174}
175
176void QIconLoader::setThemeSearchPath(const QStringList &searchPaths)
177{
178 qCDebug(lcIconLoader) << "Setting theme search path to" << searchPaths;
179 m_iconDirs = searchPaths;
180 themeList.clear();
181 invalidateKey();
182}
183
184QStringList QIconLoader::themeSearchPaths() const
185{
186 if (m_iconDirs.isEmpty()) {
187 m_iconDirs = systemIconSearchPaths();
188 // Always add resource directory as search path
189 m_iconDirs.append(t: ":/icons"_L1);
190 }
191 return m_iconDirs;
192}
193
194void QIconLoader::setFallbackSearchPaths(const QStringList &searchPaths)
195{
196 qCDebug(lcIconLoader) << "Setting fallback search path to" << searchPaths;
197 m_fallbackDirs = searchPaths;
198 invalidateKey();
199}
200
201QStringList 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*/
216class QIconCacheGtkReader
217{
218public:
219 explicit QIconCacheGtkReader(const QString &themeDir);
220 QList<const char *> lookup(QStringView);
221 bool isValid() const { return m_isValid; }
222private:
223 QFile m_file;
224 const unsigned char *m_data;
225 quint64 m_size;
226 bool m_isValid;
227
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 }
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
248QIconCacheGtkReader::QIconCacheGtkReader(const QString &dirName)
249 : m_isValid(false)
250{
251 QFileInfo info(dirName + "/icon-theme.cache"_L1);
252 if (!info.exists() || info.lastModified(tz: QTimeZone::UTC) < QFileInfo(dirName).lastModified(tz: QTimeZone::UTC))
253 return;
254 m_file.setFileName(info.absoluteFilePath());
255 if (!m_file.open(flags: QFile::ReadOnly))
256 return;
257 m_size = m_file.size();
258 m_data = m_file.map(offset: 0, size: m_size);
259 if (!m_data)
260 return;
261 if (read16(offset: 0) != 1) // VERSION_MAJOR
262 return;
263
264 m_isValid = true;
265
266 // Check that all the directories are older than the cache
267 const QDateTime lastModified = info.lastModified(tz: QTimeZone::UTC);
268 quint32 dirListOffset = read32(offset: 8);
269 quint32 dirListLen = read32(offset: dirListOffset);
270 for (uint i = 0; i < dirListLen; ++i) {
271 quint32 offset = read32(offset: dirListOffset + 4 + 4 * i);
272 if (!m_isValid || offset >= m_size || lastModified < QFileInfo(dirName + u'/'
273 + QString::fromUtf8(utf8: reinterpret_cast<const char*>(m_data + offset))).lastModified(tz: QTimeZone::UTC)) {
274 m_isValid = false;
275 return;
276 }
277 }
278}
279
280static 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 */
293QList<const char *> QIconCacheGtkReader::lookup(QStringView name)
294{
295 QList<const char *> ret;
296 if (!isValid() || name.isEmpty())
297 return ret;
298
299 QByteArray nameUtf8 = name.toUtf8();
300 quint32 hash = icon_name_hash(p: nameUtf8);
301
302 quint32 hashOffset = read32(offset: 4);
303 quint32 hashBucketCount = read32(offset: 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(offset: hashOffset + 4 + bucketIndex * 4);
312 while (bucketOffset > 0 && bucketOffset <= m_size - 12) {
313 quint32 nameOff = read32(offset: bucketOffset + 4);
314 if (nameOff < m_size && strcmp(s1: reinterpret_cast<const char*>(m_data + nameOff), s2: nameUtf8) == 0) {
315 quint32 dirListOffset = read32(offset: 8);
316 quint32 dirListLen = read32(offset: dirListOffset);
317
318 quint32 listOffset = read32(offset: bucketOffset+8);
319 quint32 listLen = read32(offset: listOffset);
320
321 if (!m_isValid || listOffset + 4 + 8 * listLen > m_size) {
322 m_isValid = false;
323 return ret;
324 }
325
326 ret.reserve(asize: listLen);
327 for (uint j = 0; j < listLen && m_isValid; ++j) {
328 quint32 dirIndex = read16(offset: listOffset + 4 + 8 * j);
329 quint32 o = read32(offset: dirListOffset + 4 + dirIndex*4);
330 if (!m_isValid || dirIndex >= dirListLen || o >= m_size) {
331 m_isValid = false;
332 return ret;
333 }
334 ret.append(t: reinterpret_cast<const char*>(m_data) + o);
335 }
336 return ret;
337 }
338 bucketOffset = read32(offset: bucketOffset);
339 }
340 return ret;
341}
342
343QIconTheme::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() + u'/' + themeName;
352 QFileInfo themeDirInfo(themeDir);
353
354 if (themeDirInfo.isDir()) {
355 m_contentDirs << themeDir;
356 m_gtkCaches << QSharedPointer<QIconCacheGtkReader>::create(arguments&: themeDir);
357 }
358
359 if (!m_valid) {
360 themeIndex.setFileName(themeDir + "/index.theme"_L1);
361 m_valid = themeIndex.exists();
362 qCDebug(lcIconLoader) << "Probing theme file at" << themeIndex.fileName() << m_valid;
363 }
364 }
365#if QT_CONFIG(settings)
366 if (m_valid) {
367 const QSettings indexReader(themeIndex.fileName(), QSettings::IniFormat);
368 const QStringList keys = indexReader.allKeys();
369 for (const QString &key : keys) {
370 if (key.endsWith(s: "/Size"_L1)) {
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(n: key.size() - 5);
375 QIconDirInfo dirInfo(directoryKey);
376 dirInfo.size = size;
377 QString type = indexReader.value(key: directoryKey + "/Type"_L1).toString();
378
379 if (type == "Fixed"_L1)
380 dirInfo.type = QIconDirInfo::Fixed;
381 else if (type == "Scalable"_L1)
382 dirInfo.type = QIconDirInfo::Scalable;
383 else
384 dirInfo.type = QIconDirInfo::Threshold;
385
386 dirInfo.threshold = indexReader.value(key: directoryKey +
387 "/Threshold"_L1,
388 defaultValue: 2).toInt();
389
390 dirInfo.minSize = indexReader.value(key: directoryKey + "/MinSize"_L1, defaultValue: size).toInt();
391
392 dirInfo.maxSize = indexReader.value(key: directoryKey + "/MaxSize"_L1, defaultValue: size).toInt();
393
394 dirInfo.scale = indexReader.value(key: directoryKey + "/Scale"_L1, defaultValue: 1).toInt();
395 m_keyList.append(t: dirInfo);
396 }
397 }
398 }
399
400 // Parent themes provide fallbacks for missing icons
401 m_parents = indexReader.value(key: "Icon Theme/Inherits"_L1).toStringList();
402 m_parents.removeAll(t: QString());
403 }
404#endif // settings
405}
406
407QStringList QIconTheme::parents() const
408{
409 // Respect explicitly declared parents
410 QStringList result = m_parents;
411
412 // Ensure a default fallback for all themes
413 const QString fallback = QIconLoader::instance()->fallbackThemeName();
414 if (!fallback.isEmpty())
415 result.append(t: fallback);
416
417 // Ensure that all themes fall back to hicolor as the last theme
418 result.removeAll(t: "hicolor"_L1);
419 result.append(t: "hicolor"_L1);
420
421 return result;
422}
423
424QDebug operator<<(QDebug debug, const std::unique_ptr<QIconLoaderEngineEntry> &entry)
425{
426 QDebugStateSaver saver(debug);
427 debug.noquote() << entry->filename;
428 return debug;
429}
430
431QThemeIconInfo QIconLoader::findIconHelper(const QString &themeName,
432 const QString &iconName,
433 QStringList &visited) const
434{
435 qCDebug(lcIconLoader) << "Finding icon" << iconName << "in theme" << themeName
436 << "skipping" << visited;
437
438 QThemeIconInfo info;
439 Q_ASSERT(!themeName.isEmpty());
440
441 // Used to protect against potential recursions
442 visited << themeName;
443
444 QIconTheme &theme = themeList[themeName];
445 if (!theme.isValid()) {
446 theme = QIconTheme(themeName);
447 if (!theme.isValid()) {
448 qCDebug(lcIconLoader) << "Theme" << themeName << "not found";
449 return info;
450 }
451 }
452
453 const QStringList contentDirs = theme.contentDirs();
454
455 QStringView iconNameFallback(iconName);
456
457 // Iterate through all icon's fallbacks in current theme
458 while (info.entries.empty()) {
459 const QString svgIconName = iconNameFallback + ".svg"_L1;
460 const QString pngIconName = iconNameFallback + ".png"_L1;
461
462 // Add all relevant files
463 for (int i = 0; i < contentDirs.size(); ++i) {
464 QList<QIconDirInfo> subDirs = theme.keyList();
465
466 // Try to reduce the amount of subDirs by looking in the GTK+ cache in order to save
467 // a massive amount of file stat (especially if the icon is not there)
468 auto cache = theme.m_gtkCaches.at(i);
469 if (cache->isValid()) {
470 const auto result = cache->lookup(name: iconNameFallback);
471 if (cache->isValid()) {
472 const QList<QIconDirInfo> subDirsCopy = subDirs;
473 subDirs.clear();
474 subDirs.reserve(asize: result.size());
475 for (const char *s : result) {
476 QString path = QString::fromUtf8(utf8: s);
477 auto it = std::find_if(first: subDirsCopy.cbegin(), last: subDirsCopy.cend(),
478 pred: [&](const QIconDirInfo &info) {
479 return info.path == path; } );
480 if (it != subDirsCopy.cend()) {
481 subDirs.append(t: *it);
482 }
483 }
484 }
485 }
486
487 QString contentDir = contentDirs.at(i) + u'/';
488 for (int j = 0; j < subDirs.size() ; ++j) {
489 const QIconDirInfo &dirInfo = subDirs.at(i: j);
490 const QString subDir = contentDir + dirInfo.path + u'/';
491 const QString pngPath = subDir + pngIconName;
492 if (QFile::exists(fileName: pngPath)) {
493 auto iconEntry = std::make_unique<PixmapEntry>();
494 iconEntry->dir = dirInfo;
495 iconEntry->filename = pngPath;
496 // Notice we ensure that pixmap entries always come before
497 // scalable to preserve search order afterwards
498 info.entries.insert(position: info.entries.begin(), x: std::move(iconEntry));
499 } else if (m_supportsSvg) {
500 const QString svgPath = subDir + svgIconName;
501 if (QFile::exists(fileName: svgPath)) {
502 auto iconEntry = std::make_unique<ScalableEntry>();
503 iconEntry->dir = dirInfo;
504 iconEntry->filename = svgPath;
505 info.entries.push_back(x: std::move(iconEntry));
506 }
507 }
508 }
509 }
510
511 if (!info.entries.empty()) {
512 info.iconName = iconNameFallback.toString();
513 break;
514 }
515
516 // If it's possible - find next fallback for the icon
517 const int indexOfDash = iconNameFallback.lastIndexOf(c: u'-');
518 if (indexOfDash == -1)
519 break;
520
521 iconNameFallback.truncate(n: indexOfDash);
522 }
523
524 if (info.entries.empty()) {
525 const QStringList parents = theme.parents();
526 qCDebug(lcIconLoader) << "Did not find matching icons in theme;"
527 << "trying parent themes" << parents
528 << "skipping visited" << visited;
529
530 // Search recursively through inherited themes
531 for (int i = 0 ; i < parents.size() ; ++i) {
532
533 const QString parentTheme = parents.at(i).trimmed();
534
535 if (!visited.contains(str: parentTheme)) // guard against recursion
536 info = findIconHelper(themeName: parentTheme, iconName, visited);
537
538 if (!info.entries.empty()) // success
539 break;
540 }
541 }
542
543 return info;
544}
545
546QThemeIconInfo QIconLoader::lookupFallbackIcon(const QString &iconName) const
547{
548 qCDebug(lcIconLoader) << "Looking up fallback icon" << iconName;
549
550 QThemeIconInfo info;
551
552 const QString pngIconName = iconName + ".png"_L1;
553 const QString xpmIconName = iconName + ".xpm"_L1;
554 const QString svgIconName = iconName + ".svg"_L1;
555
556 const auto searchPaths = QIcon::fallbackSearchPaths();
557 for (const QString &iconDir: searchPaths) {
558 QDir currentDir(iconDir);
559 std::unique_ptr<QIconLoaderEngineEntry> iconEntry;
560 if (currentDir.exists(name: pngIconName)) {
561 iconEntry = std::make_unique<PixmapEntry>();
562 iconEntry->dir.type = QIconDirInfo::Fallback;
563 iconEntry->filename = currentDir.filePath(fileName: pngIconName);
564 } else if (currentDir.exists(name: xpmIconName)) {
565 iconEntry = std::make_unique<PixmapEntry>();
566 iconEntry->dir.type = QIconDirInfo::Fallback;
567 iconEntry->filename = currentDir.filePath(fileName: xpmIconName);
568 } else if (m_supportsSvg &&
569 currentDir.exists(name: svgIconName)) {
570 iconEntry = std::make_unique<ScalableEntry>();
571 iconEntry->dir.type = QIconDirInfo::Fallback;
572 iconEntry->filename = currentDir.filePath(fileName: svgIconName);
573 }
574 if (iconEntry) {
575 info.entries.push_back(x: std::move(iconEntry));
576 break;
577 }
578 }
579
580 if (!info.entries.empty())
581 info.iconName = iconName;
582
583 return info;
584}
585
586QThemeIconInfo QIconLoader::loadIcon(const QString &name) const
587{
588 qCDebug(lcIconLoader) << "Loading icon" << name;
589
590 QThemeIconInfo iconInfo;
591 QStringList visitedThemes;
592 if (!themeName().isEmpty())
593 iconInfo = findIconHelper(themeName: themeName(), iconName: name, visited&: visitedThemes);
594
595 if (iconInfo.entries.empty() && !fallbackThemeName().isEmpty())
596 iconInfo = findIconHelper(themeName: fallbackThemeName(), iconName: name, visited&: visitedThemes);
597
598 if (iconInfo.entries.empty())
599 iconInfo = lookupFallbackIcon(iconName: name);
600
601 qCDebug(lcIconLoader) << "Resulting icon entries" << iconInfo.entries;
602 return iconInfo;
603}
604
605#ifndef QT_NO_DEBUG_STREAM
606QDebug operator<<(QDebug debug, QIconEngine *engine)
607{
608 QDebugStateSaver saver(debug);
609 debug.nospace();
610 if (engine) {
611 debug.noquote() << engine->key() << "(";
612 debug << static_cast<const void *>(engine);
613 if (!engine->isNull())
614 debug.quote() << ", " << engine->iconName();
615 else
616 debug << ", null";
617 debug << ")";
618 } else {
619 debug << "QIconEngine(nullptr)";
620 }
621 return debug;
622}
623#endif
624
625QIconEngine *QIconLoader::iconEngine(const QString &iconName) const
626{
627 qCDebug(lcIconLoader) << "Resolving icon engine for icon" << iconName;
628
629 auto *platformTheme = QGuiApplicationPrivate::platformTheme();
630 std::unique_ptr<QIconEngine> iconEngine;
631 if (!hasUserTheme() && platformTheme)
632 iconEngine.reset(p: platformTheme->createIconEngine(iconName));
633 if (!iconEngine || iconEngine->isNull()) {
634 iconEngine.reset(p: new QIconLoaderEngine(iconName));
635 }
636
637 qCDebug(lcIconLoader) << "Resulting engine" << iconEngine.get();
638 return iconEngine.release();
639}
640
641/*!
642 \internal
643 \class QThemeIconEngine
644 \inmodule QtGui
645
646 \brief A named-based icon engine for providing theme icons.
647
648 The engine supports invalidation of prior lookups, e.g. when
649 the platform theme changes or the user sets an explicit icon
650 theme.
651
652 The actual icon lookup is handed over to an engine provided
653 by QIconLoader::iconEngine().
654*/
655
656QThemeIconEngine::QThemeIconEngine(const QString& iconName)
657 : QProxyIconEngine()
658 , m_iconName(iconName)
659{
660}
661
662QThemeIconEngine::QThemeIconEngine(const QThemeIconEngine &other)
663 : QProxyIconEngine()
664 , m_iconName(other.m_iconName)
665{
666}
667
668QString QThemeIconEngine::key() const
669{
670 // Although we proxy the underlying engine, that's an implementation
671 // detail, so from the point of view of QIcon, and in terms of
672 // serialization, we are the one and only theme icon engine.
673 return u"QThemeIconEngine"_s;
674}
675
676QIconEngine *QThemeIconEngine::clone() const
677{
678 return new QThemeIconEngine(*this);
679}
680
681bool QThemeIconEngine::read(QDataStream &in) {
682 in >> m_iconName;
683 return true;
684}
685
686bool QThemeIconEngine::write(QDataStream &out) const
687{
688 out << m_iconName;
689 return true;
690}
691
692QIconEngine *QThemeIconEngine::proxiedEngine() const
693{
694 const auto *iconLoader = QIconLoader::instance();
695 auto mostRecentThemeKey = iconLoader->themeKey();
696 if (mostRecentThemeKey != m_themeKey) {
697 qCDebug(lcIconLoader) << "Theme key" << mostRecentThemeKey << "is different"
698 << "than cached key" << m_themeKey << "for icon" << m_iconName;
699 m_proxiedEngine.reset(p: iconLoader->iconEngine(iconName: m_iconName));
700 m_themeKey = mostRecentThemeKey;
701 }
702 return m_proxiedEngine.get();
703}
704
705/*!
706 \internal
707 \class QIconLoaderEngine
708 \inmodule QtGui
709
710 \brief An icon engine based on icon entries collected by QIconLoader.
711
712 The design and implementation of QIconLoader is based on
713 the XDG icon specification.
714*/
715
716QIconLoaderEngine::QIconLoaderEngine(const QString& iconName)
717 : m_iconName(iconName)
718 , m_info(QIconLoader::instance()->loadIcon(name: m_iconName))
719{
720}
721
722QIconLoaderEngine::~QIconLoaderEngine() = default;
723
724QIconEngine *QIconLoaderEngine::clone() const
725{
726 Q_UNREACHABLE();
727 return nullptr; // Cannot be cloned
728}
729
730bool QIconLoaderEngine::hasIcon() const
731{
732 return !(m_info.entries.empty());
733}
734
735void QIconLoaderEngine::paint(QPainter *painter, const QRect &rect,
736 QIcon::Mode mode, QIcon::State state)
737{
738 QSize pixmapSize = rect.size() * painter->device()->devicePixelRatio();
739 painter->drawPixmap(r: rect, pm: pixmap(size: pixmapSize, mode, state));
740}
741
742/*
743 * This algorithm is defined by the freedesktop spec:
744 * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
745 */
746static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize, int iconscale)
747{
748 if (dir.scale != iconscale)
749 return false;
750
751 if (dir.type == QIconDirInfo::Fixed) {
752 return dir.size == iconsize;
753
754 } else if (dir.type == QIconDirInfo::Scalable) {
755 return iconsize <= dir.maxSize &&
756 iconsize >= dir.minSize;
757
758 } else if (dir.type == QIconDirInfo::Threshold) {
759 return iconsize >= dir.size - dir.threshold &&
760 iconsize <= dir.size + dir.threshold;
761 } else if (dir.type == QIconDirInfo::Fallback) {
762 return true;
763 }
764
765 Q_ASSERT(1); // Not a valid value
766 return false;
767}
768
769/*
770 * This algorithm is defined by the freedesktop spec:
771 * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
772 */
773static int directorySizeDistance(const QIconDirInfo &dir, int iconsize, int iconscale)
774{
775 const int scaledIconSize = iconsize * iconscale;
776 if (dir.type == QIconDirInfo::Fixed) {
777 return qAbs(t: dir.size * dir.scale - scaledIconSize);
778
779 } else if (dir.type == QIconDirInfo::Scalable) {
780 if (scaledIconSize < dir.minSize * dir.scale)
781 return dir.minSize * dir.scale - scaledIconSize;
782 else if (scaledIconSize > dir.maxSize * dir.scale)
783 return scaledIconSize - dir.maxSize * dir.scale;
784 else
785 return 0;
786
787 } else if (dir.type == QIconDirInfo::Threshold) {
788 if (scaledIconSize < (dir.size - dir.threshold) * dir.scale)
789 return dir.minSize * dir.scale - scaledIconSize;
790 else if (scaledIconSize > (dir.size + dir.threshold) * dir.scale)
791 return scaledIconSize - dir.maxSize * dir.scale;
792 else return 0;
793 } else if (dir.type == QIconDirInfo::Fallback) {
794 return 0;
795 }
796
797 Q_ASSERT(1); // Not a valid value
798 return INT_MAX;
799}
800
801QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QThemeIconInfo &info, const QSize &size, int scale)
802{
803 int iconsize = qMin(a: size.width(), b: size.height());
804
805 // Note that m_info.entries are sorted so that png-files
806 // come first
807
808 // Search for exact matches first
809 for (const auto &entry : info.entries) {
810 if (directoryMatchesSize(dir: entry->dir, iconsize, iconscale: scale)) {
811 return entry.get();
812 }
813 }
814
815 // Find the minimum distance icon
816 int minimalSize = INT_MAX;
817 QIconLoaderEngineEntry *closestMatch = nullptr;
818 for (const auto &entry : info.entries) {
819 int distance = directorySizeDistance(dir: entry->dir, iconsize, iconscale: scale);
820 if (distance < minimalSize) {
821 minimalSize = distance;
822 closestMatch = entry.get();
823 }
824 }
825 return closestMatch;
826}
827
828/*
829 * Returns the actual icon size. For scalable svg's this is equivalent
830 * to the requested size. Otherwise the closest match is returned but
831 * we can never return a bigger size than the requested size.
832 *
833 */
834QSize QIconLoaderEngine::actualSize(const QSize &size, QIcon::Mode mode,
835 QIcon::State state)
836{
837 Q_UNUSED(mode);
838 Q_UNUSED(state);
839
840 QIconLoaderEngineEntry *entry = entryForSize(info: m_info, size);
841 if (entry) {
842 const QIconDirInfo &dir = entry->dir;
843 if (dir.type == QIconDirInfo::Scalable) {
844 return size;
845 } else if (dir.type == QIconDirInfo::Fallback) {
846 return QIcon(entry->filename).actualSize(size, mode, state);
847 } else {
848 int result = qMin<int>(a: dir.size, b: qMin(a: size.width(), b: size.height()));
849 return QSize(result, result);
850 }
851 }
852 return QSize(0, 0);
853}
854
855QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
856{
857 Q_UNUSED(state);
858
859 // Ensure that basePixmap is lazily initialized before generating the
860 // key, otherwise the cache key is not unique
861 if (basePixmap.isNull())
862 basePixmap.load(fileName: filename);
863
864 QSize actualSize = basePixmap.size();
865 // If the size of the best match we have (basePixmap) is larger than the
866 // requested size, we downscale it to match.
867 if (!actualSize.isNull() && (actualSize.width() > size.width() || actualSize.height() > size.height()))
868 actualSize.scale(s: size, mode: Qt::KeepAspectRatio);
869
870 QString key = "$qt_theme_"_L1
871 % HexString<qint64>(basePixmap.cacheKey())
872 % HexString<int>(mode)
873 % HexString<qint64>(QGuiApplication::palette().cacheKey())
874 % HexString<int>(actualSize.width())
875 % HexString<int>(actualSize.height());
876
877 QPixmap cachedPixmap;
878 if (QPixmapCache::find(key, pixmap: &cachedPixmap)) {
879 return cachedPixmap;
880 } else {
881 if (basePixmap.size() != actualSize)
882 cachedPixmap = basePixmap.scaled(s: actualSize, aspectMode: Qt::IgnoreAspectRatio, mode: Qt::SmoothTransformation);
883 else
884 cachedPixmap = basePixmap;
885 if (QGuiApplication *guiApp = qobject_cast<QGuiApplication *>(qApp))
886 cachedPixmap = static_cast<QGuiApplicationPrivate*>(QObjectPrivate::get(o: guiApp))->applyQIconStyleHelper(mode, basePixmap: cachedPixmap);
887 QPixmapCache::insert(key, pixmap: cachedPixmap);
888 }
889 return cachedPixmap;
890}
891
892QPixmap ScalableEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)
893{
894 if (svgIcon.isNull())
895 svgIcon = QIcon(filename);
896
897 // Bypass QIcon API, as that will scale by device pixel ratio of the
898 // highest DPR screen since we're not passing on any QWindow.
899 if (QIconEngine *engine = svgIcon.data_ptr() ? svgIcon.data_ptr()->engine : nullptr)
900 return engine->pixmap(size, mode, state);
901
902 return QPixmap();
903}
904
905QPixmap QIconLoaderEngine::pixmap(const QSize &size, QIcon::Mode mode,
906 QIcon::State state)
907{
908 QIconLoaderEngineEntry *entry = entryForSize(info: m_info, size);
909 if (entry)
910 return entry->pixmap(size, mode, state);
911
912 return QPixmap();
913}
914
915QString QIconLoaderEngine::key() const
916{
917 return u"QIconLoaderEngine"_s;
918}
919
920QString QIconLoaderEngine::iconName()
921{
922 return m_info.iconName;
923}
924
925bool QIconLoaderEngine::isNull()
926{
927 return m_info.entries.empty();
928}
929
930QPixmap QIconLoaderEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale)
931{
932 const int integerScale = qCeil(v: scale);
933 QIconLoaderEngineEntry *entry = entryForSize(info: m_info, size: size / integerScale, scale: integerScale);
934 return entry ? entry->pixmap(size, mode, state) : QPixmap();
935}
936
937QList<QSize> QIconLoaderEngine::availableSizes(QIcon::Mode mode, QIcon::State state)
938{
939 Q_UNUSED(mode);
940 Q_UNUSED(state);
941
942 const qsizetype N = qsizetype(m_info.entries.size());
943 QList<QSize> sizes;
944 sizes.reserve(asize: N);
945
946 // Gets all sizes from the DirectoryInfo entries
947 for (const auto &entry : m_info.entries) {
948 if (entry->dir.type == QIconDirInfo::Fallback) {
949 sizes.append(other: QIcon(entry->filename).availableSizes());
950 } else {
951 int size = entry->dir.size;
952 sizes.append(t: QSize(size, size));
953 }
954 }
955 return sizes;
956}
957
958QT_END_NAMESPACE
959
960#endif //QT_NO_ICON
961

source code of qtbase/src/gui/image/qiconloader.cpp