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 ( int i = 0 ; i < iconDirs.size() ; ++i) {
356 QDir iconDir(iconDirs[i]);
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 (int 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 (int j = 0; j < subDirs.size() ; ++j) {
508 const QIconDirInfo &dirInfo = subDirs.at(j);
509 if (searchingGenericFallback &&
510 (dirInfo.context == QIconDirInfo::Applications ||
511 dirInfo.context == QIconDirInfo::MimeTypes))
512 continue;
513
514 const QString subDir = contentDir + dirInfo.path + u'/';
515 const QString pngPath = subDir + pngIconName;
516 if (QFile::exists(pngPath)) {
517 auto iconEntry = std::make_unique<PixmapEntry>();
518 iconEntry->dir = dirInfo;
519 iconEntry->filename = pngPath;
520 // Notice we ensure that pixmap entries always come before
521 // scalable to preserve search order afterwards
522 info.entries.insert(info.entries.begin(), std::move(iconEntry));
523 } else if (m_supportsSvg) {
524 const QString svgPath = subDir + svgIconName;
525 if (QFile::exists(svgPath)) {
526 auto iconEntry = std::make_unique<ScalableEntry>();
527 iconEntry->dir = dirInfo;
528 iconEntry->filename = svgPath;
529 info.entries.push_back(std::move(iconEntry));
530 }
531 }
532 }
533 }
534
535 if (!info.entries.empty()) {
536 info.iconName = iconNameFallback.toString();
537 }
538 }
539
540 if (info.entries.empty()) {
541 const QStringList parents = theme.parents();
542 qCDebug(lcIconLoader) << "Did not find matching icons in theme;"
543 << "trying parent themes" << parents
544 << "skipping visited" << visited;
545
546 // Search recursively through inherited themes
547 for (int i = 0 ; i < parents.size() ; ++i) {
548
549 const QString parentTheme = parents.at(i).trimmed();
550
551 if (!visited.contains(parentTheme)) // guard against recursion
552 info = findIconHelper(parentTheme, iconName, visited, QIconLoader::NoFallBack);
553
554 if (!info.entries.empty()) // success
555 break;
556 }
557 }
558
559 if (rule == QIconLoader::FallBack && info.entries.empty()) {
560 // If it's possible - find next fallback for the icon
561 const int indexOfDash = iconNameFallback.lastIndexOf(u'-');
562 if (indexOfDash != -1) {
563 qCDebug(lcIconLoader) << "Did not find matching icons in all themes;"
564 << "trying dash fallback";
565 iconNameFallback.truncate(indexOfDash);
566 QStringList _visited;
567 info = findIconHelper(themeName, iconNameFallback.toString(), _visited, QIconLoader::FallBack);
568 }
569 }
570
571 return info;
572}
573
574QThemeIconInfo QIconLoader::lookupFallbackIcon(const QString &iconName) const
575{
576 qCDebug(lcIconLoader) << "Looking up fallback icon" << iconName;
577
578 QThemeIconInfo info;
579
580 const QString pngIconName = iconName + ".png"_L1;
581 const QString xpmIconName = iconName + ".xpm"_L1;
582 const QString svgIconName = iconName + ".svg"_L1;
583
584 const auto searchPaths = QIcon::fallbackSearchPaths();
585 for (const QString &iconDir: searchPaths) {
586 QDir currentDir(iconDir);
587 std::unique_ptr<QIconLoaderEngineEntry> iconEntry;
588 if (currentDir.exists(pngIconName)) {
589 iconEntry = std::make_unique<PixmapEntry>();
590 iconEntry->dir.type = QIconDirInfo::Fallback;
591 iconEntry->filename = currentDir.filePath(pngIconName);
592 } else if (currentDir.exists(xpmIconName)) {
593 iconEntry = std::make_unique<PixmapEntry>();
594 iconEntry->dir.type = QIconDirInfo::Fallback;
595 iconEntry->filename = currentDir.filePath(xpmIconName);
596 } else if (m_supportsSvg &&
597 currentDir.exists(svgIconName)) {
598 iconEntry = std::make_unique<ScalableEntry>();
599 iconEntry->dir.type = QIconDirInfo::Fallback;
600 iconEntry->filename = currentDir.filePath(svgIconName);
601 }
602 if (iconEntry) {
603 info.entries.push_back(std::move(iconEntry));
604 break;
605 }
606 }
607
608 if (!info.entries.empty())
609 info.iconName = iconName;
610
611 return info;
612}
613
614QThemeIconInfo QIconLoader::loadIcon(const QString &name) const
615{
616 qCDebug(lcIconLoader) << "Loading icon" << name;
617
618 m_iconName = name;
619 QThemeIconInfo iconInfo;
620 QStringList visitedThemes;
621 if (!themeName().isEmpty())
622 iconInfo = findIconHelper(themeName(), name, visitedThemes, QIconLoader::FallBack);
623
624 if (iconInfo.entries.empty() && !fallbackThemeName().isEmpty())
625 iconInfo = findIconHelper(fallbackThemeName(), name, visitedThemes, QIconLoader::FallBack);
626
627 if (iconInfo.entries.empty())
628 iconInfo = lookupFallbackIcon(name);
629
630 qCDebug(lcIconLoader) << "Resulting icon entries" << iconInfo.entries;
631 return iconInfo;
632}
633
634#ifndef QT_NO_DEBUG_STREAM
635QDebug operator<<(QDebug debug, QIconEngine *engine)
636{
637 QDebugStateSaver saver(debug);
638 debug.nospace();
639 if (engine) {
640 debug.noquote() << engine->key() << "(";
641 debug << static_cast<const void *>(engine);
642 if (!engine->isNull())
643 debug.quote() << ", " << engine->iconName();
644 else
645 debug << ", null";
646 debug << ")";
647 } else {
648 debug << "QIconEngine(nullptr)";
649 }
650 return debug;
651}
652#endif
653
654QIconEngine *QIconLoader::iconEngine(const QString &iconName) const
655{
656 qCDebug(lcIconLoader) << "Resolving icon engine for icon" << iconName;
657
658 std::unique_ptr<QIconEngine> iconEngine;
659
660 if (!m_factory) {
661 qCDebug(lcIconLoader) << "Finding a plugin for theme" << themeName();
662 // try to find a plugin that supports the current theme
663 const int factoryIndex = qt_iconEngineFactoryLoader()->indexOf(themeName());
664 if (factoryIndex >= 0)
665 m_factory = qobject_cast<QIconEnginePlugin *>(qt_iconEngineFactoryLoader()->instance(factoryIndex));
666 }
667 if (m_factory && *m_factory)
668 iconEngine.reset(m_factory.value()->create(iconName));
669
670 if (hasUserTheme()) {
671 if (!iconEngine || iconEngine->isNull()) {
672 if (QFontDatabase::families().contains(themeName())) {
673 QFont maybeIconFont(themeName());
674 maybeIconFont.setStyleStrategy(QFont::NoFontMerging);
675 qCDebug(lcIconLoader) << "Trying font icon engine.";
676 iconEngine.reset(new QFontIconEngine(iconName, maybeIconFont));
677 }
678 }
679 if (!iconEngine || iconEngine->isNull()) {
680 qCDebug(lcIconLoader) << "Trying loader engine for theme.";
681 iconEngine.reset(new QIconLoaderEngine(iconName));
682 }
683 }
684
685 if (!iconEngine || iconEngine->isNull()) {
686 qCDebug(lcIconLoader) << "Icon is not available from theme or fallback theme.";
687 if (auto *platformTheme = QGuiApplicationPrivate::platformTheme()) {
688 qCDebug(lcIconLoader) << "Trying platform engine.";
689 std::unique_ptr<QIconEngine> themeEngine(platformTheme->createIconEngine(iconName));
690 if (themeEngine && !themeEngine->isNull()) {
691 iconEngine = std::move(themeEngine);
692 qCDebug(lcIconLoader) << "Icon provided by platform engine.";
693 }
694 }
695 }
696 // We need to maintain the invariant that the QIcon has a valid engine
697 if (!iconEngine)
698 iconEngine.reset(new QIconLoaderEngine(iconName));
699
700 qCDebug(lcIconLoader) << "Resulting engine" << iconEngine.get();
701 return iconEngine.release();
702}
703
704/*!
705 \internal
706 \class QThemeIconEngine
707 \inmodule QtGui
708
709 \brief A named-based icon engine for providing theme icons.
710
711 The engine supports invalidation of prior lookups, e.g. when
712 the platform theme changes or the user sets an explicit icon
713 theme.
714
715 The actual icon lookup is handed over to an engine provided
716 by QIconLoader::iconEngine().
717*/
718
719QThemeIconEngine::QThemeIconEngine(const QString& iconName)
720 : QProxyIconEngine()
721 , m_iconName(iconName)
722{
723}
724
725QThemeIconEngine::QThemeIconEngine(const QThemeIconEngine &other)
726 : QProxyIconEngine()
727 , m_iconName(other.m_iconName)
728{
729}
730
731QString QThemeIconEngine::key() const
732{
733 // Although we proxy the underlying engine, that's an implementation
734 // detail, so from the point of view of QIcon, and in terms of
735 // serialization, we are the one and only theme icon engine.
736 return u"QThemeIconEngine"_s;
737}
738
739QIconEngine *QThemeIconEngine::clone() const
740{
741 return new QThemeIconEngine(*this);
742}
743
744bool QThemeIconEngine::read(QDataStream &in) {
745 in >> m_iconName;
746 return true;
747}
748
749bool QThemeIconEngine::write(QDataStream &out) const
750{
751 out << m_iconName;
752 return true;
753}
754
755QIconEngine *QThemeIconEngine::proxiedEngine() const
756{
757 const auto *iconLoader = QIconLoader::instance();
758 auto mostRecentThemeKey = iconLoader->themeKey();
759 if (mostRecentThemeKey != m_themeKey) {
760 qCDebug(lcIconLoader) << "Theme key" << mostRecentThemeKey << "is different"
761 << "than cached key" << m_themeKey << "for icon" << m_iconName;
762 m_proxiedEngine.reset(iconLoader->iconEngine(m_iconName));
763 m_themeKey = mostRecentThemeKey;
764 }
765 return m_proxiedEngine.get();
766}
767
768/*!
769 \internal
770 \class QIconLoaderEngine
771 \inmodule QtGui
772
773 \brief An icon engine based on icon entries collected by QIconLoader.
774
775 The design and implementation of QIconLoader is based on
776 the XDG icon specification.
777*/
778
779QIconLoaderEngine::QIconLoaderEngine(const QString& iconName)
780 : m_iconName(iconName)
781 , m_info(QIconLoader::instance()->loadIcon(m_iconName))
782{
783}
784
785QIconLoaderEngine::~QIconLoaderEngine() = default;
786
787QIconEngine *QIconLoaderEngine::clone() const
788{
789 Q_UNREACHABLE();
790 return nullptr; // Cannot be cloned
791}
792
793bool QIconLoaderEngine::hasIcon() const
794{
795 return !(m_info.entries.empty());
796}
797
798void QIconLoaderEngine::paint(QPainter *painter, const QRect &rect,
799 QIcon::Mode mode, QIcon::State state)
800{
801 const auto dpr = painter->device()->devicePixelRatio();
802 painter->drawPixmap(rect, scaledPixmap(rect.size(), mode, state, dpr));
803}
804
805/*
806 * This algorithm is defined by the freedesktop spec:
807 * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
808 */
809static bool directoryMatchesSizeAndScale(const QIconDirInfo &dir, int iconsize, int iconscale)
810{
811 if (dir.scale != iconscale)
812 return false;
813
814 switch (dir.type) {
815 case QIconDirInfo::Fixed:
816 return dir.size == iconsize;
817 case QIconDirInfo::Scalable:
818 return iconsize <= dir.maxSize && iconsize >= dir.minSize;
819 case QIconDirInfo::Threshold:
820 return iconsize >= dir.size - dir.threshold && iconsize <= dir.size + dir.threshold;
821 case QIconDirInfo::Fallback:
822 return false; // just because the scale matches it doesn't mean there is a better sized icon somewhere
823 }
824
825 Q_ASSERT(1); // Not a valid value
826 return false;
827}
828
829/*
830 * This algorithm is a modification of the algorithm defined by the freedesktop spec:
831 * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
832 */
833static int directorySizeDelta(const QIconDirInfo &dir, int iconsize, int iconscale)
834{
835 const auto scaledIconSize = iconsize * iconscale;
836
837 switch (dir.type) {
838 case QIconDirInfo::Fixed:
839 return dir.size * dir.scale - scaledIconSize;
840 case QIconDirInfo::Scalable: {
841 const auto minScaled = dir.minSize * dir.scale;
842 if (scaledIconSize < minScaled)
843 return minScaled - scaledIconSize;
844 const auto maxScaled = dir.maxSize * dir.scale;
845 if (scaledIconSize > maxScaled)
846 return scaledIconSize - maxScaled;
847 return 0;
848 }
849 case QIconDirInfo::Threshold:
850 if (scaledIconSize < (dir.size - dir.threshold) * dir.scale)
851 return dir.minSize * dir.scale - scaledIconSize;
852 if (scaledIconSize > (dir.size + dir.threshold) * dir.scale)
853 return scaledIconSize - dir.maxSize * dir.scale;
854 return 0;
855 case QIconDirInfo::Fallback:
856 return INT_MAX;
857 }
858
859 Q_ASSERT(1); // Not a valid value
860 return INT_MAX;
861}
862
863QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QThemeIconInfo &info, const QSize &size, int scale)
864{
865 if (info.entries.empty())
866 return nullptr;
867 if (info.entries.size() == 1)
868 return info.entries.at(0).get();
869
870 int iconsize = qMin(size.width(), size.height());
871
872 // Note that m_info.entries are sorted so that png-files
873 // come first
874
875 int minimalDelta = INT_MIN;
876 QIconLoaderEngineEntry *closestMatch = nullptr;
877 for (const auto &entry : info.entries) {
878 // exact match in scale and dpr
879 if (directoryMatchesSizeAndScale(entry->dir, iconsize, scale))
880 return entry.get();
881
882 // Find the minimum distance icon
883 const auto deltaValue = directorySizeDelta(entry->dir, iconsize, scale);
884 // always prefer downscaled icons over upscaled icons
885 if (deltaValue > minimalDelta && minimalDelta <= 0) {
886 minimalDelta = deltaValue;
887 closestMatch = entry.get();
888 } else if (deltaValue > 0 && deltaValue < qAbs(minimalDelta)) {
889 minimalDelta = deltaValue;
890 closestMatch = entry.get();
891 } else if (deltaValue == 0) {
892 // exact match but different dpr:
893 // --> size * scale == entry.size * entry.scale
894 minimalDelta = deltaValue;
895 closestMatch = entry.get();
896 }
897 }
898 return closestMatch ? closestMatch : info.entries.at(0).get();
899}
900
901/*
902 * Returns the actual icon size. For scalable svg's this is equivalent
903 * to the requested size. Otherwise the closest match is returned but
904 * we can never return a bigger size than the requested size.
905 *
906 */
907QSize QIconLoaderEngine::actualSize(const QSize &size, QIcon::Mode mode,
908 QIcon::State state)
909{
910 Q_UNUSED(mode);
911 Q_UNUSED(state);
912
913 QIconLoaderEngineEntry *entry = entryForSize(m_info, size);
914 if (entry) {
915 const QIconDirInfo &dir = entry->dir;
916 if (dir.type == QIconDirInfo::Scalable) {
917 return size;
918 } else if (dir.type == QIconDirInfo::Fallback) {
919 return QIcon(entry->filename).actualSize(size, mode, state);
920 } else {
921 int result = qMin<int>(dir.size * dir.scale, qMin(size.width(), size.height()));
922 return QSize(result, result);
923 }
924 }
925 return QSize(0, 0);
926}
927
928QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale)
929{
930 Q_UNUSED(state);
931
932 // Ensure that basePixmap is lazily initialized before generating the
933 // key, otherwise the cache key is not unique
934 if (basePixmap.isNull())
935 basePixmap.load(filename);
936
937 // If the size of the best match we have (basePixmap) is larger than the
938 // requested size, we downscale it to match.
939 const auto actualSize = QPixmapIconEngine::adjustSize(size * scale, basePixmap.size());
940 const auto calculatedDpr = QIconPrivate::pixmapDevicePixelRatio(scale, size, actualSize);
941 QString key = "$qt_theme_"_L1
942 % HexString<quint64>(basePixmap.cacheKey())
943 % HexString<quint8>(mode)
944 % HexString<quint64>(QGuiApplication::palette().cacheKey())
945 % HexString<uint>(actualSize.width())
946 % HexString<uint>(actualSize.height())
947 % HexString<quint16>(qRound(calculatedDpr * 1000));
948
949 QPixmap cachedPixmap;
950 if (QPixmapCache::find(key, &cachedPixmap)) {
951 return cachedPixmap;
952 } else {
953 if (basePixmap.size() != actualSize)
954 cachedPixmap = basePixmap.scaled(actualSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
955 else
956 cachedPixmap = basePixmap;
957 if (QGuiApplication *guiApp = qobject_cast<QGuiApplication *>(qApp))
958 cachedPixmap = static_cast<QGuiApplicationPrivate*>(QObjectPrivate::get(guiApp))->applyQIconStyleHelper(mode, cachedPixmap);
959 cachedPixmap.setDevicePixelRatio(calculatedDpr);
960 QPixmapCache::insert(key, cachedPixmap);
961 }
962 return cachedPixmap;
963}
964
965QPixmap ScalableEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale)
966{
967 if (svgIcon.isNull())
968 svgIcon = QIcon(filename);
969
970 return svgIcon.pixmap(size, scale, mode, state);
971}
972
973QPixmap QIconLoaderEngine::pixmap(const QSize &size, QIcon::Mode mode,
974 QIcon::State state)
975{
976 return scaledPixmap(size, mode, state, 1.0);
977}
978
979QString QIconLoaderEngine::key() const
980{
981 return u"QIconLoaderEngine"_s;
982}
983
984QString QIconLoaderEngine::iconName()
985{
986 return m_info.iconName;
987}
988
989bool QIconLoaderEngine::isNull()
990{
991 return m_info.entries.empty();
992}
993
994QPixmap QIconLoaderEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale)
995{
996 const int integerScale = qCeil(scale);
997 QIconLoaderEngineEntry *entry = entryForSize(m_info, size, integerScale);
998 return entry ? entry->pixmap(size, mode, state, scale) : QPixmap();
999}
1000
1001QList<QSize> QIconLoaderEngine::availableSizes(QIcon::Mode mode, QIcon::State state)
1002{
1003 Q_UNUSED(mode);
1004 Q_UNUSED(state);
1005
1006 const qsizetype N = qsizetype(m_info.entries.size());
1007 QList<QSize> sizes;
1008 sizes.reserve(N);
1009
1010 // Gets all sizes from the DirectoryInfo entries
1011 for (const auto &entry : m_info.entries) {
1012 if (entry->dir.type == QIconDirInfo::Fallback) {
1013 sizes.append(QIcon(entry->filename).availableSizes());
1014 } else {
1015 int size = entry->dir.size;
1016 sizes.append(QSize(size, size));
1017 }
1018 }
1019 return sizes;
1020}
1021
1022QT_END_NAMESPACE
1023
1024#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)