Tweak Language Pickers to scroll the selected one into view when appropriate, and allow selection with letter keys (#10569)

This commit is contained in:
SomeTroglodyte
2023-11-25 19:11:41 +01:00
committed by GitHub
parent b61c9de39e
commit e15b6cab76
6 changed files with 125 additions and 45 deletions

View File

@ -19,6 +19,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton import com.badlogic.gdx.scenes.scene2d.ui.ImageButton
import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
@ -130,6 +131,11 @@ fun Actor.getAscendant(predicate: (Actor) -> Boolean): Actor? {
return null return null
} }
/** Gets the nearest parent of this actor that is a [T], or null if none of its parents is of that type. */
inline fun <reified T> Actor.getAscendant(): T? {
return getAscendant { it is T } as? T
}
/** The actors bounding box in stage coordinates */ /** The actors bounding box in stage coordinates */
val Actor.stageBoundingBox: Rectangle get() { val Actor.stageBoundingBox: Rectangle get() {
val bottomLeft = localToStageCoordinates(Vector2.Zero.cpy()) val bottomLeft = localToStageCoordinates(Vector2.Zero.cpy())
@ -225,6 +231,10 @@ fun Image.setSize(size: Float) {
setSize(size, size) setSize(size, size)
} }
/** Proxy for [ScrollPane.scrollTo] using the [bounds][Actor.setBounds] of a given [actor] for its parameters */
fun ScrollPane.scrollTo(actor: Actor, center: Boolean = false) =
scrollTo(actor.x, actor.y, actor.width, actor.height, center, center)
/** Translate a [String] and make a [TextButton] widget from it */ /** Translate a [String] and make a [TextButton] widget from it */
fun String.toTextButton(style: TextButtonStyle? = null, hideIcons: Boolean = false): TextButton { fun String.toTextButton(style: TextButtonStyle? = null, hideIcons: Boolean = false): TextButton {
val text = this.tr(hideIcons) val text = this.tr(hideIcons)

View File

@ -1,18 +1,29 @@
package com.unciv.ui.components.widgets package com.unciv.ui.components.widgets
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyShortcutDispatcher
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.options.OptionsPopup
import com.unciv.ui.screens.LanguagePickerScreen
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.civilopediascreen.MarkupRenderer import com.unciv.ui.screens.civilopediascreen.MarkupRenderer
import java.util.Locale import java.util.Locale
/** Represents a row in the Language picker, used both in OptionsPopup and in LanguagePickerScreen */
/** Represents a row in the Language picker, used both in [OptionsPopup] and in [LanguagePickerScreen]
* @see addLanguageTables
*/
internal class LanguageTable(val language:String, val percentComplete: Int) : Table() { internal class LanguageTable(val language:String, val percentComplete: Int) : Table() {
private val baseColor = BaseScreen.skinStrings.skinConfig.baseColor private val baseColor = BaseScreen.skinStrings.skinConfig.baseColor
private val darkBaseColor = baseColor.darken(0.5f) private val darkBaseColor = baseColor.darken(0.5f)
@ -41,7 +52,7 @@ internal class LanguageTable(val language:String, val percentComplete: Int) : Ta
companion object { companion object {
/** Extension to add the Language boxes to a Table, used both in OptionsPopup and in LanguagePickerScreen */ /** Extension to add the Language boxes to a Table, used both in OptionsPopup and in LanguagePickerScreen */
internal fun Table.addLanguageTables(expectedWidth: Float): ArrayList<LanguageTable> { fun Table.addLanguageTables(expectedWidth: Float): ArrayList<LanguageTable> {
val languageTables = ArrayList<LanguageTable>() val languageTables = ArrayList<LanguageTable>()
val translationDisclaimer = FormattedLine( val translationDisclaimer = FormattedLine(
@ -54,27 +65,21 @@ internal class LanguageTable(val language:String, val percentComplete: Int) : Ta
add(MarkupRenderer.render(listOf(translationDisclaimer),expectedWidth)).pad(5f).row() add(MarkupRenderer.render(listOf(translationDisclaimer),expectedWidth)).pad(5f).row()
val tableLanguages = Table() val tableLanguages = Table()
tableLanguages.defaults().uniformX() tableLanguages.defaults().uniformX().fillX().pad(10.0f)
tableLanguages.defaults().pad(10.0f)
tableLanguages.defaults().fillX()
val systemLanguage = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH) val systemLanguage = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH)
val languageCompletionPercentage = UncivGame.Current.translations val languageCompletionPercentage = UncivGame.Current.translations
.percentCompleteOfLanguages .percentCompleteOfLanguages
languageTables.addAll(languageCompletionPercentage languageTables.addAll(
languageCompletionPercentage
.map { LanguageTable(it.key, if (it.key == Constants.english) 100 else it.value) } .map { LanguageTable(it.key, if (it.key == Constants.english) 100 else it.value) }
.sortedWith { p0, p1 -> .sortedWith(
when { compareBy<LanguageTable> { it.language != Constants.english }
p0.language == Constants.english -> -1 .thenBy { it.language != systemLanguage }
p1.language == Constants.english -> 1 .thenByDescending { it.percentComplete }
p0.language == systemLanguage -> -1 )
p1.language == systemLanguage -> 1 )
p0.percentComplete > p1.percentComplete -> -1
p0.percentComplete == p1.percentComplete -> 0
else -> 1
}
})
languageTables.forEach { languageTables.forEach {
tableLanguages.add(it).row() tableLanguages.add(it).row()
@ -83,5 +88,28 @@ internal class LanguageTable(val language:String, val percentComplete: Int) : Ta
return languageTables return languageTables
} }
/** Create round-robin letter key handling, such that repeatedly pressing 'R' will cycle through all languages starting with 'R' */
fun Actor.addLanguageKeyShortcuts(languageTables: ArrayList<LanguageTable>, getSelection: ()->String, action: (String)->Unit) {
// Yes this is too complicated. Trying to preserve existing architecture choices.
// One - extending KeyShortcut to allow another type filtering by a lambda,
// then teach KeyShortcutDispatcher.Resolver to recognize that - and pass on the actual key to its activation - could help.
// Two - Changing addLanguageTables above to an actual container class holding the LanguageTables - could help.
fun activation(letter: Char) {
val candidates = languageTables.filter { it.language.first() == letter }
if (candidates.isEmpty()) return
if (candidates.size == 1) return action(candidates.first().language)
val currentIndex = candidates.indexOfFirst { it.language == getSelection() }
val newSelection = candidates[(currentIndex + 1) % candidates.size]
action(newSelection.language)
}
val letters = languageTables.map { it.language.first() }.toSet()
for (letter in letters) {
keyShortcuts.add(KeyShortcutDispatcher.KeyShortcut(KeyboardBinding.None, KeyCharAndCode(letter), 0)) {
activation(letter)
}
}
}
} }
} }

View File

@ -13,6 +13,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener
import com.badlogic.gdx.scenes.scene2d.utils.Layout
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
@ -23,6 +24,7 @@ import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.packIfNeeded import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.components.extensions.pad import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.scrollTo
import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onActivation
@ -459,6 +461,18 @@ open class TabbedPager(
contentScroll.scrollY = scrollY contentScroll.scrollY = scrollY
if (!animation) contentScroll.updateVisualScroll() if (!animation) contentScroll.updateVisualScroll()
} }
/** Change the vertical scroll position af the [active][activePage] page's contents to scroll a given Actor into view
*
* Assumes [actor] is a direct child of the content page, and **if** the page does **not** implement [Layout],
* that [actor] has somehow been given valid [bounds][Actor.setBounds] within the page.
*/
fun pageScrollTo(actor: Actor, animation: Boolean = false) {
if (activePage < 0) return
(contentScroll.actor as? Layout)?.validate()
contentScroll.scrollTo(actor)
pages[activePage].scrollY = contentScroll.scrollY
if (!animation) contentScroll.updateVisualScroll()
}
/** Disable/Enable built-in ScrollPane for content pages, including focus stealing prevention */ /** Disable/Enable built-in ScrollPane for content pages, including focus stealing prevention */
fun setScrollDisabled(disabled: Boolean) { fun setScrollDisabled(disabled: Boolean) {

View File

@ -2,37 +2,52 @@ package com.unciv.ui.popups.options
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.extensions.getAscendant
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageKeyShortcuts
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.components.widgets.TabbedPager
fun languageTab( class LanguageTab(
optionsPopup: OptionsPopup, optionsPopup: OptionsPopup,
onLanguageSelected: () -> Unit private val onLanguageSelected: () -> Unit
): Table = Table(BaseScreen.skin).apply { ): Table(), TabbedPager.IPageExtensions {
val settings = optionsPopup.settings private val languageTables = this.addLanguageTables(optionsPopup.tabs.prefWidth * 0.9f - 10f)
private val settings = optionsPopup.settings
private var chosenLanguage = settings.language
val languageTables = this.addLanguageTables(optionsPopup.tabs.prefWidth * 0.9f - 10f) private fun selectLanguage() {
var chosenLanguage = settings.language
fun selectLanguage() {
settings.language = chosenLanguage settings.language = chosenLanguage
settings.updateLocaleFromLanguage() settings.updateLocaleFromLanguage()
UncivGame.Current.translations.tryReadTranslationForCurrentLanguage() UncivGame.Current.translations.tryReadTranslationForCurrentLanguage()
onLanguageSelected() onLanguageSelected()
} }
fun updateSelection() { private fun updateSelection() {
languageTables.forEach { it.update(chosenLanguage) } languageTables.forEach { it.update(chosenLanguage) }
if (chosenLanguage != settings.language) if (chosenLanguage != settings.language)
selectLanguage() selectLanguage()
} }
updateSelection()
languageTables.forEach { init {
it.onClick { for (langTable in languageTables) {
chosenLanguage = it.language langTable.onClick {
updateSelection() chosenLanguage = langTable.language
updateSelection()
}
}
addLanguageKeyShortcuts(languageTables, getSelection = { chosenLanguage }) {
chosenLanguage = it
val pager = this.getAscendant<TabbedPager>()
?: return@addLanguageKeyShortcuts
activated(pager.activePage, "", pager)
} }
} }
override fun activated(index: Int, caption: String, pager: TabbedPager) {
updateSelection()
val selectedTable = languageTables.firstOrNull { it.language == chosenLanguage }
?: return
pager.pageScrollTo(selectedTable, true)
}
} }

View File

@ -90,7 +90,7 @@ class OptionsPopup(
) )
tabs.addPage( tabs.addPage(
"Language", "Language",
languageTab(this, ::reloadWorldAndOptions), LanguageTab(this, ::reloadWorldAndOptions),
ImageGetter.getImage("FlagIcons/${settings.language}"), 24f ImageGetter.getImage("FlagIcons/${settings.language}"), 24f
) )
tabs.addPage( tabs.addPage(

View File

@ -2,20 +2,22 @@ package com.unciv.ui.screens
import com.unciv.Constants import com.unciv.Constants
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.popups.options.OptionsPopup
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.components.widgets.LanguageTable
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.components.extensions.enable import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.scrollTo
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.LanguageTable
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageKeyShortcuts
import com.unciv.ui.components.widgets.LanguageTable.Companion.addLanguageTables
import com.unciv.ui.popups.options.OptionsPopup
import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen
import com.unciv.ui.screens.pickerscreens.PickerScreen
/** A [PickerScreen] to select a language, used once on the initial run after a fresh install. /** A [PickerScreen] to select a language, used once on the initial run after a fresh install.
* After that, [OptionsPopup] provides the functionality. * After that, [OptionsPopup] provides the functionality.
* Reusable code is in [LanguageTable] and [addLanguageTables]. * Reusable code is in [LanguageTable] and [addLanguageTables].
*/ */
class LanguagePickerScreen : PickerScreen() { class LanguagePickerScreen : PickerScreen() {
var chosenLanguage = Constants.english private var chosenLanguage = Constants.english
private val languageTables: ArrayList<LanguageTable> private val languageTables: ArrayList<LanguageTable>
@ -28,21 +30,32 @@ class LanguagePickerScreen : PickerScreen() {
languageTables = topTable.addLanguageTables(stage.width - 60f) languageTables = topTable.addLanguageTables(stage.width - 60f)
languageTables.forEach { for (languageTable in languageTables) {
it.onClick { languageTable.onClick {
chosenLanguage = it.language onChoice(languageTable.language)
rightSideButton.enable()
update()
} }
} }
topTable.addLanguageKeyShortcuts(languageTables, { chosenLanguage }) { language ->
onChoice(language)
val selectedTable = languageTables.firstOrNull { it.language == language }
?: return@addLanguageKeyShortcuts
scrollPane.scrollTo(selectedTable, true)
}
rightSideButton.setText("Pick language".tr()) rightSideButton.setText("Pick language".tr())
rightSideButton.onClick { rightSideButton.onClick {
pickLanguage() pickLanguage()
} }
} }
fun pickLanguage() { private fun onChoice(choice: String) {
chosenLanguage = choice
rightSideButton.enable()
update()
}
private fun pickLanguage() {
game.settings.language = chosenLanguage game.settings.language = chosenLanguage
game.settings.updateLocaleFromLanguage() game.settings.updateLocaleFromLanguage()
game.settings.isFreshlyCreated = false // mark so the picker isn't called next launch game.settings.isFreshlyCreated = false // mark so the picker isn't called next launch