QIcon: enable icon engine plugins to implement themes

So far, the keys of icon engine plugins were only interpreted as the
suffix of icon files, loaded via QIcon(filename). However, an icon
engine could provide a lot more flexibility if it could implement an
entire theme.

Match the list of keys a plugin can register itself with also against
the current theme name. If a matching plugin is found, use that plugin
to create the icon engine. Store the factory from the plugin to avoid
costly lookups for each icon.

Extend the QIcon test case by adding a custom plugin that supports two
themes. Since the plugin and icon engine creation infrastructure
doesn't communicate which theme the plugin was created for, use
QIcon::themeName to record the current theme when the engine gets
created.

[ChangeLog][QtGui][QIconEnginePlugin] The keys registered by an
QIconEnginePlugin implementation are now also matched against the
current theme (system or user theme), allowing engine providers
to implement entire themes through a plugin.

Change-Id: I8a5e30ff8b5bb7c78b5204e82760e4328671e4c1
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
This commit is contained in:
Volker Hilsheimer 2024-05-23 18:53:04 +02:00
parent 5831c31235
commit 87896c03c1
7 changed files with 150 additions and 1 deletions

View File

@ -137,6 +137,10 @@ void QIconLoader::invalidateKey()
// recreating the actual engine the next time the icon is used. // recreating the actual engine the next time the icon is used.
// We don't need to clear the QIcon cache itself. // We don't need to clear the QIcon cache itself.
m_themeKey++; m_themeKey++;
// invalidating the factory results in us looking once for
// a plugin that provides icon for the new themeName()
m_factory = std::nullopt;
} }
QString QIconLoader::themeName() const QString QIconLoader::themeName() const
@ -650,7 +654,18 @@ QIconEngine *QIconLoader::iconEngine(const QString &iconName) const
qCDebug(lcIconLoader) << "Resolving icon engine for icon" << iconName; qCDebug(lcIconLoader) << "Resolving icon engine for icon" << iconName;
std::unique_ptr<QIconEngine> iconEngine; std::unique_ptr<QIconEngine> iconEngine;
if (hasUserTheme())
if (!m_factory) {
qCDebug(lcIconLoader) << "Finding a plugin for theme" << themeName();
// try to find a plugin that supports the current theme
const int factoryIndex = qt_iconEngineFactoryLoader()->indexOf(themeName());
if (factoryIndex >= 0)
m_factory = qobject_cast<QIconEnginePlugin *>(qt_iconEngineFactoryLoader()->instance(factoryIndex));
}
if (m_factory && *m_factory)
iconEngine.reset(m_factory.value()->create(iconName));
if (hasUserTheme() && (!iconEngine || iconEngine->isNull()))
iconEngine.reset(new QIconLoaderEngine(iconName)); iconEngine.reset(new QIconLoaderEngine(iconName));
if (!iconEngine || iconEngine->isNull()) { if (!iconEngine || iconEngine->isNull()) {
qCDebug(lcIconLoader) << "Icon is not available from theme or fallback theme."; qCDebug(lcIconLoader) << "Icon is not available from theme or fallback theme.";

View File

@ -30,6 +30,7 @@
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <optional>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -161,6 +162,8 @@ public:
QList<QSharedPointer<QIconCacheGtkReader>> m_gtkCaches; QList<QSharedPointer<QIconCacheGtkReader>> m_gtkCaches;
}; };
class QIconEnginePlugin;
class Q_GUI_EXPORT QIconLoader class Q_GUI_EXPORT QIconLoader
{ {
public: public:
@ -195,6 +198,7 @@ private:
QThemeIconInfo lookupFallbackIcon(const QString &iconName) const; QThemeIconInfo lookupFallbackIcon(const QString &iconName) const;
uint m_themeKey; uint m_themeKey;
mutable std::optional<QIconEnginePlugin *> m_factory;
bool m_supportsSvg; bool m_supportsSvg;
bool m_initialized; bool m_initialized;

View File

@ -37,11 +37,14 @@ file(GLOB_RECURSE test_data_glob
*.svgz) *.svgz)
list(APPEND test_data ${test_data_glob}) list(APPEND test_data ${test_data_glob})
add_subdirectory(plugin)
qt_internal_add_test(tst_qicon qt_internal_add_test(tst_qicon
SOURCES SOURCES
tst_qicon.cpp tst_qicon.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
TestIconPlugin
TESTDATA ${test_data} TESTDATA ${test_data}
) )
@ -105,3 +108,5 @@ qt_internal_extend_target(tst_qicon CONDITION TARGET Qt::Widgets
LIBRARIES LIBRARIES
Qt::Widgets Qt::Widgets
) )
add_dependencies(tst_qicon TestIconPlugin)

View File

@ -0,0 +1,20 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## TestIconEngine Plugin:
#####################################################################
qt_internal_add_plugin(TestIconPlugin
STATIC
OUTPUT_NAME qtesticonplugin
OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
SKIP_INSTALL
PLUGIN_TYPE iconengines
DEFAULT_IF TRUE
SOURCES
main.cpp
LIBRARIES
Qt::Core
Qt::Gui
)

View File

@ -0,0 +1,73 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <qiconengineplugin.h>
#include <qiconengine.h>
QT_BEGIN_NAMESPACE
class TestIconPlugin : public QIconEnginePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QIconEngineFactoryInterface" FILE "plugin.json")
public:
QIconEngine *create(const QString &icon) override;
};
class TestIconEngine : public QIconEngine
{
public:
TestIconEngine(const QString &icon)
: m_iconName(QIcon::themeName() + "/" + icon)
{
}
~TestIconEngine()
{}
QIconEngine *clone() const override
{
return new TestIconEngine(m_iconName);
}
QString key() const override
{
return QStringLiteral("TestIconEngine");
}
QString iconName() override
{
return m_iconName;
}
bool isNull() override
{
return m_iconName.isNull();
}
QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override
{
return {{16, 16}, {48, 48}, {64, 64}};
}
void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override
{
Q_UNUSED(painter);
Q_UNUSED(rect);
Q_UNUSED(mode);
Q_UNUSED(state);
}
private:
const QString m_iconName;
};
QIconEngine *TestIconPlugin::create(const QString &icon)
{
return new TestIconEngine(icon);
}
QT_END_NAMESPACE
#include "main.moc"

View File

@ -0,0 +1,3 @@
{
"Keys": [ "plugintheme", "SpecialTheme" ]
}

View File

@ -48,6 +48,9 @@ private slots:
#endif #endif
void task223279_inconsistentAddFile(); void task223279_inconsistentAddFile();
void themeFromPlugin_data();
void themeFromPlugin();
private: private:
bool haveImageFormat(QByteArray const&); bool haveImageFormat(QByteArray const&);
@ -872,6 +875,32 @@ void tst_QIcon::task223279_inconsistentAddFile()
QCOMPARE(pm1.size(), pm2.size()); QCOMPARE(pm1.size(), pm2.size());
} }
Q_IMPORT_PLUGIN(TestIconPlugin)
void tst_QIcon::themeFromPlugin_data()
{
QTest::addColumn<QString>("themeName");
QTest::addRow("plugintheme") << "plugintheme";
QTest::addRow("specialtheme") << "specialTheme"; // deliberately not matching case
}
void tst_QIcon::themeFromPlugin()
{
QFETCH(const QString, themeName);
auto restoreTheme = qScopeGuard([oldTheme = QIcon::themeName()]{
QIcon::setThemeName(oldTheme);
});
QIcon icon = QIcon::fromTheme("icon1");
QVERIFY(icon.isNull());
QIcon::setThemeName(themeName);
icon = QIcon::fromTheme("icon1");
QVERIFY(!icon.isNull());
QCOMPARE(icon.name(), themeName + "/icon1");
}
QTEST_MAIN(tst_QIcon) QTEST_MAIN(tst_QIcon)
#include "tst_qicon.moc" #include "tst_qicon.moc"