mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-24 18:06:04 +07:00
Font choice rework (#6670)
* Font choice rework * Font choice rework - naming * Font choice rework - fix default font selection
This commit is contained in:
parent
4b7edca7a8
commit
3ea9c6503b
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user