QFontIconEngine: render named glyphs as icons, if possible

If the QIcon::themeName matches an installed font, and if the
name of the icon matches a named glyph in the font, then render
that glyph as a painter path.

Overrides of QFontIconEngine::text() take priority.

Amend the manual test to allow specifying an icon theme on the
command line, and render the named glyph also as text, as
some icon fonts will define ligatures that turn the string into
the corresponding icon.

Task-number: QTBUG-102346
Change-Id: I788c6274322359955cbfe58175a2999a57cfce95
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
This commit is contained in:
Volker Hilsheimer 2024-11-14 12:33:45 +01:00
parent 2af58490b3
commit b3788c7bfc
4 changed files with 130 additions and 16 deletions

View File

@ -11,8 +11,12 @@
#include <QtGui/qfontdatabase.h>
#include <QtGui/qpainter.h>
#include <QtGui/qpainterpath.h>
#include <QtGui/qpalette.h>
#include <QtGui/private/qfont_p.h>
#include <QtGui/private/qfontengine_p.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
@ -25,6 +29,16 @@ QFontIconEngine::QFontIconEngine(const QString &iconName, const QFont &font)
QFontIconEngine::~QFontIconEngine() = default;
QIconEngine *QFontIconEngine::clone() const
{
return new QFontIconEngine(m_iconName, m_iconFont);
}
QString QFontIconEngine::key() const
{
return u"QFontIconEngine("_s + m_iconFont.key() + u')';
}
QString QFontIconEngine::iconName()
{
return m_iconName;
@ -33,13 +47,15 @@ QString QFontIconEngine::iconName()
bool QFontIconEngine::isNull()
{
const QString text = string();
if (text.isEmpty())
return true;
const QChar c0 = text.at(0);
const QFontMetrics fontMetrics(m_iconFont);
if (c0.category() == QChar::Other_Surrogate && text.size() > 1)
return !fontMetrics.inFontUcs4(QChar::surrogateToUcs4(c0, text.at(1)));
return !fontMetrics.inFont(c0);
if (!text.isEmpty()) {
const QChar c0 = text.at(0);
const QFontMetrics fontMetrics(m_iconFont);
if (c0.isHighSurrogate() && text.size() > 1)
return !fontMetrics.inFontUcs4(QChar::surrogateToUcs4(c0, text.at(1)));
return !fontMetrics.inFont(c0);
}
return glyph() == 0;
}
QList<QSize> QFontIconEngine::availableSizes(QIcon::Mode, QIcon::State)
@ -84,27 +100,57 @@ void QFontIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mo
painter->save();
QFont renderFont(m_iconFont);
renderFont.setPixelSize(rect.height());
painter->setFont(renderFont);
QColor color = Qt::black;
QPalette palette;
switch (mode) {
case QIcon::Active:
painter->setPen(palette.color(QPalette::Active, QPalette::Text));
color = palette.color(QPalette::Active, QPalette::Text);
break;
case QIcon::Normal:
painter->setPen(palette.color(QPalette::Active, QPalette::Text));
color = palette.color(QPalette::Active, QPalette::Text);
break;
case QIcon::Disabled:
painter->setPen(palette.color(QPalette::Disabled, QPalette::Text));
color = palette.color(QPalette::Disabled, QPalette::Text);
break;
case QIcon::Selected:
painter->setPen(palette.color(QPalette::Active, QPalette::HighlightedText));
color = palette.color(QPalette::Active, QPalette::HighlightedText);
break;
}
const QString text = string();
if (!text.isEmpty()) {
painter->setFont(renderFont);
painter->setPen(color);
painter->drawText(rect, Qt::AlignCenter, text);
} else if (glyph_t glyphIndex = glyph()) {
QFontEngine *engine = QFontPrivate::get(renderFont)->engineForScript(QChar::Script_Common);
painter->drawText(rect, Qt::AlignCenter, text);
const glyph_metrics_t gm = engine->boundingBox(glyphIndex);
const int glyph_x = qFloor(gm.x.toReal());
const int glyph_y = qFloor(gm.y.toReal());
const int glyph_width = qCeil((gm.x + gm.width).toReal()) - glyph_x;
const int glyph_height = qCeil((gm.y + gm.height).toReal()) - glyph_y;
QPainterPath path;
if (glyph_width > 0 && glyph_height > 0) {
QFixedPoint pt(QFixed(-glyph_x), QFixed(-glyph_y));
path.setFillRule(Qt::WindingFill);
engine->addGlyphsToPath(&glyphIndex, &pt, 1, &path, {});
// make the glyph fit tightly into rect
const QRectF pathBoundingRect = path.boundingRect();
// center the glyph inside the rect
const QPointF topLeft = rect.topLeft() - pathBoundingRect.topLeft()
+ (QPointF(rect.width(), rect.height())
- QPointF(pathBoundingRect.width(), pathBoundingRect.height())) / 2;
painter->translate(topLeft);
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(Qt::NoPen);
painter->setBrush(color);
painter->drawPath(path);
}
}
painter->restore();
}
@ -113,6 +159,16 @@ QString QFontIconEngine::string() const
return {};
}
glyph_t QFontIconEngine::glyph() const
{
if (m_glyph == uninitializedGlyph) {
QFontEngine *engine = QFontPrivate::get(m_iconFont)->engineForScript(QChar::Script_Common);
if (engine)
m_glyph = engine->findGlyph(QLatin1StringView(m_iconName.toLatin1()));
}
return m_glyph;
}
QT_END_NAMESPACE
#endif // QT_NO_ICON

View File

@ -22,6 +22,8 @@
QT_BEGIN_NAMESPACE
using glyph_t = quint32;
class Q_GUI_EXPORT QFontIconEngine : public QIconEngine
{
public:
@ -30,6 +32,8 @@ public:
QString iconName() override;
bool isNull() override;
QString key() const override;
QIconEngine *clone() const override;
QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override;
QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override;
@ -39,6 +43,7 @@ public:
protected:
virtual QString string() const;
virtual glyph_t glyph() const;
private:
static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state)
@ -50,6 +55,8 @@ private:
const QFont m_iconFont;
mutable QPixmap m_pixmap;
mutable quint64 m_pixmapCacheKey = {};
static constexpr glyph_t uninitializedGlyph = std::numeric_limits<glyph_t>::max();
mutable glyph_t m_glyph = uninitializedGlyph;
};
QT_END_NAMESPACE

View File

@ -9,6 +9,7 @@
#include <QtGui/QIconEnginePlugin>
#include <QtGui/QPixmapCache>
#include <qpa/qplatformtheme.h>
#include <QtGui/qfontdatabase.h>
#include <QtGui/QPalette>
#include <QtCore/qmath.h>
#include <QtCore/QList>
@ -21,6 +22,7 @@
#include <private/qhexstring_p.h>
#include <private/qfactoryloader_p.h>
#include <private/qfonticonengine_p.h>
QT_BEGIN_NAMESPACE
@ -665,8 +667,21 @@ QIconEngine *QIconLoader::iconEngine(const QString &iconName) const
if (m_factory && *m_factory)
iconEngine.reset(m_factory.value()->create(iconName));
if (hasUserTheme() && (!iconEngine || iconEngine->isNull()))
iconEngine.reset(new QIconLoaderEngine(iconName));
if (hasUserTheme()) {
if (!iconEngine || iconEngine->isNull()) {
if (QFontDatabase::families().contains(themeName())) {
QFont maybeIconFont(themeName());
maybeIconFont.setStyleStrategy(QFont::NoFontMerging);
qCDebug(lcIconLoader) << "Trying font icon engine.";
iconEngine.reset(new QFontIconEngine(iconName, maybeIconFont));
}
}
if (!iconEngine || iconEngine->isNull()) {
qCDebug(lcIconLoader) << "Trying loader engine for theme.";
iconEngine.reset(new QIconLoaderEngine(iconName));
}
}
if (!iconEngine || iconEngine->isNull()) {
qCDebug(lcIconLoader) << "Icon is not available from theme or fallback theme.";
if (auto *platformTheme = QGuiApplicationPrivate::platformTheme()) {

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtWidgets>
#include <QtCore/QCommandLineParser>
#include <QtWidgets/private/qapplication_p.h>
#include <QtGui/qpa/qplatformtheme.h>
@ -497,9 +498,15 @@ public:
connect(lineEdit, &QLineEdit::textChanged,
this, &IconInspector::updateIcon);
button = new QToolButton;
button->setCheckable(true);
QVBoxLayout *vbox = new QVBoxLayout;
QHBoxLayout *hbox = new QHBoxLayout;
vbox->addStretch(10);
vbox->addWidget(lineEdit);
hbox->addWidget(lineEdit);
hbox->addWidget(button);
vbox->addLayout(hbox);
setLayout(vbox);
}
@ -508,6 +515,18 @@ protected:
{
QPainter painter(this);
painter.fillRect(event->rect(), palette().window());
// some fonts use icon names as ligatures
if (const QString themeName = QIcon::themeName(); !themeName.isEmpty()) {
const QFont themeFont(themeName, 24);
if (QFontInfo(themeFont).family() == themeName) {
painter.save();
painter.setFont(themeFont);
painter.drawText(rect(), icon.name());
painter.restore();
}
}
if (!icon.isNull()) {
const QString modeLabels[] = { u"Normal"_s, u"Disabled"_s, u"Active"_s, u"Selected"_s};
const QString stateLabels[] = { u"On"_s, u"Off"_s};
@ -555,10 +574,12 @@ protected:
QFrame::paintEvent(event);
}
private:
QToolButton *button;
QIcon icon;
void updateIcon(const QString &iconName)
{
icon = QIcon::fromTheme(iconName);
button->setIcon(icon);
update();
}
};
@ -567,6 +588,21 @@ int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QApplication::setApplicationVersion(QT_VERSION_STR);
QApplication::setApplicationName(QLatin1String("IconBrowser Manual Test"));
QApplication::setOrganizationName(QLatin1String("QtProject"));
QCommandLineParser parser;
parser.setApplicationDescription(QApplication::applicationName());
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption themeOption({u"theme"_s, u"t"_s},
u"The name of the icon theme"_s, u"theme"_s);
parser.addOption(themeOption);
parser.process(app);
if (const QString theme = parser.value(themeOption); !theme.isEmpty())
QIcon::setThemeName(theme);
#ifdef ICONBROWSER_RESOURCE
Q_INIT_RESOURCE(icons);
#endif