Coverage Report

Created: 2026-06-30 07:44

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