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 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | Q_LOGGING_CATEGORY(lcIconLoader, "qt.gui.icon.loader") |
28 | |
29 | using namespace Qt::StringLiterals; |
30 | |
31 | Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance) |
32 | |
33 | /* Theme to use in last resort, if the theme does not have the icon, neither the parents */ |
34 | static 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 | |
44 | QIconLoader::QIconLoader() : |
45 | m_themeKey(1), m_supportsSvg(false), m_initialized(false) |
46 | { |
47 | } |
48 | |
49 | static 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 | |
62 | static 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 | |
72 | static 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 | |
82 | extern QFactoryLoader *qt_iconEngineFactoryLoader(); // qicon.cpp |
83 | |
84 | void 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 | */ |
112 | QIconLoader *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. |
120 | void 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 | |
134 | void 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 | |
146 | QString QIconLoader::themeName() const |
147 | { |
148 | return m_userTheme.isEmpty() ? m_systemTheme : m_userTheme; |
149 | } |
150 | |
151 | void 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 | |
168 | QString QIconLoader::fallbackThemeName() const |
169 | { |
170 | return m_userFallbackTheme.isEmpty() ? systemFallbackThemeName() : m_userFallbackTheme; |
171 | } |
172 | |
173 | void QIconLoader::setFallbackThemeName(const QString &themeName) |
174 | { |
175 | qCDebug(lcIconLoader) << "Setting fallback theme name to"<< themeName; |
176 | m_userFallbackTheme = themeName; |
177 | invalidateKey(); |
178 | } |
179 | |
180 | void 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 | |
188 | QStringList 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 | |
198 | void QIconLoader::setFallbackSearchPaths(const QStringList &searchPaths) |
199 | { |
200 | qCDebug(lcIconLoader) << "Setting fallback search path to"<< searchPaths; |
201 | m_fallbackDirs = searchPaths; |
202 | invalidateKey(); |
203 | } |
204 | |
205 | QStringList 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 | */ |
220 | class QIconCacheGtkReader |
221 | { |
222 | public: |
223 | explicit QIconCacheGtkReader(const QString &themeDir); |
224 | QList<const char *> lookup(QStringView); |
225 | bool isValid() const { return m_isValid; } |
226 | private: |
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 | |
252 | QIconCacheGtkReader::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 | |
284 | static 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 | */ |
297 | QList<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 | |
347 | QIconTheme::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 | |
422 | QStringList 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 | |
439 | QDebug operator<<(QDebug debug, const std::unique_ptr<QIconLoaderEngineEntry> &entry) |
440 | { |
441 | QDebugStateSaver saver(debug); |
442 | debug.noquote() << entry->filename; |
443 | return debug; |
444 | } |
445 | |
446 | QThemeIconInfo 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 | |
572 | QThemeIconInfo 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 | |
612 | QThemeIconInfo 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 |
633 | QDebug 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 | |
652 | QIconEngine *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 | |
704 | QThemeIconEngine::QThemeIconEngine(const QString& iconName) |
705 | : QProxyIconEngine() |
706 | , m_iconName(iconName) |
707 | { |
708 | } |
709 | |
710 | QThemeIconEngine::QThemeIconEngine(const QThemeIconEngine &other) |
711 | : QProxyIconEngine() |
712 | , m_iconName(other.m_iconName) |
713 | { |
714 | } |
715 | |
716 | QString 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 | |
724 | QIconEngine *QThemeIconEngine::clone() const |
725 | { |
726 | return new QThemeIconEngine(*this); |
727 | } |
728 | |
729 | bool QThemeIconEngine::read(QDataStream &in) { |
730 | in >> m_iconName; |
731 | return true; |
732 | } |
733 | |
734 | bool QThemeIconEngine::write(QDataStream &out) const |
735 | { |
736 | out << m_iconName; |
737 | return true; |
738 | } |
739 | |
740 | QIconEngine *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 | |
764 | QIconLoaderEngine::QIconLoaderEngine(const QString& iconName) |
765 | : m_iconName(iconName) |
766 | , m_info(QIconLoader::instance()->loadIcon(name: m_iconName)) |
767 | { |
768 | } |
769 | |
770 | QIconLoaderEngine::~QIconLoaderEngine() = default; |
771 | |
772 | QIconEngine *QIconLoaderEngine::clone() const |
773 | { |
774 | Q_UNREACHABLE(); |
775 | return nullptr; // Cannot be cloned |
776 | } |
777 | |
778 | bool QIconLoaderEngine::hasIcon() const |
779 | { |
780 | return !(m_info.entries.empty()); |
781 | } |
782 | |
783 | void 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 | */ |
794 | static 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 | */ |
821 | static 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 | |
849 | QIconLoaderEngineEntry *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 | */ |
882 | QSize 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 | |
903 | QPixmap 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 | |
940 | QPixmap 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 | |
948 | QPixmap QIconLoaderEngine::pixmap(const QSize &size, QIcon::Mode mode, |
949 | QIcon::State state) |
950 | { |
951 | return scaledPixmap(size, mode, state, scale: 1.0); |
952 | } |
953 | |
954 | QString QIconLoaderEngine::key() const |
955 | { |
956 | return u"QIconLoaderEngine"_s; |
957 | } |
958 | |
959 | QString QIconLoaderEngine::iconName() |
960 | { |
961 | return m_info.iconName; |
962 | } |
963 | |
964 | bool QIconLoaderEngine::isNull() |
965 | { |
966 | return m_info.entries.empty(); |
967 | } |
968 | |
969 | QPixmap 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 | |
976 | QList<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 | |
997 | QT_END_NAMESPACE |
998 | |
999 | #endif //QT_NO_ICON |
1000 |
Definitions
- lcIconLoader
- iconLoaderInstance
- systemFallbackThemeName
- QIconLoader
- systemThemeName
- systemIconSearchPaths
- systemFallbackSearchPaths
- ensureInitialized
- instance
- updateSystemTheme
- invalidateKey
- themeName
- setThemeName
- fallbackThemeName
- setFallbackThemeName
- setThemeSearchPath
- themeSearchPaths
- setFallbackSearchPaths
- fallbackSearchPaths
- QIconCacheGtkReader
- isValid
- read16
- read32
- QIconCacheGtkReader
- icon_name_hash
- lookup
- QIconTheme
- parents
- operator<<
- findIconHelper
- lookupFallbackIcon
- loadIcon
- operator<<
- iconEngine
- QThemeIconEngine
- QThemeIconEngine
- key
- clone
- read
- write
- proxiedEngine
- QIconLoaderEngine
- ~QIconLoaderEngine
- clone
- hasIcon
- paint
- directoryMatchesSize
- directorySizeDistance
- entryForSize
- actualSize
- pixmap
- pixmap
- pixmap
- key
- iconName
- isNull
- scaledPixmap
Learn to use CMake with our Intro Training
Find out more