Font choice rework (#6670)

* Font choice rework

* Font choice rework - naming

* Font choice rework - fix default font selection
This commit is contained in:
SomeTroglodyte 2022-05-08 20:22:23 +02:00 committed by GitHub
parent 4b7edca7a8
commit 3ea9c6503b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 60 deletions

View File

@ -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<Font>
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<FontData> {
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<FontFamilyData> {
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) }
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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<FontData>
fun getAvailableFontFamilies(): Sequence<FontFamilyData>
}
// 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<FontData> {
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<FontFamilyData> {
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
}
}
}

View File

@ -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<FontData>, selectCell: Cell<Actor>) {
if (fonts.isEmpty()) return
fun loadFontSelect(fonts: GdxArray<FontFamilyData>, selectCell: Cell<Actor>) {
if (fonts.isEmpty) return
val fontSelectBox = SelectBox<String>(skin)
val fontsLocalName = GdxArray<String>().apply { add("Default Font".tr()) }
val fontsEnName = GdxArray<String>().apply { add("") }
for (font in fonts) {
fontsLocalName.add(font.localName)
fontsEnName.add(font.enName)
}
val fontSelectBox = SelectBox<FontFamilyData>(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<FontFamilyData>().apply {
add(FontFamilyData.default)
for (font in Fonts.getAvailableFontFamilyNames())
add(font)
}
postCrashHandlingRunnable { loadFontSelect(fonts, selectCell) }
}
}

View File

@ -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<FontData> {
val allFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts.map {
FontData(it.fontName, it.getFamily(Locale.ENGLISH))
}.toSet()
return allFonts
override fun getAvailableFontFamilies(): Sequence<FontFamilyData> {
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 }
}
}
}