Introduce QIcon::fallbackSearchPaths()

... that will be used if an icon can't be found in the
current theme.

The Icon Theme Specification
https://standards.freedesktop.org/icon-theme-spec/latest/ar01s05.html
states that unthemed icons must be searched in the base directories,
i.e. /usr/share/icons, ... But in practice unthemed icons are
installed into /usr/share/pixmaps and this dir is not used as
a base dir for icon themes. So it's better to explicitly specify
fallback dirs to avoid needless access to the filesystem.

Also some KDE application install their own unthemed icons
(into /usr/share/<appname>/pics), that can't be found by
QIconLoader. With this change it would be possible for them
to specify dirs with unthemed icons and thus be displayed
correctly in non-KDE environments.

[ChangeLog][QtGui][QIcon] Added fallbackSearchPaths() that
will be used to find icons missing in the current icon theme.

Change-Id: I0dc55ba958b29356a3b0a2123d6b8faa24d4c91e
Task-number: QTBUG-33123
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
Reviewed-by: Olivier Goffart (Woboq GmbH) <ogoffart@woboq.com>
bb10
Alexander Volkov 2017-12-20 16:03:42 +03:00
parent 537af27302
commit 85aa0fd041
13 changed files with 155 additions and 9 deletions

View File

@ -79,3 +79,6 @@ void MyWidget::drawIcon(QPainter *painter, QPoint pos)
QIcon undoicon = QIcon::fromTheme("edit-undo", QIcon(":/undo.png"));
//! [4]
//! [5]
QIcon::setFallbackSearchPaths(QIcon::fallbackSearchPaths() << "my/search/path");
//! [5]

View File

@ -1157,6 +1157,36 @@ QStringList QIcon::themeSearchPaths()
return QIconLoader::instance()->themeSearchPaths();
}
/*!
\since 5.11
Returns the fallback search paths for icons.
The default value will depend on the platform.
\sa setFallbackSearchPaths(), themeSearchPaths()
*/
QStringList QIcon::fallbackSearchPaths()
{
return QIconLoader::instance()->fallbackSearchPaths();
}
/*!
\since 5.11
Sets the fallback search paths for icons to \a paths.
\note To add some path without replacing existing ones:
\snippet code/src_gui_image_qicon.cpp 5
\sa fallbackSearchPaths(), setThemeSearchPaths()
*/
void QIcon::setFallbackSearchPaths(const QStringList &paths)
{
QIconLoader::instance()->setFallbackSearchPaths(paths);
}
/*!
\since 4.6
@ -1216,7 +1246,10 @@ QString QIcon::themeName()
the lookup. These caches can be generated using gtk-update-icon-cache:
\l{https://developer.gnome.org/gtk3/stable/gtk-update-icon-cache.html}.
\sa themeName(), setThemeName(), themeSearchPaths()
\note If an icon can't be found in the current theme, then it will be
searched in fallbackSearchPaths() as an unthemed icon.
\sa themeName(), setThemeName(), themeSearchPaths(), fallbackSearchPaths()
*/
QIcon QIcon::fromTheme(const QString &name)
{

View File

@ -118,6 +118,9 @@ public:
static QStringList themeSearchPaths();
static void setThemeSearchPaths(const QStringList &searchpath);
static QStringList fallbackSearchPaths();
static void setFallbackSearchPaths(const QStringList &paths);
static QString themeName();
static void setThemeName(const QString &path);

View File

@ -95,6 +95,16 @@ static inline QStringList systemIconSearchPaths()
return QStringList();
}
static inline QStringList systemFallbackSearchPaths()
{
if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) {
const QVariant themeHint = theme->themeHint(QPlatformTheme::IconFallbackSearchPaths);
if (themeHint.isValid())
return themeHint.toStringList();
}
return QStringList();
}
extern QFactoryLoader *qt_iconEngineFactoryLoader(); // qicon.cpp
void QIconLoader::ensureInitialized()
@ -158,6 +168,20 @@ QStringList QIconLoader::themeSearchPaths() const
return m_iconDirs;
}
void QIconLoader::setFallbackSearchPaths(const QStringList &searchPaths)
{
m_fallbackDirs = searchPaths;
invalidateKey();
}
QStringList QIconLoader::fallbackSearchPaths() const
{
if (m_fallbackDirs.isEmpty()) {
m_fallbackDirs = systemFallbackSearchPaths();
}
return m_fallbackDirs;
}
/*!
\internal
Helper class that reads and looks up into the icon-theme.cache generated with
@ -481,11 +505,54 @@ QThemeIconInfo QIconLoader::findIconHelper(const QString &themeName,
return info;
}
QThemeIconInfo QIconLoader::lookupFallbackIcon(const QString &iconName) const
{
QThemeIconInfo info;
const QString pngIconName = iconName + QLatin1String(".png");
const QString xpmIconName = iconName + QLatin1String(".xpm");
const QString svgIconName = iconName + QLatin1String(".svg");
const auto searchPaths = QIcon::fallbackSearchPaths();
for (const QString &iconDir: searchPaths) {
QDir currentDir(iconDir);
if (currentDir.exists(pngIconName)) {
PixmapEntry *iconEntry = new PixmapEntry;
iconEntry->dir.type = QIconDirInfo::Fallback;
iconEntry->filename = currentDir.filePath(pngIconName);
info.entries.append(iconEntry);
break;
} else if (currentDir.exists(xpmIconName)) {
PixmapEntry *iconEntry = new PixmapEntry;
iconEntry->dir.type = QIconDirInfo::Fallback;
iconEntry->filename = currentDir.filePath(xpmIconName);
info.entries.append(iconEntry);
break;
} else if (m_supportsSvg &&
currentDir.exists(svgIconName)) {
ScalableEntry *iconEntry = new ScalableEntry;
iconEntry->dir.type = QIconDirInfo::Fallback;
iconEntry->filename = currentDir.filePath(svgIconName);
info.entries.append(iconEntry);
break;
}
}
if (!info.entries.isEmpty())
info.iconName = iconName;
return info;
}
QThemeIconInfo QIconLoader::loadIcon(const QString &name) const
{
if (!themeName().isEmpty()) {
QStringList visited;
return findIconHelper(themeName(), name, visited);
const QThemeIconInfo iconInfo = findIconHelper(themeName(), name, visited);
if (!iconInfo.entries.isEmpty())
return iconInfo;
return lookupFallbackIcon(name);
}
return QThemeIconInfo();
@ -573,6 +640,8 @@ static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize, int icon
} else if (dir.type == QIconDirInfo::Threshold) {
return iconsize >= dir.size - dir.threshold &&
iconsize <= dir.size + dir.threshold;
} else if (dir.type == QIconDirInfo::Fallback) {
return true;
}
Q_ASSERT(1); // Not a valid value
@ -603,6 +672,8 @@ static int directorySizeDistance(const QIconDirInfo &dir, int iconsize, int icon
else if (scaledIconSize > (dir.size + dir.threshold) * dir.scale)
return scaledIconSize - dir.maxSize * dir.scale;
else return 0;
} else if (dir.type == QIconDirInfo::Fallback) {
return 0;
}
Q_ASSERT(1); // Not a valid value
@ -657,9 +728,11 @@ QSize QIconLoaderEngine::actualSize(const QSize &size, QIcon::Mode mode,
QIconLoaderEngineEntry *entry = entryForSize(m_info, size);
if (entry) {
const QIconDirInfo &dir = entry->dir;
if (dir.type == QIconDirInfo::Scalable)
if (dir.type == QIconDirInfo::Scalable) {
return size;
else {
} else if (dir.type == QIconDirInfo::Fallback) {
return QIcon(entry->filename).actualSize(size, mode, state);
} else {
int result = qMin<int>(dir.size, qMin(size.width(), size.height()));
return QSize(result, result);
}
@ -745,8 +818,13 @@ void QIconLoaderEngine::virtual_hook(int id, void *data)
// Gets all sizes from the DirectoryInfo entries
for (int i = 0; i < N; ++i) {
int size = m_info.entries.at(i)->dir.size;
sizes.append(QSize(size, size));
const QIconLoaderEngineEntry *entry = m_info.entries.at(i);
if (entry->dir.type == QIconDirInfo::Fallback) {
sizes.append(QIcon(entry->filename).availableSizes());
} else {
int size = entry->dir.size;
sizes.append(QSize(size, size));
}
}
arg.sizes.swap(sizes); // commit
}

View File

@ -69,7 +69,7 @@ class QIconLoader;
struct QIconDirInfo
{
enum Type { Fixed, Scalable, Threshold };
enum Type { Fixed, Scalable, Threshold, Fallback };
QIconDirInfo(const QString &_path = QString()) :
path(_path),
size(0),
@ -180,6 +180,8 @@ public:
QIconTheme theme() { return themeList.value(themeName()); }
void setThemeSearchPath(const QStringList &searchPaths);
QStringList themeSearchPaths() const;
void setFallbackSearchPaths(const QStringList &searchPaths);
QStringList fallbackSearchPaths() const;
QIconDirInfo dirInfo(int dirindex);
static QIconLoader *instance();
void updateSystemTheme();
@ -191,6 +193,8 @@ private:
QThemeIconInfo findIconHelper(const QString &themeName,
const QString &iconName,
QStringList &visited) const;
QThemeIconInfo lookupFallbackIcon(const QString &iconName) const;
uint m_themeKey;
bool m_supportsSvg;
bool m_initialized;
@ -199,6 +203,7 @@ private:
mutable QString m_systemTheme;
mutable QStringList m_iconDirs;
mutable QHash <QString, QIconTheme> themeList;
mutable QStringList m_fallbackDirs;
};
QT_END_NAMESPACE

View File

@ -516,6 +516,8 @@ QVariant QPlatformTheme::defaultThemeHint(ThemeHint hint)
return QVariant(QString());
case QPlatformTheme::IconThemeSearchPaths:
return QVariant(QStringList());
case QPlatformTheme::IconFallbackSearchPaths:
return QVariant(QStringList());
case QPlatformTheme::StyleNames:
return QVariant(QStringList());
case QPlatformTheme::ShowShortcutsInContextMenus:

View File

@ -116,7 +116,8 @@ public:
MouseDoubleClickDistance,
WheelScrollLines,
TouchDoubleTapDistance,
ShowShortcutsInContextMenus
ShowShortcutsInContextMenus,
IconFallbackSearchPaths
};
enum DialogType {

View File

@ -178,6 +178,12 @@ QStringList QGenericUnixTheme::xdgIconThemePaths()
paths.append(xdgIconsDir.absoluteFilePath());
}
return paths;
}
QStringList QGenericUnixTheme::iconFallbackPaths()
{
QStringList paths;
const QFileInfo pixmapsIconsDir(QStringLiteral("/usr/share/pixmaps"));
if (pixmapsIconsDir.isDir())
paths.append(pixmapsIconsDir.absoluteFilePath());
@ -210,6 +216,8 @@ QVariant QGenericUnixTheme::themeHint(ThemeHint hint) const
return QVariant(QString(QStringLiteral("hicolor")));
case QPlatformTheme::IconThemeSearchPaths:
return xdgIconThemePaths();
case QPlatformTheme::IconFallbackSearchPaths:
return iconFallbackPaths();
case QPlatformTheme::DialogButtonBoxButtonsHaveIcons:
return QVariant(true);
case QPlatformTheme::StyleNames: {

View File

@ -85,6 +85,7 @@ public:
QVariant themeHint(ThemeHint hint) const override;
static QStringList xdgIconThemePaths();
static QStringList iconFallbackPaths();
#ifndef QT_NO_DBUS
QPlatformMenuBar *createPlatformMenuBar() const override;
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

@ -6,4 +6,4 @@ qtHaveModule(widgets): QT += widgets
SOURCES += tst_qicon.cpp
RESOURCES = tst_qicon.qrc tst_qicon.cpp
TESTDATA += icons/* second_icons/* *.png *.svg *.svgz
TESTDATA += icons/* second_icons/* fallback_icons/* *.png *.svg *.svgz

View File

@ -554,6 +554,11 @@ void tst_QIcon::fromTheme()
QCOMPARE(firstSearchPath, QIcon::themeSearchPaths()[0]);
QCOMPARE(secondSearchPath, QIcon::themeSearchPaths()[1]);
QString fallbackSearchPath = QStringLiteral(":/fallback_icons");
QIcon::setFallbackSearchPaths(QStringList() << fallbackSearchPath);
QCOMPARE(QIcon::fallbackSearchPaths().size(), 1);
QCOMPARE(fallbackSearchPath, QIcon::fallbackSearchPaths().at(0));
QString themeName("testtheme");
QIcon::setThemeName(themeName);
QCOMPARE(QIcon::themeName(), themeName);
@ -580,6 +585,12 @@ void tst_QIcon::fromTheme()
QVERIFY(QIcon::hasThemeIcon("address-book-new"));
QVERIFY(!abIcon.availableSizes().isEmpty());
// Test icon from fallback path
QIcon fallbackIcon = QIcon::fromTheme("red");
QVERIFY(!fallbackIcon.isNull());
QVERIFY(QIcon::hasThemeIcon("red"));
QCOMPARE(fallbackIcon.availableSizes().size(), 1);
// Test non existing icon
QIcon noIcon = QIcon::fromTheme("broken-icon");
QVERIFY(noIcon.isNull());

View File

@ -6,6 +6,7 @@
<file>./icons/testtheme/16x16/actions/appointment-new.png</file>
<file>./icons/testtheme/22x22/actions/appointment-new.png</file>
<file>./second_icons/testtheme/32x32/actions/appointment-new.png</file>
<file>./fallback_icons/red.png</file>
<file>./icons/testtheme/index.theme</file>
<file>./icons/testtheme/scalable/actions/svg-only.svg</file>
<file>./icons/themeparent/16x16/actions/address-book-new.png</file>