Coverage Report

Created: 2026-08-11 07:29

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