/src/qtbase/src/gui/image/qiconloader.cpp
Line | Count | Source |
1 | | /**************************************************************************** |
2 | | ** |
3 | | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | | ** Contact: https://www.qt.io/licensing/ |
5 | | ** |
6 | | ** This file is part of the QtGui module of the Qt Toolkit. |
7 | | ** |
8 | | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | | ** Commercial License Usage |
10 | | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | | ** accordance with the commercial license agreement provided with the |
12 | | ** Software or, alternatively, in accordance with the terms contained in |
13 | | ** a written agreement between you and The Qt Company. For licensing terms |
14 | | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | | ** information use the contact form at https://www.qt.io/contact-us. |
16 | | ** |
17 | | ** GNU Lesser General Public License Usage |
18 | | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | | ** General Public License version 3 as published by the Free Software |
20 | | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | | ** packaging of this file. Please review the following information to |
22 | | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | | ** |
25 | | ** GNU General Public License Usage |
26 | | ** Alternatively, this file may be used under the terms of the GNU |
27 | | ** General Public License version 2.0 or (at your option) the GNU General |
28 | | ** Public license version 3 or any later version approved by the KDE Free |
29 | | ** Qt Foundation. The licenses are as published by the Free Software |
30 | | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | | ** included in the packaging of this file. Please review the following |
32 | | ** information to ensure the GNU General Public License requirements will |
33 | | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | | ** |
36 | | ** $QT_END_LICENSE$ |
37 | | ** |
38 | | ****************************************************************************/ |
39 | | #ifndef QT_NO_ICON |
40 | | #include <private/qiconloader_p.h> |
41 | | |
42 | | #include <private/qguiapplication_p.h> |
43 | | #include <private/qicon_p.h> |
44 | | |
45 | | #include <QtGui/QIconEnginePlugin> |
46 | | #include <QtGui/QPixmapCache> |
47 | | #include <qpa/qplatformtheme.h> |
48 | | #include <QtGui/QIconEngine> |
49 | | #include <QtGui/QPalette> |
50 | | #include <QtCore/qmath.h> |
51 | | #include <QtCore/QList> |
52 | | #include <QtCore/QDir> |
53 | | #if QT_CONFIG(settings) |
54 | | #include <QtCore/QSettings> |
55 | | #endif |
56 | | #include <QtGui/QPainter> |
57 | | |
58 | | #include <private/qhexstring_p.h> |
59 | | |
60 | | QT_BEGIN_NAMESPACE |
61 | | |
62 | | Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance) |
63 | | |
64 | | /* Theme to use in last resort, if the theme does not have the icon, neither the parents */ |
65 | | static QString systemFallbackThemeName() |
66 | 0 | { |
67 | 0 | if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) { |
68 | 0 | const QVariant themeHint = theme->themeHint(QPlatformTheme::SystemIconFallbackThemeName); |
69 | 0 | if (themeHint.isValid()) |
70 | 0 | return themeHint.toString(); |
71 | 0 | } |
72 | 0 | return QString(); |
73 | 0 | } |
74 | | |
75 | | QIconLoader::QIconLoader() : |
76 | 0 | m_themeKey(1), m_supportsSvg(false), m_initialized(false) |
77 | 0 | { |
78 | 0 | } |
79 | | |
80 | | static inline QString systemThemeName() |
81 | 0 | { |
82 | 0 | const auto override = qgetenv("QT_QPA_SYSTEM_ICON_THEME"); |
83 | 0 | if (!override.isEmpty()) |
84 | 0 | return QString::fromLocal8Bit(override); |
85 | 0 | if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) { |
86 | 0 | const QVariant themeHint = theme->themeHint(QPlatformTheme::SystemIconThemeName); |
87 | 0 | if (themeHint.isValid()) |
88 | 0 | return themeHint.toString(); |
89 | 0 | } |
90 | 0 | return QString(); |
91 | 0 | } |
92 | | |
93 | | static inline QStringList systemIconSearchPaths() |
94 | 0 | { |
95 | 0 | if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) { |
96 | 0 | const QVariant themeHint = theme->themeHint(QPlatformTheme::IconThemeSearchPaths); |
97 | 0 | if (themeHint.isValid()) |
98 | 0 | return themeHint.toStringList(); |
99 | 0 | } |
100 | 0 | return QStringList(); |
101 | 0 | } |
102 | | |
103 | | static inline QStringList systemFallbackSearchPaths() |
104 | 0 | { |
105 | 0 | if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) { |
106 | 0 | const QVariant themeHint = theme->themeHint(QPlatformTheme::IconFallbackSearchPaths); |
107 | 0 | if (themeHint.isValid()) |
108 | 0 | return themeHint.toStringList(); |
109 | 0 | } |
110 | 0 | return QStringList(); |
111 | 0 | } |
112 | | |
113 | | extern QFactoryLoader *qt_iconEngineFactoryLoader(); // qicon.cpp |
114 | | |
115 | | void QIconLoader::ensureInitialized() |
116 | 0 | { |
117 | 0 | if (!m_initialized) { |
118 | 0 | if (!QGuiApplicationPrivate::platformTheme()) |
119 | 0 | return; // it's too early: try again later (QTBUG-74252) |
120 | 0 | m_initialized = true; |
121 | 0 | m_systemTheme = systemThemeName(); |
122 | |
|
123 | 0 | if (m_systemTheme.isEmpty()) |
124 | 0 | m_systemTheme = systemFallbackThemeName(); |
125 | 0 | if (qt_iconEngineFactoryLoader()->keyMap().key(QLatin1String("svg"), -1) != -1) |
126 | 0 | m_supportsSvg = true; |
127 | 0 | } |
128 | 0 | } |
129 | | |
130 | | /*! |
131 | | \internal |
132 | | Gets an instance. |
133 | | |
134 | | \l QIcon::setFallbackThemeName() should be called before QGuiApplication is |
135 | | created, to avoid a race condition (QTBUG-74252). When this function is |
136 | | called from there, ensureInitialized() does not succeed because there |
137 | | is no QPlatformTheme yet, so systemThemeName() is empty, and we don't want |
138 | | m_systemTheme to get intialized to the fallback theme instead of the normal one. |
139 | | */ |
140 | | QIconLoader *QIconLoader::instance() |
141 | 0 | { |
142 | 0 | iconLoaderInstance()->ensureInitialized(); |
143 | 0 | return iconLoaderInstance(); |
144 | 0 | } |
145 | | |
146 | | // Queries the system theme and invalidates existing |
147 | | // icons if the theme has changed. |
148 | | void QIconLoader::updateSystemTheme() |
149 | 0 | { |
150 | | // Only change if this is not explicitly set by the user |
151 | 0 | if (m_userTheme.isEmpty()) { |
152 | 0 | QString theme = systemThemeName(); |
153 | 0 | if (theme.isEmpty()) |
154 | 0 | theme = fallbackThemeName(); |
155 | 0 | if (theme != m_systemTheme) { |
156 | 0 | m_systemTheme = theme; |
157 | 0 | invalidateKey(); |
158 | 0 | } |
159 | 0 | } |
160 | 0 | } |
161 | | |
162 | | void QIconLoader::setThemeName(const QString &themeName) |
163 | 0 | { |
164 | 0 | m_userTheme = themeName; |
165 | 0 | invalidateKey(); |
166 | 0 | } |
167 | | |
168 | | QString QIconLoader::fallbackThemeName() const |
169 | 0 | { |
170 | 0 | return m_userFallbackTheme.isEmpty() ? systemFallbackThemeName() : m_userFallbackTheme; |
171 | 0 | } |
172 | | |
173 | | void QIconLoader::setFallbackThemeName(const QString &themeName) |
174 | 0 | { |
175 | 0 | m_userFallbackTheme = themeName; |
176 | 0 | } |
177 | | |
178 | | void QIconLoader::setThemeSearchPath(const QStringList &searchPaths) |
179 | 0 | { |
180 | 0 | m_iconDirs = searchPaths; |
181 | 0 | themeList.clear(); |
182 | 0 | invalidateKey(); |
183 | 0 | } |
184 | | |
185 | | QStringList QIconLoader::themeSearchPaths() const |
186 | 0 | { |
187 | 0 | if (m_iconDirs.isEmpty()) { |
188 | 0 | m_iconDirs = systemIconSearchPaths(); |
189 | | // Always add resource directory as search path |
190 | 0 | m_iconDirs.append(QLatin1String(":/icons")); |
191 | 0 | } |
192 | 0 | return m_iconDirs; |
193 | 0 | } |
194 | | |
195 | | void QIconLoader::setFallbackSearchPaths(const QStringList &searchPaths) |
196 | 0 | { |
197 | 0 | m_fallbackDirs = searchPaths; |
198 | 0 | invalidateKey(); |
199 | 0 | } |
200 | | |
201 | | QStringList QIconLoader::fallbackSearchPaths() const |
202 | 0 | { |
203 | 0 | if (m_fallbackDirs.isEmpty()) { |
204 | 0 | m_fallbackDirs = systemFallbackSearchPaths(); |
205 | 0 | } |
206 | 0 | return m_fallbackDirs; |
207 | 0 | } |
208 | | |
209 | | /*! |
210 | | \internal |
211 | | Helper class that reads and looks up into the icon-theme.cache generated with |
212 | | gtk-update-icon-cache. If at any point we detect a corruption in the file |
213 | | (because the offsets point at wrong locations for example), the reader |
214 | | is marked as invalid. |
215 | | */ |
216 | | class QIconCacheGtkReader |
217 | | { |
218 | | public: |
219 | | explicit QIconCacheGtkReader(const QString &themeDir); |
220 | | QVector<const char *> lookup(const QStringRef &); |
221 | 0 | bool isValid() const { return m_isValid; } |
222 | | private: |
223 | | QFile m_file; |
224 | | const unsigned char *m_data; |
225 | | quint64 m_size; |
226 | | bool m_isValid; |
227 | | |
228 | | quint16 read16(uint offset) |
229 | 0 | { |
230 | 0 | if (offset > m_size - 2 || (offset & 0x1)) { |
231 | 0 | m_isValid = false; |
232 | 0 | return 0; |
233 | 0 | } |
234 | 0 | return m_data[offset+1] | m_data[offset] << 8; |
235 | 0 | } |
236 | | quint32 read32(uint offset) |
237 | 0 | { |
238 | 0 | if (offset > m_size - 4 || (offset & 0x3)) { |
239 | 0 | m_isValid = false; |
240 | 0 | return 0; |
241 | 0 | } |
242 | 0 | return m_data[offset+3] | m_data[offset+2] << 8 |
243 | 0 | | m_data[offset+1] << 16 | m_data[offset] << 24; |
244 | 0 | } |
245 | | }; |
246 | | |
247 | | |
248 | | QIconCacheGtkReader::QIconCacheGtkReader(const QString &dirName) |
249 | 0 | : m_isValid(false) |
250 | 0 | { |
251 | 0 | QFileInfo info(dirName + QLatin1String("/icon-theme.cache")); |
252 | 0 | if (!info.exists() || info.lastModified() < QFileInfo(dirName).lastModified()) |
253 | 0 | return; |
254 | 0 | m_file.setFileName(info.absoluteFilePath()); |
255 | 0 | if (!m_file.open(QFile::ReadOnly)) |
256 | 0 | return; |
257 | 0 | m_size = m_file.size(); |
258 | 0 | m_data = m_file.map(0, m_size); |
259 | 0 | if (!m_data) |
260 | 0 | return; |
261 | 0 | if (read16(0) != 1) // VERSION_MAJOR |
262 | 0 | return; |
263 | | |
264 | 0 | m_isValid = true; |
265 | | |
266 | | // Check that all the directories are older than the cache |
267 | 0 | auto lastModified = info.lastModified(); |
268 | 0 | quint32 dirListOffset = read32(8); |
269 | 0 | quint32 dirListLen = read32(dirListOffset); |
270 | 0 | for (uint i = 0; i < dirListLen; ++i) { |
271 | 0 | quint32 offset = read32(dirListOffset + 4 + 4 * i); |
272 | 0 | if (!m_isValid || offset >= m_size || lastModified < QFileInfo(dirName + QLatin1Char('/') |
273 | 0 | + QString::fromUtf8(reinterpret_cast<const char*>(m_data + offset))).lastModified()) { |
274 | 0 | m_isValid = false; |
275 | 0 | return; |
276 | 0 | } |
277 | 0 | } |
278 | 0 | } |
279 | | |
280 | | static quint32 icon_name_hash(const char *p) |
281 | 0 | { |
282 | 0 | quint32 h = static_cast<signed char>(*p); |
283 | 0 | for (p += 1; *p != '\0'; p++) |
284 | 0 | h = (h << 5) - h + *p; |
285 | 0 | return h; |
286 | 0 | } |
287 | | |
288 | | /*! \internal |
289 | | lookup the icon name and return the list of subdirectories in which an icon |
290 | | with this name is present. The char* are pointers to the mapped data. |
291 | | For example, this would return { "32x32/apps", "24x24/apps" , ... } |
292 | | */ |
293 | | QVector<const char *> QIconCacheGtkReader::lookup(const QStringRef &name) |
294 | 0 | { |
295 | 0 | QVector<const char *> ret; |
296 | 0 | if (!isValid() || name.isEmpty()) |
297 | 0 | return ret; |
298 | | |
299 | 0 | QByteArray nameUtf8 = name.toUtf8(); |
300 | 0 | quint32 hash = icon_name_hash(nameUtf8); |
301 | |
|
302 | 0 | quint32 hashOffset = read32(4); |
303 | 0 | quint32 hashBucketCount = read32(hashOffset); |
304 | |
|
305 | 0 | if (!isValid() || hashBucketCount == 0) { |
306 | 0 | m_isValid = false; |
307 | 0 | return ret; |
308 | 0 | } |
309 | | |
310 | 0 | quint32 bucketIndex = hash % hashBucketCount; |
311 | 0 | quint32 bucketOffset = read32(hashOffset + 4 + bucketIndex * 4); |
312 | 0 | while (bucketOffset > 0 && bucketOffset <= m_size - 12) { |
313 | 0 | quint32 nameOff = read32(bucketOffset + 4); |
314 | 0 | if (nameOff < m_size && strcmp(reinterpret_cast<const char*>(m_data + nameOff), nameUtf8) == 0) { |
315 | 0 | quint32 dirListOffset = read32(8); |
316 | 0 | quint32 dirListLen = read32(dirListOffset); |
317 | |
|
318 | 0 | quint32 listOffset = read32(bucketOffset+8); |
319 | 0 | quint32 listLen = read32(listOffset); |
320 | |
|
321 | 0 | if (!m_isValid || listOffset + 4 + 8 * listLen > m_size) { |
322 | 0 | m_isValid = false; |
323 | 0 | return ret; |
324 | 0 | } |
325 | | |
326 | 0 | ret.reserve(listLen); |
327 | 0 | for (uint j = 0; j < listLen && m_isValid; ++j) { |
328 | 0 | quint32 dirIndex = read16(listOffset + 4 + 8 * j); |
329 | 0 | quint32 o = read32(dirListOffset + 4 + dirIndex*4); |
330 | 0 | if (!m_isValid || dirIndex >= dirListLen || o >= m_size) { |
331 | 0 | m_isValid = false; |
332 | 0 | return ret; |
333 | 0 | } |
334 | 0 | ret.append(reinterpret_cast<const char*>(m_data) + o); |
335 | 0 | } |
336 | 0 | return ret; |
337 | 0 | } |
338 | 0 | bucketOffset = read32(bucketOffset); |
339 | 0 | } |
340 | 0 | return ret; |
341 | 0 | } |
342 | | |
343 | | QIconTheme::QIconTheme(const QString &themeName) |
344 | 0 | : m_valid(false) |
345 | 0 | { |
346 | 0 | QFile themeIndex; |
347 | |
|
348 | 0 | const QStringList iconDirs = QIcon::themeSearchPaths(); |
349 | 0 | for ( int i = 0 ; i < iconDirs.size() ; ++i) { |
350 | 0 | QDir iconDir(iconDirs[i]); |
351 | 0 | QString themeDir = iconDir.path() + QLatin1Char('/') + themeName; |
352 | 0 | QFileInfo themeDirInfo(themeDir); |
353 | |
|
354 | 0 | if (themeDirInfo.isDir()) { |
355 | 0 | m_contentDirs << themeDir; |
356 | 0 | m_gtkCaches << QSharedPointer<QIconCacheGtkReader>::create(themeDir); |
357 | 0 | } |
358 | |
|
359 | 0 | if (!m_valid) { |
360 | 0 | themeIndex.setFileName(themeDir + QLatin1String("/index.theme")); |
361 | 0 | if (themeIndex.exists()) |
362 | 0 | m_valid = true; |
363 | 0 | } |
364 | 0 | } |
365 | 0 | #if QT_CONFIG(settings) |
366 | 0 | if (themeIndex.exists()) { |
367 | 0 | const QSettings indexReader(themeIndex.fileName(), QSettings::IniFormat); |
368 | 0 | const QStringList keys = indexReader.allKeys(); |
369 | 0 | for (const QString &key : keys) { |
370 | 0 | if (key.endsWith(QLatin1String("/Size"))) { |
371 | | // Note the QSettings ini-format does not accept |
372 | | // slashes in key names, hence we have to cheat |
373 | 0 | if (int size = indexReader.value(key).toInt()) { |
374 | 0 | QString directoryKey = key.left(key.size() - 5); |
375 | 0 | QIconDirInfo dirInfo(directoryKey); |
376 | 0 | dirInfo.size = size; |
377 | 0 | QString type = indexReader.value(directoryKey + |
378 | 0 | QLatin1String("/Type") |
379 | 0 | ).toString(); |
380 | |
|
381 | 0 | if (type == QLatin1String("Fixed")) |
382 | 0 | dirInfo.type = QIconDirInfo::Fixed; |
383 | 0 | else if (type == QLatin1String("Scalable")) |
384 | 0 | dirInfo.type = QIconDirInfo::Scalable; |
385 | 0 | else |
386 | 0 | dirInfo.type = QIconDirInfo::Threshold; |
387 | |
|
388 | 0 | dirInfo.threshold = indexReader.value(directoryKey + |
389 | 0 | QLatin1String("/Threshold"), |
390 | 0 | 2).toInt(); |
391 | |
|
392 | 0 | dirInfo.minSize = indexReader.value(directoryKey + |
393 | 0 | QLatin1String("/MinSize"), |
394 | 0 | size).toInt(); |
395 | |
|
396 | 0 | dirInfo.maxSize = indexReader.value(directoryKey + |
397 | 0 | QLatin1String("/MaxSize"), |
398 | 0 | size).toInt(); |
399 | |
|
400 | 0 | dirInfo.scale = indexReader.value(directoryKey + |
401 | 0 | QLatin1String("/Scale"), |
402 | 0 | 1).toInt(); |
403 | 0 | m_keyList.append(dirInfo); |
404 | 0 | } |
405 | 0 | } |
406 | 0 | } |
407 | | |
408 | | // Parent themes provide fallbacks for missing icons |
409 | 0 | m_parents = indexReader.value( |
410 | 0 | QLatin1String("Icon Theme/Inherits")).toStringList(); |
411 | 0 | m_parents.removeAll(QString()); |
412 | | |
413 | | // Ensure a default platform fallback for all themes |
414 | 0 | if (m_parents.isEmpty()) { |
415 | 0 | const QString fallback = QIconLoader::instance()->fallbackThemeName(); |
416 | 0 | if (!fallback.isEmpty()) |
417 | 0 | m_parents.append(fallback); |
418 | 0 | } |
419 | | |
420 | | // Ensure that all themes fall back to hicolor |
421 | 0 | if (!m_parents.contains(QLatin1String("hicolor"))) |
422 | 0 | m_parents.append(QLatin1String("hicolor")); |
423 | 0 | } |
424 | 0 | #endif // settings |
425 | 0 | } |
426 | | |
427 | | QThemeIconInfo QIconLoader::findIconHelper(const QString &themeName, |
428 | | const QString &iconName, |
429 | | QStringList &visited) const |
430 | 0 | { |
431 | 0 | QThemeIconInfo info; |
432 | 0 | Q_ASSERT(!themeName.isEmpty()); |
433 | | |
434 | | // Used to protect against potential recursions |
435 | 0 | visited << themeName; |
436 | |
|
437 | 0 | QIconTheme &theme = themeList[themeName]; |
438 | 0 | if (!theme.isValid()) { |
439 | 0 | theme = QIconTheme(themeName); |
440 | 0 | if (!theme.isValid()) |
441 | 0 | theme = QIconTheme(fallbackThemeName()); |
442 | 0 | } |
443 | |
|
444 | 0 | const QStringList contentDirs = theme.contentDirs(); |
445 | |
|
446 | 0 | QStringRef iconNameFallback(&iconName); |
447 | | |
448 | | // Iterate through all icon's fallbacks in current theme |
449 | 0 | while (info.entries.isEmpty()) { |
450 | 0 | const QString svgIconName = iconNameFallback + QLatin1String(".svg"); |
451 | 0 | const QString pngIconName = iconNameFallback + QLatin1String(".png"); |
452 | | |
453 | | // Add all relevant files |
454 | 0 | for (int i = 0; i < contentDirs.size(); ++i) { |
455 | 0 | QVector<QIconDirInfo> subDirs = theme.keyList(); |
456 | | |
457 | | // Try to reduce the amount of subDirs by looking in the GTK+ cache in order to save |
458 | | // a massive amount of file stat (especially if the icon is not there) |
459 | 0 | auto cache = theme.m_gtkCaches.at(i); |
460 | 0 | if (cache->isValid()) { |
461 | 0 | const auto result = cache->lookup(iconNameFallback); |
462 | 0 | if (cache->isValid()) { |
463 | 0 | const QVector<QIconDirInfo> subDirsCopy = subDirs; |
464 | 0 | subDirs.clear(); |
465 | 0 | subDirs.reserve(result.count()); |
466 | 0 | for (const char *s : result) { |
467 | 0 | QString path = QString::fromUtf8(s); |
468 | 0 | auto it = std::find_if(subDirsCopy.cbegin(), subDirsCopy.cend(), |
469 | 0 | [&](const QIconDirInfo &info) { |
470 | 0 | return info.path == path; } ); |
471 | 0 | if (it != subDirsCopy.cend()) { |
472 | 0 | subDirs.append(*it); |
473 | 0 | } |
474 | 0 | } |
475 | 0 | } |
476 | 0 | } |
477 | |
|
478 | 0 | QString contentDir = contentDirs.at(i) + QLatin1Char('/'); |
479 | 0 | for (int j = 0; j < subDirs.size() ; ++j) { |
480 | 0 | const QIconDirInfo &dirInfo = subDirs.at(j); |
481 | 0 | const QString subDir = contentDir + dirInfo.path + QLatin1Char('/'); |
482 | 0 | const QString pngPath = subDir + pngIconName; |
483 | 0 | if (QFile::exists(pngPath)) { |
484 | 0 | PixmapEntry *iconEntry = new PixmapEntry; |
485 | 0 | iconEntry->dir = dirInfo; |
486 | 0 | iconEntry->filename = pngPath; |
487 | | // Notice we ensure that pixmap entries always come before |
488 | | // scalable to preserve search order afterwards |
489 | 0 | info.entries.prepend(iconEntry); |
490 | 0 | } else if (m_supportsSvg) { |
491 | 0 | const QString svgPath = subDir + svgIconName; |
492 | 0 | if (QFile::exists(svgPath)) { |
493 | 0 | ScalableEntry *iconEntry = new ScalableEntry; |
494 | 0 | iconEntry->dir = dirInfo; |
495 | 0 | iconEntry->filename = svgPath; |
496 | 0 | info.entries.append(iconEntry); |
497 | 0 | } |
498 | 0 | } |
499 | 0 | } |
500 | 0 | } |
501 | |
|
502 | 0 | if (!info.entries.isEmpty()) { |
503 | 0 | info.iconName = iconNameFallback.toString(); |
504 | 0 | break; |
505 | 0 | } |
506 | | |
507 | | // If it's possible - find next fallback for the icon |
508 | 0 | const int indexOfDash = iconNameFallback.lastIndexOf(QLatin1Char('-')); |
509 | 0 | if (indexOfDash == -1) |
510 | 0 | break; |
511 | | |
512 | 0 | iconNameFallback.truncate(indexOfDash); |
513 | 0 | } |
514 | |
|
515 | 0 | if (info.entries.isEmpty()) { |
516 | 0 | const QStringList parents = theme.parents(); |
517 | | // Search recursively through inherited themes |
518 | 0 | for (int i = 0 ; i < parents.size() ; ++i) { |
519 | |
|
520 | 0 | const QString parentTheme = parents.at(i).trimmed(); |
521 | |
|
522 | 0 | if (!visited.contains(parentTheme)) // guard against recursion |
523 | 0 | info = findIconHelper(parentTheme, iconName, visited); |
524 | |
|
525 | 0 | if (!info.entries.isEmpty()) // success |
526 | 0 | break; |
527 | 0 | } |
528 | 0 | } |
529 | 0 | return info; |
530 | 0 | } |
531 | | |
532 | | QThemeIconInfo QIconLoader::lookupFallbackIcon(const QString &iconName) const |
533 | 0 | { |
534 | 0 | QThemeIconInfo info; |
535 | |
|
536 | 0 | const QString pngIconName = iconName + QLatin1String(".png"); |
537 | 0 | const QString xpmIconName = iconName + QLatin1String(".xpm"); |
538 | 0 | const QString svgIconName = iconName + QLatin1String(".svg"); |
539 | |
|
540 | 0 | const auto searchPaths = QIcon::fallbackSearchPaths(); |
541 | 0 | for (const QString &iconDir: searchPaths) { |
542 | 0 | QDir currentDir(iconDir); |
543 | 0 | if (currentDir.exists(pngIconName)) { |
544 | 0 | PixmapEntry *iconEntry = new PixmapEntry; |
545 | 0 | iconEntry->dir.type = QIconDirInfo::Fallback; |
546 | 0 | iconEntry->filename = currentDir.filePath(pngIconName); |
547 | 0 | info.entries.append(iconEntry); |
548 | 0 | break; |
549 | 0 | } else if (currentDir.exists(xpmIconName)) { |
550 | 0 | PixmapEntry *iconEntry = new PixmapEntry; |
551 | 0 | iconEntry->dir.type = QIconDirInfo::Fallback; |
552 | 0 | iconEntry->filename = currentDir.filePath(xpmIconName); |
553 | 0 | info.entries.append(iconEntry); |
554 | 0 | break; |
555 | 0 | } else if (m_supportsSvg && |
556 | 0 | currentDir.exists(svgIconName)) { |
557 | 0 | ScalableEntry *iconEntry = new ScalableEntry; |
558 | 0 | iconEntry->dir.type = QIconDirInfo::Fallback; |
559 | 0 | iconEntry->filename = currentDir.filePath(svgIconName); |
560 | 0 | info.entries.append(iconEntry); |
561 | 0 | break; |
562 | 0 | } |
563 | 0 | } |
564 | |
|
565 | 0 | if (!info.entries.isEmpty()) |
566 | 0 | info.iconName = iconName; |
567 | |
|
568 | 0 | return info; |
569 | 0 | } |
570 | | |
571 | | QThemeIconInfo QIconLoader::loadIcon(const QString &name) const |
572 | 0 | { |
573 | 0 | if (!themeName().isEmpty()) { |
574 | 0 | QStringList visited; |
575 | 0 | const QThemeIconInfo iconInfo = findIconHelper(themeName(), name, visited); |
576 | 0 | if (!iconInfo.entries.isEmpty()) |
577 | 0 | return iconInfo; |
578 | | |
579 | 0 | return lookupFallbackIcon(name); |
580 | 0 | } |
581 | | |
582 | 0 | return QThemeIconInfo(); |
583 | 0 | } |
584 | | |
585 | | |
586 | | // -------- Icon Loader Engine -------- // |
587 | | |
588 | | |
589 | | QIconLoaderEngine::QIconLoaderEngine(const QString& iconName) |
590 | 0 | : m_iconName(iconName), m_key(0) |
591 | 0 | { |
592 | 0 | } |
593 | | |
594 | | QIconLoaderEngine::~QIconLoaderEngine() |
595 | 0 | { |
596 | 0 | qDeleteAll(m_info.entries); |
597 | 0 | } |
598 | | |
599 | | QIconLoaderEngine::QIconLoaderEngine(const QIconLoaderEngine &other) |
600 | 0 | : QIconEngine(other), |
601 | 0 | m_iconName(other.m_iconName), |
602 | 0 | m_key(0) |
603 | 0 | { |
604 | 0 | } |
605 | | |
606 | | QIconEngine *QIconLoaderEngine::clone() const |
607 | 0 | { |
608 | 0 | return new QIconLoaderEngine(*this); |
609 | 0 | } |
610 | | |
611 | 0 | bool QIconLoaderEngine::read(QDataStream &in) { |
612 | 0 | in >> m_iconName; |
613 | 0 | return true; |
614 | 0 | } |
615 | | |
616 | | bool QIconLoaderEngine::write(QDataStream &out) const |
617 | 0 | { |
618 | 0 | out << m_iconName; |
619 | 0 | return true; |
620 | 0 | } |
621 | | |
622 | | bool QIconLoaderEngine::hasIcon() const |
623 | 0 | { |
624 | 0 | return !(m_info.entries.isEmpty()); |
625 | 0 | } |
626 | | |
627 | | // Lazily load the icon |
628 | | void QIconLoaderEngine::ensureLoaded() |
629 | 0 | { |
630 | 0 | if (!(QIconLoader::instance()->themeKey() == m_key)) { |
631 | 0 | qDeleteAll(m_info.entries); |
632 | 0 | m_info.entries.clear(); |
633 | 0 | m_info.iconName.clear(); |
634 | |
|
635 | 0 | Q_ASSERT(m_info.entries.size() == 0); |
636 | 0 | m_info = QIconLoader::instance()->loadIcon(m_iconName); |
637 | 0 | m_key = QIconLoader::instance()->themeKey(); |
638 | 0 | } |
639 | 0 | } |
640 | | |
641 | | void QIconLoaderEngine::paint(QPainter *painter, const QRect &rect, |
642 | | QIcon::Mode mode, QIcon::State state) |
643 | 0 | { |
644 | 0 | const qreal dpr = !qApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ? |
645 | 0 | qreal(1.0) : painter->device()->devicePixelRatioF(); |
646 | |
|
647 | 0 | QSize pixmapSize = rect.size() * dpr; |
648 | 0 | painter->drawPixmap(rect, pixmap(pixmapSize, mode, state)); |
649 | 0 | } |
650 | | |
651 | | /* |
652 | | * This algorithm is defined by the freedesktop spec: |
653 | | * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html |
654 | | */ |
655 | | static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize, int iconscale) |
656 | 0 | { |
657 | 0 | if (dir.scale != iconscale) |
658 | 0 | return false; |
659 | | |
660 | 0 | if (dir.type == QIconDirInfo::Fixed) { |
661 | 0 | return dir.size == iconsize; |
662 | |
|
663 | 0 | } else if (dir.type == QIconDirInfo::Scalable) { |
664 | 0 | return iconsize <= dir.maxSize && |
665 | 0 | iconsize >= dir.minSize; |
666 | |
|
667 | 0 | } else if (dir.type == QIconDirInfo::Threshold) { |
668 | 0 | return iconsize >= dir.size - dir.threshold && |
669 | 0 | iconsize <= dir.size + dir.threshold; |
670 | 0 | } else if (dir.type == QIconDirInfo::Fallback) { |
671 | 0 | return true; |
672 | 0 | } |
673 | | |
674 | 0 | Q_ASSERT(1); // Not a valid value |
675 | 0 | return false; |
676 | 0 | } |
677 | | |
678 | | /* |
679 | | * This algorithm is defined by the freedesktop spec: |
680 | | * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html |
681 | | */ |
682 | | static int directorySizeDistance(const QIconDirInfo &dir, int iconsize, int iconscale) |
683 | 0 | { |
684 | 0 | const int scaledIconSize = iconsize * iconscale; |
685 | 0 | if (dir.type == QIconDirInfo::Fixed) { |
686 | 0 | return qAbs(dir.size * dir.scale - scaledIconSize); |
687 | |
|
688 | 0 | } else if (dir.type == QIconDirInfo::Scalable) { |
689 | 0 | if (scaledIconSize < dir.minSize * dir.scale) |
690 | 0 | return dir.minSize * dir.scale - scaledIconSize; |
691 | 0 | else if (scaledIconSize > dir.maxSize * dir.scale) |
692 | 0 | return scaledIconSize - dir.maxSize * dir.scale; |
693 | 0 | else |
694 | 0 | return 0; |
695 | |
|
696 | 0 | } else if (dir.type == QIconDirInfo::Threshold) { |
697 | 0 | if (scaledIconSize < (dir.size - dir.threshold) * dir.scale) |
698 | 0 | return dir.minSize * dir.scale - scaledIconSize; |
699 | 0 | else if (scaledIconSize > (dir.size + dir.threshold) * dir.scale) |
700 | 0 | return scaledIconSize - dir.maxSize * dir.scale; |
701 | 0 | else return 0; |
702 | 0 | } else if (dir.type == QIconDirInfo::Fallback) { |
703 | 0 | return 0; |
704 | 0 | } |
705 | | |
706 | 0 | Q_ASSERT(1); // Not a valid value |
707 | 0 | return INT_MAX; |
708 | 0 | } |
709 | | |
710 | | QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QThemeIconInfo &info, const QSize &size, int scale) |
711 | 0 | { |
712 | 0 | int iconsize = qMin(size.width(), size.height()); |
713 | | |
714 | | // Note that m_info.entries are sorted so that png-files |
715 | | // come first |
716 | |
|
717 | 0 | const int numEntries = info.entries.size(); |
718 | | |
719 | | // Search for exact matches first |
720 | 0 | for (int i = 0; i < numEntries; ++i) { |
721 | 0 | QIconLoaderEngineEntry *entry = info.entries.at(i); |
722 | 0 | if (directoryMatchesSize(entry->dir, iconsize, scale)) { |
723 | 0 | return entry; |
724 | 0 | } |
725 | 0 | } |
726 | | |
727 | | // Find the minimum distance icon |
728 | 0 | int minimalSize = INT_MAX; |
729 | 0 | QIconLoaderEngineEntry *closestMatch = nullptr; |
730 | 0 | for (int i = 0; i < numEntries; ++i) { |
731 | 0 | QIconLoaderEngineEntry *entry = info.entries.at(i); |
732 | 0 | int distance = directorySizeDistance(entry->dir, iconsize, scale); |
733 | 0 | if (distance < minimalSize) { |
734 | 0 | minimalSize = distance; |
735 | 0 | closestMatch = entry; |
736 | 0 | } |
737 | 0 | } |
738 | 0 | return closestMatch; |
739 | 0 | } |
740 | | |
741 | | /* |
742 | | * Returns the actual icon size. For scalable svg's this is equivalent |
743 | | * to the requested size. Otherwise the closest match is returned but |
744 | | * we can never return a bigger size than the requested size. |
745 | | * |
746 | | */ |
747 | | QSize QIconLoaderEngine::actualSize(const QSize &size, QIcon::Mode mode, |
748 | | QIcon::State state) |
749 | 0 | { |
750 | 0 | Q_UNUSED(mode); |
751 | 0 | Q_UNUSED(state); |
752 | |
|
753 | 0 | ensureLoaded(); |
754 | |
|
755 | 0 | QIconLoaderEngineEntry *entry = entryForSize(m_info, size); |
756 | 0 | if (entry) { |
757 | 0 | const QIconDirInfo &dir = entry->dir; |
758 | 0 | if (dir.type == QIconDirInfo::Scalable) { |
759 | 0 | return size; |
760 | 0 | } else if (dir.type == QIconDirInfo::Fallback) { |
761 | 0 | return QIcon(entry->filename).actualSize(size, mode, state); |
762 | 0 | } else { |
763 | 0 | int result = qMin<int>(dir.size, qMin(size.width(), size.height())); |
764 | 0 | return QSize(result, result); |
765 | 0 | } |
766 | 0 | } |
767 | 0 | return QSize(0, 0); |
768 | 0 | } |
769 | | |
770 | | QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) |
771 | 0 | { |
772 | 0 | Q_UNUSED(state); |
773 | | |
774 | | // Ensure that basePixmap is lazily initialized before generating the |
775 | | // key, otherwise the cache key is not unique |
776 | 0 | if (basePixmap.isNull()) |
777 | 0 | basePixmap.load(filename); |
778 | |
|
779 | 0 | QSize actualSize = basePixmap.size(); |
780 | | // If the size of the best match we have (basePixmap) is larger than the |
781 | | // requested size, we downscale it to match. |
782 | 0 | if (!actualSize.isNull() && (actualSize.width() > size.width() || actualSize.height() > size.height())) |
783 | 0 | actualSize.scale(size, Qt::KeepAspectRatio); |
784 | |
|
785 | 0 | QString key = QLatin1String("$qt_theme_") |
786 | 0 | % HexString<qint64>(basePixmap.cacheKey()) |
787 | 0 | % HexString<int>(mode) |
788 | 0 | % HexString<qint64>(QGuiApplication::palette().cacheKey()) |
789 | 0 | % HexString<int>(actualSize.width()) |
790 | 0 | % HexString<int>(actualSize.height()); |
791 | |
|
792 | 0 | QPixmap cachedPixmap; |
793 | 0 | if (QPixmapCache::find(key, &cachedPixmap)) { |
794 | 0 | return cachedPixmap; |
795 | 0 | } else { |
796 | 0 | if (basePixmap.size() != actualSize) |
797 | 0 | cachedPixmap = basePixmap.scaled(actualSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); |
798 | 0 | else |
799 | 0 | cachedPixmap = basePixmap; |
800 | 0 | if (QGuiApplication *guiApp = qobject_cast<QGuiApplication *>(qApp)) |
801 | 0 | cachedPixmap = static_cast<QGuiApplicationPrivate*>(QObjectPrivate::get(guiApp))->applyQIconStyleHelper(mode, cachedPixmap); |
802 | 0 | QPixmapCache::insert(key, cachedPixmap); |
803 | 0 | } |
804 | 0 | return cachedPixmap; |
805 | 0 | } |
806 | | |
807 | | QPixmap ScalableEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) |
808 | 0 | { |
809 | 0 | if (svgIcon.isNull()) |
810 | 0 | svgIcon = QIcon(filename); |
811 | | |
812 | | // Bypass QIcon API, as that will scale by device pixel ratio of the |
813 | | // highest DPR screen since we're not passing on any QWindow. |
814 | 0 | if (QIconEngine *engine = svgIcon.data_ptr() ? svgIcon.data_ptr()->engine : nullptr) |
815 | 0 | return engine->pixmap(size, mode, state); |
816 | | |
817 | 0 | return QPixmap(); |
818 | 0 | } |
819 | | |
820 | | QPixmap QIconLoaderEngine::pixmap(const QSize &size, QIcon::Mode mode, |
821 | | QIcon::State state) |
822 | 0 | { |
823 | 0 | ensureLoaded(); |
824 | |
|
825 | 0 | QIconLoaderEngineEntry *entry = entryForSize(m_info, size); |
826 | 0 | if (entry) |
827 | 0 | return entry->pixmap(size, mode, state); |
828 | | |
829 | 0 | return QPixmap(); |
830 | 0 | } |
831 | | |
832 | | QString QIconLoaderEngine::key() const |
833 | 0 | { |
834 | 0 | return QLatin1String("QIconLoaderEngine"); |
835 | 0 | } |
836 | | |
837 | | void QIconLoaderEngine::virtual_hook(int id, void *data) |
838 | 0 | { |
839 | 0 | ensureLoaded(); |
840 | |
|
841 | 0 | switch (id) { |
842 | 0 | case QIconEngine::AvailableSizesHook: |
843 | 0 | { |
844 | 0 | QIconEngine::AvailableSizesArgument &arg |
845 | 0 | = *reinterpret_cast<QIconEngine::AvailableSizesArgument*>(data); |
846 | 0 | const int N = m_info.entries.size(); |
847 | 0 | QList<QSize> sizes; |
848 | 0 | sizes.reserve(N); |
849 | | |
850 | | // Gets all sizes from the DirectoryInfo entries |
851 | 0 | for (int i = 0; i < N; ++i) { |
852 | 0 | const QIconLoaderEngineEntry *entry = m_info.entries.at(i); |
853 | 0 | if (entry->dir.type == QIconDirInfo::Fallback) { |
854 | 0 | sizes.append(QIcon(entry->filename).availableSizes()); |
855 | 0 | } else { |
856 | 0 | int size = entry->dir.size; |
857 | 0 | sizes.append(QSize(size, size)); |
858 | 0 | } |
859 | 0 | } |
860 | 0 | arg.sizes.swap(sizes); // commit |
861 | 0 | } |
862 | 0 | break; |
863 | 0 | case QIconEngine::IconNameHook: |
864 | 0 | { |
865 | 0 | QString &name = *reinterpret_cast<QString*>(data); |
866 | 0 | name = m_info.iconName; |
867 | 0 | } |
868 | 0 | break; |
869 | 0 | case QIconEngine::IsNullHook: |
870 | 0 | { |
871 | 0 | *reinterpret_cast<bool*>(data) = m_info.entries.isEmpty(); |
872 | 0 | } |
873 | 0 | break; |
874 | 0 | case QIconEngine::ScaledPixmapHook: |
875 | 0 | { |
876 | 0 | QIconEngine::ScaledPixmapArgument &arg = *reinterpret_cast<QIconEngine::ScaledPixmapArgument*>(data); |
877 | | // QIcon::pixmap() multiplies size by the device pixel ratio. |
878 | 0 | const int integerScale = qCeil(arg.scale); |
879 | 0 | QIconLoaderEngineEntry *entry = entryForSize(m_info, arg.size / integerScale, integerScale); |
880 | 0 | arg.pixmap = entry ? entry->pixmap(arg.size, arg.mode, arg.state) : QPixmap(); |
881 | 0 | } |
882 | 0 | break; |
883 | 0 | default: |
884 | 0 | QIconEngine::virtual_hook(id, data); |
885 | 0 | } |
886 | 0 | } |
887 | | |
888 | | QT_END_NAMESPACE |
889 | | |
890 | | #endif //QT_NO_ICON |