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