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