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