diff --git a/src/gui/text/qfontdatabase.cpp b/src/gui/text/qfontdatabase.cpp index 719411bf3c3..d3a13d801bb 100644 --- a/src/gui/text/qfontdatabase.cpp +++ b/src/gui/text/qfontdatabase.cpp @@ -670,7 +670,8 @@ static QStringList fallbacksForFamily(const QString &family, QFont::Style style, return *fallbacks; // make sure that the db has all fallback families - QStringList retList = QGuiApplicationPrivate::platformIntegration()->fontDatabase()->fallbacksForFamily(family,style,styleHint,script); + QStringList userFallbacks = db->applicationFallbackFontFamilies.value(script == QChar::Script_Common ? QChar::Script_Latin : script); + QStringList retList = userFallbacks + QGuiApplicationPrivate::platformIntegration()->fontDatabase()->fallbacksForFamily(family,style,styleHint,script); QStringList::iterator i; for (i = retList.begin(); i != retList.end(); ++i) { @@ -2359,6 +2360,126 @@ bool QFontDatabase::removeAllApplicationFonts() return true; } +/*! + \since 6.8 + + Adds \a familyName as an application-defined fallback font for \a script. + + When Qt encounters characters that are not supported by the selected font, it will search + through a list of fallback fonts to find a match for them. This ensures that combining multiple + scripts in a single string is possible, even if the main font does not support them. + + The list of fallback fonts is selected based on the script of the string as well as other + conditions, such as system language. + + While the system fallback list is usually sufficient, there are cases where it is useful + to override the default behavior. One such case is for using application fonts as fallback to + ensure cross-platform consistency. + + In another case the application may be written in a script with regional differences and want + to run it untranslated in multiple regions. In this case, it might be useful to override the + local region's fallback with one that matches the language of the application. + + By passing \a familyName to addApplicationFallbackFontFamily(), this will become the preferred + family when matching missing characters from \a script. The \a script must be a valid script + (\c QChar::Script_Latin or higher). When adding multiple fonts for the same script, they will + be prioritized in reverse order, so that the last family added will be checked first and so + on. + + \sa setApplicationFallbackFontFamilies(), removeApplicationFallbackFontFamily(), applicationFallbackFontFamilies() +*/ +void QFontDatabase::addApplicationFallbackFontFamily(QChar::Script script, const QString &familyName) +{ + QMutexLocker locker(fontDatabaseMutex()); + + if (script < QChar::Script_Latin) { + qCWarning(lcFontDb) << "Invalid script passed to addApplicationFallbackFontFamily:" << script; + return; + } + + auto *db = QFontDatabasePrivate::instance(); + auto it = db->applicationFallbackFontFamilies.find(script); + if (it == db->applicationFallbackFontFamilies.end()) + it = db->applicationFallbackFontFamilies.insert(script, QStringList{}); + + it->prepend(familyName); + db->fallbacksCache.clear(); +} + +/*! + \since 6.8 + + Removes \a familyName from the list of application-defined fallback fonts for \a script, + provided that it has previously been added with \l{addApplicationFallbackFontFamily()}. + + Returns true if the family name was in the list and false if it was not. + + \sa addApplicationFallbackFontFamily(), setApplicationFallbackFontFamilies(), applicationFallbackFontFamilies() +*/ +bool QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script script, const QString &familyName) +{ + QMutexLocker locker(fontDatabaseMutex()); + + auto *db = QFontDatabasePrivate::instance(); + auto it = db->applicationFallbackFontFamilies.find(script); + if (it != db->applicationFallbackFontFamilies.end()) { + if (it->removeAll(familyName) > 0) { + if (it->isEmpty()) + it = db->applicationFallbackFontFamilies.erase(it); + QFontCache::instance()->clear(); + db->fallbacksCache.clear(); + return true; + } + } + + return false; +} + +/*! + \since 6.8 + + Sets the list of application-defined fallback fonts for \a script to \a familyNames. + + When Qt encounters a character in \a script which is not supported by the current font, it will + check the families in \a familyNames, in order from first to last, until it finds a match. See + \l{addApplicationFallbackFontFamily()} for more details. + + This function overwrites the current list of application-defined fallback fonts for \a script. + + \sa addApplicationFallbackFontFamily(), removeApplicationFallbackFontFamily(), applicationFallbackFontFamilies() +*/ +void QFontDatabase::setApplicationFallbackFontFamilies(QChar::Script script, const QStringList &familyNames) +{ + QMutexLocker locker(fontDatabaseMutex()); + + if (script < QChar::Script_Latin) { + qCWarning(lcFontDb) << "Invalid script passed to setApplicationFallbackFontFamilies:" << script; + return; + } + + auto *db = QFontDatabasePrivate::instance(); + db->applicationFallbackFontFamilies[script] = familyNames; + + QFontCache::instance()->clear(); + db->fallbacksCache.clear(); +} + +/*! + \since 6.8 + + Returns the list of application-defined fallback font families previously added for \a script + by the \l{addApplicationFallbackFontFamily()} function. + + \sa setApplicationFallbackFontFamilies(), addApplicationFallbackFontFamily(), removeApplicationFallbackFontFamily() +*/ +QStringList QFontDatabase::applicationFallbackFontFamilies(QChar::Script script) +{ + QMutexLocker locker(fontDatabaseMutex()); + + auto *db = QFontDatabasePrivate::instance(); + return db->applicationFallbackFontFamilies.value(script); +} + /*! \internal */ diff --git a/src/gui/text/qfontdatabase.h b/src/gui/text/qfontdatabase.h index c66451a1644..91a534265e7 100644 --- a/src/gui/text/qfontdatabase.h +++ b/src/gui/text/qfontdatabase.h @@ -112,6 +112,11 @@ public: static bool removeApplicationFont(int id); static bool removeAllApplicationFonts(); + static void addApplicationFallbackFontFamily(QChar::Script script, const QString &familyName); + static bool removeApplicationFallbackFontFamily(QChar::Script script, const QString &familyName); + static void setApplicationFallbackFontFamilies(QChar::Script, const QStringList &familyNames); + static QStringList applicationFallbackFontFamilies(QChar::Script script); + static QFont systemFont(SystemFont type); }; diff --git a/src/gui/text/qfontdatabase_p.h b/src/gui/text/qfontdatabase_p.h index a0796d25c0a..38e1b4ad208 100644 --- a/src/gui/text/qfontdatabase_p.h +++ b/src/gui/text/qfontdatabase_p.h @@ -204,6 +204,8 @@ public: QtFontFamily **families; bool populated = false; + QHash applicationFallbackFontFamilies; + QCache fallbacksCache; struct ApplicationFont { QString fileName; diff --git a/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp b/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp index b2d00f6666a..849e7432d19 100644 --- a/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp +++ b/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp @@ -70,6 +70,8 @@ private slots: void findCourier(); #endif + void addApplicationFontFallback(); + private: QString m_ledFont; QString m_testFont; @@ -555,5 +557,83 @@ void tst_QFontDatabase::variableFont() QFontDatabase::removeApplicationFont(id); } +void tst_QFontDatabase::addApplicationFontFallback() +{ + int ledId = -1; + int id = -1; + auto cleanup = qScopeGuard([&id, &ledId] { + if (id >= 0) + QFontDatabase::removeApplicationFont(id); + if (ledId >= 0) + QFontDatabase::removeApplicationFont(ledId); + }); + + const QChar hebrewChar(0x05D0); // Hebrew 'aleph' + + ledId = QFontDatabase::addApplicationFont(m_ledFont); + if (ledId < 0) + QSKIP("Skip the test since app fonts are not supported on this system"); + + auto getHebrewFont = [&]() { + QTextLayout layout; + layout.setText(hebrewChar); + layout.setFont(QFont(u"LED Real"_s)); + layout.beginLayout(); + layout.createLine(); + layout.endLayout(); + + QList glyphRuns = layout.glyphRuns(); + if (glyphRuns.isEmpty()) + return QString{}; + + return glyphRuns.first().rawFont().familyName(); + }; + + QString defaultHebrewFont = getHebrewFont(); + if (defaultHebrewFont.isEmpty()) + QSKIP("Skip the test since Hebrew is not supported on this system"); + + QVERIFY(QFontDatabase::applicationFallbackFontFamilies(QChar::Script_Hebrew).isEmpty()); + QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Hebrew, u"QtBidiTestFont"_s); + + QCOMPARE(QFontDatabase::applicationFallbackFontFamilies(QChar::Script_Hebrew).size(), 1); + QCOMPARE(QFontDatabase::applicationFallbackFontFamilies(QChar::Script_Hebrew).first(), u"QtBidiTestFont"_s); + + { + QString hebrewFontNow = getHebrewFont(); + QCOMPARE(hebrewFontNow, defaultHebrewFont); + } + + id = QFontDatabase::addApplicationFont(m_testFont); + QVERIFY(id >= 0); + + { + QString hebrewFontNow = getHebrewFont(); + QCOMPARE(hebrewFontNow, u"QtBidiTestFont"_s); + } + + QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script_Hebrew, u"QtBidiTestFont"_s); + + { + QString hebrewFontNow = getHebrewFont(); + QCOMPARE(hebrewFontNow, defaultHebrewFont); + } + + QFontDatabase::setApplicationFallbackFontFamilies(QChar::Script_Hebrew, QStringList(u"QtBidiTestFont"_s)); + + { + QString hebrewFontNow = getHebrewFont(); + QCOMPARE(hebrewFontNow, u"QtBidiTestFont"_s); + } + + QFontDatabase::setApplicationFallbackFontFamilies(QChar::Script_Hebrew, QStringList{}); + + { + QString hebrewFontNow = getHebrewFont(); + QCOMPARE(hebrewFontNow, defaultHebrewFont); + } + +} + QTEST_MAIN(tst_QFontDatabase) #include "tst_qfontdatabase.moc"