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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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