From 3ea9c6503ba86a4ef1efff32f17182a93023cb4e Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 8 May 2022 20:22:23 +0200 Subject: [PATCH] Font choice rework (#6670) * Font choice rework * Font choice rework - naming * Font choice rework - fix default font selection --- .../src/com/unciv/app/NativeFontAndroid.kt | 72 ++++++++++++++----- core/src/com/unciv/UncivGame.kt | 2 +- core/src/com/unciv/ui/utils/BaseScreen.kt | 4 +- core/src/com/unciv/ui/utils/Fonts.kt | 52 +++++++++++--- .../ui/worldscreen/mainmenu/OptionsPopup.kt | 56 +++++++++------ .../unciv/app/desktop/NativeFontDesktop.kt | 15 ++-- 6 files changed, 141 insertions(+), 60 deletions(-) diff --git a/android/src/com/unciv/app/NativeFontAndroid.kt b/android/src/com/unciv/app/NativeFontAndroid.kt index 82b8884415..2dbddcb271 100755 --- a/android/src/com/unciv/app/NativeFontAndroid.kt +++ b/android/src/com/unciv/app/NativeFontAndroid.kt @@ -6,20 +6,42 @@ import android.graphics.Paint import android.graphics.Typeface import android.graphics.fonts.Font import android.graphics.fonts.FontFamily +import android.graphics.fonts.FontStyle import android.graphics.fonts.SystemFonts import android.os.Build import com.badlogic.gdx.graphics.Pixmap -import com.unciv.ui.utils.FontData +import com.unciv.ui.utils.FontFamilyData import com.unciv.ui.utils.NativeFontImplementation +import java.util.* +import kotlin.math.abs /** * Created by tian on 2016/10/2. */ -class NativeFontAndroid(private val size: Int, private val fontFamily: String) : - NativeFontImplementation { +class NativeFontAndroid( + private val size: Int, + private val fontFamily: String +) : NativeFontImplementation { + private val fontList = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet() + else SystemFonts.getAvailableFonts() + private val paint = Paint().apply { typeface = if (fontFamily.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val font = fontList.firstOrNull { it.file?.nameWithoutExtension == fontFamily } + // Helper within the VERSION_CODES.Q gate: Evaluate a Font's desirability (lower = better) for a given family. + fun Font.matchesFamily(family: String): Int { + val name = file?.nameWithoutExtension ?: return Int.MAX_VALUE + if (name == family) return 0 + if (!name.startsWith("$family-")) return Int.MAX_VALUE + if (style.weight == FontStyle.FONT_WEIGHT_NORMAL && style.slant == FontStyle.FONT_SLANT_UPRIGHT) return 1 + return 2 + + abs(style.weight - FontStyle.FONT_WEIGHT_NORMAL) / 100 + + abs(style.slant - FontStyle.FONT_SLANT_UPRIGHT) + } + val font = fontList.mapNotNull { + val distanceToRegular = it.matchesFamily(fontFamily) + if (distanceToRegular == Int.MAX_VALUE) null else it to distanceToRegular + }.minByOrNull { it.second }?.first if (font != null) { Typeface.CustomFallbackBuilder(FontFamily.Builder(font).build()) .setSystemFallback(fontFamily).build() @@ -32,10 +54,6 @@ class NativeFontAndroid(private val size: Int, private val fontFamily: String) : setARGB(255, 255, 255, 255) } - private val fontList: List - get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptyList() - else SystemFonts.getAvailableFonts().toList() - override fun getFontSize(): Int { return size } @@ -64,13 +82,35 @@ class NativeFontAndroid(private val size: Int, private val fontFamily: String) : return pixmap } - override fun getAvailableFont(): Collection { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - SystemFonts.getAvailableFonts().asSequence().mapNotNull { - it.file?.nameWithoutExtension - }.map { FontData(it) }.toSet() - } else { - listOf(FontData("sans-serif"), FontData("serif"), FontData("mono")) + override fun getAvailableFontFamilies(): Sequence { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + return sequenceOf(FontFamilyData("sans-serif"), FontFamilyData("serif"), FontFamilyData("mono")) + + fun String.stripFromFirstDash(): String { + val dashPos = indexOf('-') + if (dashPos < 0) return this + return this.substring(0, dashPos) } + + // To get _all_ Languages a user has in their Android settings, we would need more help + // from the launcher: (Activity).resources.configuration.locales + val languageTag = Locale.getDefault().toLanguageTag() // e.g. he-IL, corresponds to the _first_ Language in Android settings + val supportedLocales = arrayOf(languageTag, "en-US") + val supportedLanguages = supportedLocales.map { it.take(2) } + return fontList.asSequence() + .mapNotNull { + if (it.file == null) return@mapNotNull null + val fontLocale = it.localeList.getFirstMatch(supportedLocales) + val fontScriptToLanguage = fontLocale?.script?.take(2)?.lowercase() + // The font localeList contains locales that have nothing to do with the system locales + // their language and country fields are empty - so **guess** that the first two letters + // of their Script (coming in at 4 chars) corresponds to the first two of the default Locale toLanguageTag: + if (!it.localeList.isEmpty && fontScriptToLanguage !in supportedLanguages) + return@mapNotNull null + // The API talks about FontFamily, but I see no methods to ask for the family of a Font instance. + // No displayName either. So, again, infer from the file name: + it.file!!.nameWithoutExtension.stripFromFirstDash() + }.distinct() + .map { FontFamilyData(it, it) } } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 4581c0020d..d21fe80f49 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -33,7 +33,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val version = parameters.version val crashReportSysInfo = parameters.crashReportSysInfo val cancelDiscordEvent = parameters.cancelDiscordEvent - val fontImplementation = parameters.fontImplementation + var fontImplementation = parameters.fontImplementation val consoleMode = parameters.consoleMode val customSaveLocationHelper = parameters.customSaveLocationHelper val platformSpecificHelper = parameters.platformSpecificHelper diff --git a/core/src/com/unciv/ui/utils/BaseScreen.kt b/core/src/com/unciv/ui/utils/BaseScreen.kt index 0fa008d0b1..5fc3795496 100644 --- a/core/src/com/unciv/ui/utils/BaseScreen.kt +++ b/core/src/com/unciv/ui/utils/BaseScreen.kt @@ -124,7 +124,7 @@ abstract class BaseScreen : Screen { /** @return `true` if the screen is narrower than 4:3 landscape */ fun isNarrowerThan4to3() = stage.viewport.screenHeight * 4 > stage.viewport.screenWidth * 3 - fun openOptionsPopup() { - OptionsPopup(this).open(force = true) + fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage) { + OptionsPopup(this, startingPage).open(force = true) } } diff --git a/core/src/com/unciv/ui/utils/Fonts.kt b/core/src/com/unciv/ui/utils/Fonts.kt index e7ec6f8437..bcc0e6b926 100644 --- a/core/src/com/unciv/ui/utils/Fonts.kt +++ b/core/src/com/unciv/ui/utils/Fonts.kt @@ -14,27 +14,36 @@ import com.badlogic.gdx.utils.Disposable import com.unciv.Constants import com.unciv.UncivGame import com.unciv.models.stats.Stat +import com.unciv.models.translations.tr import com.unciv.ui.images.ImageGetter +import java.lang.Exception interface NativeFontImplementation { fun getFontSize(): Int fun getCharPixmap(char: Char): Pixmap - fun getAvailableFont(): Collection + fun getAvailableFontFamilies(): Sequence } -// If save in `GameSettings` need use enName. +// If save in `GameSettings` need use invariantFamily. // If show to user need use localName. // If save localName in `GameSettings` may generate garbled characters by encoding. -data class FontData(val localName: String, val enName: String = localName) { +class FontFamilyData( + val localName: String, + val invariantName: String = localName +) { + // Implement kotlin equality contract such that _only_ the invariantName field is compared. override fun equals(other: Any?): Boolean { - return if (other is FontData) enName == other.enName + return if (other is FontFamilyData) invariantName == other.invariantName else super.equals(other) } - override fun hashCode(): Int { - var result = localName.hashCode() - result = 31 * result + enName.hashCode() - return result + override fun hashCode() = invariantName.hashCode() + + /** For SelectBox usage */ + override fun toString() = localName + + companion object { + val default = FontFamilyData("Default Font".tr(), Fonts.DEFAULT_FONT_FAMILY) } } @@ -149,6 +158,10 @@ object Fonts { const val DEFAULT_FONT_FAMILY = "" lateinit var font: BitmapFont + + /** This resets all cached font data in object Fonts. + * Do not call from normal code - reset the Skin instead: `BaseScreen.setSkin()` + */ fun resetFont() { val fontData = NativeBitmapFontData(UncivGame.Current.fontImplementation!!) font = BitmapFont(fontData, fontData.regions, false) @@ -156,9 +169,24 @@ object Fonts { font.data.setScale(Constants.defaultFontSize / ORIGINAL_FONT_SIZE) } - fun getAvailableFontFamilyNames(): Collection { - if (UncivGame.Current.fontImplementation == null) return emptyList() - return UncivGame.Current.fontImplementation!!.getAvailableFont() + /** This resets all cached font data and allows changing the font */ + fun resetFont(newFamily: String) { + try { + val fontImplementationClass = UncivGame.Current.fontImplementation!!::class.java + val fontImplementationConstructor = fontImplementationClass.constructors.first() + val newFontImpl = fontImplementationConstructor.newInstance(ORIGINAL_FONT_SIZE.toInt(), newFamily) + if (newFontImpl is NativeFontImplementation) + UncivGame.Current.fontImplementation = newFontImpl + } catch (ex: Exception) {} + BaseScreen.setSkin() // calls our resetFont() - needed - the Skin seems to cache glyphs + } + + /** Reduce the font list returned by platform-specific code to font families (plain variant if possible) */ + fun getAvailableFontFamilyNames(): Sequence { + val fontImplementation = UncivGame.Current.fontImplementation + ?: return emptySequence() + return fontImplementation.getAvailableFontFamilies() + .sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.localName }) } /** @@ -206,6 +234,7 @@ object Fonts { const val happiness = '⌣' // U+2323 'smile' (😀 U+1F600 'grinning face') const val faith = '☮' // U+262E 'peace symbol' (🕊 U+1F54A 'dove of peace') + @Deprecated("Since quite a while", ReplaceWith("stat.character"), DeprecationLevel.ERROR) fun statToChar(stat: Stat): Char { return when (stat) { Stat.Food -> food @@ -217,4 +246,5 @@ object Fonts { Stat.Faith -> faith } } + } diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index 46cda2e574..2f7b4b9f35 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -48,7 +48,10 @@ import com.badlogic.gdx.utils.Array as GdxArray * @param previousScreen The caller - note if this is a [WorldScreen] or [MainMenuScreen] they will be rebuilt when major options change. */ //region Fields -class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { +class OptionsPopup( + private val previousScreen: BaseScreen, + private val selectPage: Int = defaultPage +) : Popup(previousScreen) { private val settings = previousScreen.game.settings private val tabs: TabbedPager private val resolutionArray = com.badlogic.gdx.utils.Array(arrayOf("750x500", "900x600", "1050x700", "1200x800", "1500x1000")) @@ -60,6 +63,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { //endregion companion object { + const val defaultPage = 2 // Gameplay private const val modCheckWithoutBase = "-none-" } @@ -115,10 +119,10 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { super.setVisible(visible) if (!visible) return tabs.askForPassword(secretHashCode = 2747985) - if (tabs.activePage < 0) tabs.selectPage(2) + if (tabs.activePage < 0) tabs.selectPage(selectPage) } - /** Reload this Popup after major changes (resolution, tileset, language) */ + /** Reload this Popup after major changes (resolution, tileset, language, font) */ private fun reloadWorldAndOptions() { settings.save() if (previousScreen is WorldScreen) { @@ -127,7 +131,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { } else if (previousScreen is MainMenuScreen) { previousScreen.game.setScreen(MainMenuScreen()) } - (previousScreen.game.screen as BaseScreen).openOptionsPopup() + (previousScreen.game.screen as BaseScreen).openOptionsPopup(tabs.activePage) } private fun successfullyConnectedToServer(action: (Boolean, String)->Unit){ @@ -876,34 +880,40 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { val selectCell = add() row() - fun loadFontSelect(fonts: Collection, selectCell: Cell) { - if (fonts.isEmpty()) return + fun loadFontSelect(fonts: GdxArray, selectCell: Cell) { + if (fonts.isEmpty) return - val fontSelectBox = SelectBox(skin) - val fontsLocalName = GdxArray().apply { add("Default Font".tr()) } - val fontsEnName = GdxArray().apply { add("") } - for (font in fonts) { - fontsLocalName.add(font.localName) - fontsEnName.add(font.enName) - } + val fontSelectBox = SelectBox(skin) + fontSelectBox.items = fonts - val selectedIndex = fontsEnName.indexOf(settings.fontFamily).let { if (it == -1) 0 else it } - - fontSelectBox.items = fontsLocalName - fontSelectBox.selected = fontsLocalName[selectedIndex] + // `FontFamilyData` implements kotlin equality contract such that _only_ the invariantName field is compared. + // The Gdx SelectBox should honor that - but it doesn't, as it is a _kotlin_ thing to implement + // `==` by calling `equals`, and there's precompiled _Java_ `==` in the widget code. + // `setSelected` first calls a `contains` which can switch between using `==` and `equals` (set to `equals`) + // but just one step later (where it re-checks whether the new selection is equal to the old one) + // it does a hard `==`. Also, setSelection copies its argument to the selection var, it doesn't pull a match from `items`. + // Therefore, _selecting_ an item in a `SelectBox` by an instance of `FontFamilyData` where only the `invariantName` is valid won't work properly. + // + // This is why it's _not_ `fontSelectBox.selected = FontFamilyData(settings.fontFamily)` + val fontToSelect = settings.fontFamily + fontSelectBox.selected = fonts.firstOrNull { it.invariantName == fontToSelect } // will default to first entry if `null` is passed selectCell.setActor(fontSelectBox).minWidth(selectBoxMinWidth).pad(10f) fontSelectBox.onChange { - settings.fontFamily = fontsEnName[fontSelectBox.selectedIndex] - ToastPopup( - "You need to restart the game for this change to take effect.", previousScreen - ) + settings.fontFamily = fontSelectBox.selected.invariantName + Fonts.resetFont(settings.fontFamily) + reloadWorldAndOptions() } } - crashHandlingThread(name="Add Font Select") { - val fonts = Fonts.getAvailableFontFamilyNames() // This is a heavy operation and causes ANRs + crashHandlingThread(name = "Add Font Select") { + // This is a heavy operation and causes ANRs + val fonts = GdxArray().apply { + add(FontFamilyData.default) + for (font in Fonts.getAvailableFontFamilyNames()) + add(font) + } postCrashHandlingRunnable { loadFontSelect(fonts, selectCell) } } } diff --git a/desktop/src/com/unciv/app/desktop/NativeFontDesktop.kt b/desktop/src/com/unciv/app/desktop/NativeFontDesktop.kt index 6887cd2af5..46f4f1fe2d 100755 --- a/desktop/src/com/unciv/app/desktop/NativeFontDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/NativeFontDesktop.kt @@ -1,7 +1,7 @@ package com.unciv.app.desktop import com.badlogic.gdx.graphics.Pixmap -import com.unciv.ui.utils.FontData +import com.unciv.ui.utils.FontFamilyData import com.unciv.ui.utils.NativeFontImplementation import java.awt.* import java.awt.image.BufferedImage @@ -50,10 +50,11 @@ class NativeFontDesktop(private val size: Int, private val fontFamily: String) : return pixmap } - override fun getAvailableFont(): Collection { - val allFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts.map { - FontData(it.fontName, it.getFamily(Locale.ENGLISH)) - }.toSet() - return allFonts + override fun getAvailableFontFamilies(): Sequence { + val cjkLanguage = " CJK " +System.getProperty("user.language").uppercase() + return GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts.asSequence() + .filter { " CJK " !in it.fontName || cjkLanguage in it.fontName } + .map { FontFamilyData(it.family, it.getFamily(Locale.ROOT)) } + .distinctBy { it.invariantName } } -} \ No newline at end of file +}