From b3788c7bfc04a9ad5538c4a05a9adb4e1d565249 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Thu, 14 Nov 2024 12:33:45 +0100 Subject: [PATCH] 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 --- src/gui/image/qfonticonengine.cpp | 82 ++++++++++++++++++++++++++----- src/gui/image/qfonticonengine_p.h | 7 +++ src/gui/image/qiconloader.cpp | 19 ++++++- tests/manual/iconbrowser/main.cpp | 38 +++++++++++++- 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/gui/image/qfonticonengine.cpp b/src/gui/image/qfonticonengine.cpp index cca04b66ef9..e8e76271d19 100644 --- a/src/gui/image/qfonticonengine.cpp +++ b/src/gui/image/qfonticonengine.cpp @@ -11,8 +11,12 @@ #include #include +#include #include +#include +#include + 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 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 diff --git a/src/gui/image/qfonticonengine_p.h b/src/gui/image/qfonticonengine_p.h index 8cc967ec95c..c3a29add913 100644 --- a/src/gui/image/qfonticonengine_p.h +++ b/src/gui/image/qfonticonengine_p.h @@ -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 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::max(); + mutable glyph_t m_glyph = uninitializedGlyph; }; QT_END_NAMESPACE diff --git a/src/gui/image/qiconloader.cpp b/src/gui/image/qiconloader.cpp index e8d97b61e21..006b5f2793a 100644 --- a/src/gui/image/qiconloader.cpp +++ b/src/gui/image/qiconloader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +#include 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()) { diff --git a/tests/manual/iconbrowser/main.cpp b/tests/manual/iconbrowser/main.cpp index 1e4c7efe71e..c22dcbf7113 100644 --- a/tests/manual/iconbrowser/main.cpp +++ b/tests/manual/iconbrowser/main.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include +#include #include #include @@ -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