mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-24 06:39:16 +07:00
Tabbed options (#5081)
* Tabbed Options Screen * Tabbed Options Screen - atlas
This commit is contained in:
@ -43,7 +43,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
*/
|
||||
var viewEntireMapForDebug = false
|
||||
/** For when you need to test something in an advanced game and don't have time to faff around */
|
||||
val superchargedForDebug = false
|
||||
var superchargedForDebug = false
|
||||
|
||||
/** Simulate until this turn on the first "Next turn" button press.
|
||||
* Does not update World View changes until finished.
|
||||
|
@ -1,69 +1,31 @@
|
||||
package com.unciv.ui
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.utils.*
|
||||
import com.unciv.ui.utils.enable
|
||||
import com.unciv.ui.utils.onClick
|
||||
import com.unciv.ui.utils.LanguageTable
|
||||
import com.unciv.ui.utils.LanguageTable.Companion.addLanguageTables
|
||||
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
|
||||
|
||||
|
||||
class LanguageTable(val language:String, val percentComplete: Int):Table(){
|
||||
private val blue = ImageGetter.getBlue()
|
||||
private val darkBlue = blue.cpy().lerp(Color.BLACK,0.5f)!!
|
||||
|
||||
init{
|
||||
pad(10f)
|
||||
defaults().pad(10f)
|
||||
left()
|
||||
if(ImageGetter.imageExists("FlagIcons/$language"))
|
||||
add(ImageGetter.getImage("FlagIcons/$language")).size(40f)
|
||||
|
||||
val spaceSplitLang = language.replace("_"," ")
|
||||
add("$spaceSplitLang ($percentComplete%)".toLabel())
|
||||
update("")
|
||||
touchable = Touchable.enabled // so click listener is activated when any part is clicked, not only children
|
||||
pack()
|
||||
}
|
||||
|
||||
fun update(chosenLanguage:String){
|
||||
background = ImageGetter.getBackground( if(chosenLanguage==language) blue else darkBlue)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LanguagePickerScreen : PickerScreen(){
|
||||
/** A [PickerScreen] to select a language, used once on the initial run after a fresh install.
|
||||
* After that, [OptionsPopup] provides the functionality.
|
||||
* Reusable code is in [LanguageTable] and [addLanguageTables].
|
||||
*/
|
||||
class LanguagePickerScreen : PickerScreen() {
|
||||
var chosenLanguage = "English"
|
||||
|
||||
private val languageTables = ArrayList<LanguageTable>()
|
||||
private val languageTables: ArrayList<LanguageTable>
|
||||
|
||||
fun update(){
|
||||
fun update() {
|
||||
languageTables.forEach { it.update(chosenLanguage) }
|
||||
}
|
||||
|
||||
init {
|
||||
closeButton.isVisible = false
|
||||
/// trimMargin is overhead, but easier to maintain and see when it might get trimmed without wrap:
|
||||
val translationDisclaimer = """
|
||||
|Please note that translations are a community-based work in progress and are INCOMPLETE!
|
||||
|The percentage shown is how much of the language is translated in-game.
|
||||
|If you want to help translating the game into your language,
|
||||
| instructions are in the Github readme! (Menu > Community > Github)
|
||||
""".trimMargin()
|
||||
topTable.add(translationDisclaimer.toLabel()).pad(10f).row()
|
||||
val tableLanguages = Table()
|
||||
tableLanguages.defaults().uniformX()
|
||||
tableLanguages.defaults().pad(10.0f)
|
||||
tableLanguages.defaults().fillX()
|
||||
topTable.add(tableLanguages).row()
|
||||
|
||||
val languageCompletionPercentage = UncivGame.Current.translations
|
||||
.percentCompleteOfLanguages
|
||||
languageTables.addAll(languageCompletionPercentage
|
||||
.map { LanguageTable(it.key,if(it.key=="English") 100 else it.value) }
|
||||
.sortedByDescending { it.percentComplete} )
|
||||
languageTables = topTable.addLanguageTables(stage.width - 60f)
|
||||
|
||||
languageTables.forEach {
|
||||
it.onClick {
|
||||
@ -71,7 +33,6 @@ class LanguagePickerScreen : PickerScreen(){
|
||||
rightSideButton.enable()
|
||||
update()
|
||||
}
|
||||
tableLanguages.add(it).row()
|
||||
}
|
||||
|
||||
rightSideButton.setText("Pick language".tr())
|
||||
@ -89,4 +50,4 @@ class LanguagePickerScreen : PickerScreen(){
|
||||
game.setScreen(MainMenuScreen())
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
68
core/src/com/unciv/ui/utils/LanguageTable.kt
Normal file
68
core/src/com/unciv/ui/utils/LanguageTable.kt
Normal file
@ -0,0 +1,68 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.ui.civilopedia.FormattedLine
|
||||
import com.unciv.ui.civilopedia.MarkupRenderer
|
||||
import java.util.ArrayList
|
||||
|
||||
/** Represents a row in the Language picker, used both in OptionsPopup and in LanguagePickerScreen */
|
||||
internal class LanguageTable(val language:String, val percentComplete: Int): Table(){
|
||||
private val blue = ImageGetter.getBlue()
|
||||
private val darkBlue = blue.cpy().lerp(Color.BLACK,0.5f)!!
|
||||
|
||||
init{
|
||||
pad(10f)
|
||||
defaults().pad(10f)
|
||||
left()
|
||||
if(ImageGetter.imageExists("FlagIcons/$language"))
|
||||
add(ImageGetter.getImage("FlagIcons/$language")).size(40f)
|
||||
|
||||
val spaceSplitLang = language.replace("_"," ")
|
||||
add("$spaceSplitLang ($percentComplete%)".toLabel())
|
||||
update("")
|
||||
touchable =
|
||||
Touchable.enabled // so click listener is activated when any part is clicked, not only children
|
||||
pack()
|
||||
}
|
||||
|
||||
fun update(chosenLanguage:String){
|
||||
background = ImageGetter.getBackground(if (chosenLanguage == language) blue else darkBlue)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Extension to add the Language boxes to a Table, used both in OptionsPopup and in LanguagePickerScreen */
|
||||
internal fun Table.addLanguageTables(expectedWidth: Float): ArrayList<LanguageTable> {
|
||||
val languageTables = ArrayList<LanguageTable>()
|
||||
|
||||
val translationDisclaimer = FormattedLine(
|
||||
text = "Please note that translations are a community-based work in progress and are" +
|
||||
" INCOMPLETE! The percentage shown is how much of the language is translated in-game." +
|
||||
" If you want to help translating the game into your language, click here.",
|
||||
link = "https://github.com/yairm210/Unciv/wiki/Translating",
|
||||
size = 15
|
||||
)
|
||||
add(MarkupRenderer.render(listOf(translationDisclaimer),expectedWidth)).pad(5f).row()
|
||||
|
||||
val tableLanguages = Table()
|
||||
tableLanguages.defaults().uniformX()
|
||||
tableLanguages.defaults().pad(10.0f)
|
||||
tableLanguages.defaults().fillX()
|
||||
|
||||
val languageCompletionPercentage = UncivGame.Current.translations
|
||||
.percentCompleteOfLanguages
|
||||
languageTables.addAll(languageCompletionPercentage
|
||||
.map { LanguageTable(it.key, if (it.key == "English") 100 else it.value) }
|
||||
.sortedByDescending { it.percentComplete} )
|
||||
|
||||
languageTables.forEach {
|
||||
tableLanguages.add(it).row()
|
||||
}
|
||||
add(tableLanguages).row()
|
||||
|
||||
return languageTables
|
||||
}
|
||||
}
|
||||
}
|
355
core/src/com/unciv/ui/utils/TabbedPager.kt
Normal file
355
core/src/com/unciv/ui/utils/TabbedPager.kt
Normal file
@ -0,0 +1,355 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.unciv.UncivGame
|
||||
import kotlin.math.min
|
||||
|
||||
/*
|
||||
Unimplemented ideas:
|
||||
Allow "fixed header" content that does not participate in scrolling
|
||||
(OptionsPopup mod check tab)
|
||||
`scrollAlign: Align` property controls initial content scroll position (currently it's Align.top)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements a 'Tabs' widget where different pages can be switched by selecting a header button.
|
||||
*
|
||||
* Each page is an Actor, passed to the Widget via [addPage]. Pages can be [removed][removePage],
|
||||
* [replaced][replacePage] or dynamically added after the Widget is already shown.
|
||||
|
||||
* Pages are automatically scrollable, switching pages preserves scroll positions individually.
|
||||
* Pages can be disabled or secret - any 'secret' pages added require a later call to [askForPassword]
|
||||
* to activate them (or discard if the password is wrong).
|
||||
*
|
||||
* The size parameters are lower and upper bounds of the page content area. The widget will always report
|
||||
* these bounds (plus header height) as layout properties min/max-Width/Height, and measure the content
|
||||
* area of added pages and set the reported pref-W/H to their maximum within these bounds. But, if a
|
||||
* maximum is not specified, that coordinate will grow with content unlimited, and layout max-W/H will
|
||||
* always report the same as pref-W/H.
|
||||
*/
|
||||
//region Fields and initialization
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused") // All member are part of our API
|
||||
class TabbedPager(
|
||||
private val minimumWidth: Float = 0f,
|
||||
private var maximumWidth: Float = Float.MAX_VALUE,
|
||||
private val minimumHeight: Float = 0f,
|
||||
private var maximumHeight: Float = Float.MAX_VALUE,
|
||||
private val headerFontSize: Int = 18,
|
||||
private val headerFontColor: Color = Color.WHITE,
|
||||
private val highlightColor: Color = Color.BLUE,
|
||||
backgroundColor: Color = ImageGetter.getBlue().lerp(Color.BLACK, 0.5f),
|
||||
private val headerPadding: Float = 10f,
|
||||
capacity: Int = 4
|
||||
) : Table() {
|
||||
|
||||
private class PageState(
|
||||
var content: Actor,
|
||||
var disabled: Boolean = false,
|
||||
val onActivation: ((Int, String)->Unit)? = null
|
||||
) {
|
||||
var scrollX = 0f
|
||||
var scrollY = 0f
|
||||
|
||||
var button: Button = Button(CameraStageBaseScreen.skin)
|
||||
var buttonX = 0f
|
||||
var buttonW = 0f
|
||||
}
|
||||
|
||||
private var preferredWidth = minimumWidth
|
||||
private val growMaxWidth = maximumWidth == Float.MAX_VALUE
|
||||
private val limitWidth = maximumWidth
|
||||
private var preferredHeight = minimumHeight
|
||||
private val growMaxHeight = maximumHeight == Float.MAX_VALUE
|
||||
private val limitHeight = maximumHeight
|
||||
|
||||
private val pages = ArrayList<PageState>(capacity)
|
||||
|
||||
/**
|
||||
* Index of currently selected page, or -1 of none. Read-only, use [selectPage] to change.
|
||||
*/
|
||||
var activePage = -1
|
||||
private set
|
||||
|
||||
private val header = Table(CameraStageBaseScreen.skin)
|
||||
private val headerScroll = AutoScrollPane(header)
|
||||
private var headerHeight = 0f
|
||||
|
||||
private val contentScroll = AutoScrollPane(null)
|
||||
|
||||
private val deferredSecretPages = ArrayDeque<PageState>(0)
|
||||
private var askPasswordLock = false
|
||||
|
||||
init {
|
||||
background = ImageGetter.getBackground(backgroundColor)
|
||||
header.defaults().pad(headerPadding, headerPadding * 0.5f)
|
||||
headerScroll.setOverscroll(false,false)
|
||||
headerScroll.setScrollingDisabled(false, true)
|
||||
// Measure header height, most likely its final value
|
||||
removePage(addPage("Dummy"))
|
||||
add(headerScroll).growX().minHeight(headerHeight).row()
|
||||
add(contentScroll).grow().row()
|
||||
}
|
||||
|
||||
//endregion
|
||||
//region Widget interface
|
||||
|
||||
// The following are part of the Widget interface and serve dynamic sizing
|
||||
override fun getPrefWidth() = preferredWidth
|
||||
fun setPrefWidth(width: Float) {
|
||||
if (width !in minimumWidth..maximumWidth) throw IllegalArgumentException()
|
||||
preferredWidth = width
|
||||
invalidateHierarchy()
|
||||
}
|
||||
override fun getPrefHeight() = preferredHeight + headerHeight
|
||||
fun setPrefHeight(height: Float) {
|
||||
if (height - headerHeight !in minimumHeight..maximumHeight) throw IllegalArgumentException()
|
||||
preferredHeight = height - headerHeight
|
||||
invalidateHierarchy()
|
||||
}
|
||||
override fun getMinWidth() = minimumWidth
|
||||
override fun getMaxWidth() = maximumWidth
|
||||
override fun getMinHeight() = headerHeight + minimumHeight
|
||||
override fun getMaxHeight() = headerHeight + maximumHeight
|
||||
|
||||
//endregion
|
||||
//region API
|
||||
|
||||
/** @return Number of pages currently stored */
|
||||
fun pageCount() = pages.size
|
||||
|
||||
/** @return index of a page by its (untranslated) caption, or -1 if no such page exists */
|
||||
fun getPageIndex(caption: String) = pages.indexOfLast { it.button.name == caption }
|
||||
|
||||
/** Change the selected page by using its index.
|
||||
* @param index Page number or -1 to deselect the current page.
|
||||
* @return `true` if the page was successfully changed.
|
||||
*/
|
||||
fun selectPage(index: Int): Boolean {
|
||||
if (index !in -1 until pages.size) return false
|
||||
if (activePage == index) return false
|
||||
if (index >= 0 && pages[index].disabled) return false
|
||||
if (activePage != -1) {
|
||||
pages[activePage].apply {
|
||||
button.color = Color.WHITE
|
||||
scrollX = contentScroll.scrollX
|
||||
scrollY = contentScroll.scrollY
|
||||
contentScroll.removeActor(content)
|
||||
}
|
||||
}
|
||||
activePage = index
|
||||
if (index != -1) {
|
||||
pages[index].apply {
|
||||
button.color = highlightColor
|
||||
contentScroll.actor = content
|
||||
contentScroll.layout()
|
||||
if (scrollX < 0f) // was marked to center on first show
|
||||
scrollX = ((content.width - this@TabbedPager.width) / 2).coerceIn(0f, contentScroll.maxX)
|
||||
contentScroll.scrollX = scrollX
|
||||
contentScroll.scrollY = scrollY
|
||||
contentScroll.updateVisualScroll()
|
||||
headerScroll.let {
|
||||
it.scrollX = (buttonX + (buttonW - it.width) / 2).coerceIn(0f, it.maxX)
|
||||
}
|
||||
onActivation?.invoke(index, button.name)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** Change the selected page by using its caption.
|
||||
* @param caption Caption of the page to select. A nonexistent name will deselect the current page.
|
||||
* @return `true` if the page was successfully changed.
|
||||
*/
|
||||
fun selectPage(caption: String) = selectPage(getPageIndex(caption))
|
||||
private fun selectPage(page: PageState) = selectPage(getPageIndex(page))
|
||||
|
||||
/** Change the disabled property of a page by its index.
|
||||
* @return previous value or `false` if index invalid.
|
||||
*/
|
||||
fun setPageDisabled(index: Int, disabled: Boolean): Boolean {
|
||||
if (index !in 0 until pages.size) return false
|
||||
val page = pages[index]
|
||||
val oldValue = page.disabled
|
||||
page.disabled = disabled
|
||||
page.button.isEnabled = !disabled
|
||||
if (disabled && index == activePage) selectPage(-1)
|
||||
return oldValue
|
||||
}
|
||||
|
||||
/** Change the disabled property of a page by its caption.
|
||||
* @return previous value or `false` if caption not found.
|
||||
*/
|
||||
fun setPageDisabled(caption: String, disabled: Boolean) = setPageDisabled(getPageIndex(caption), disabled)
|
||||
|
||||
/** Remove a page by its index.
|
||||
* @return `true` if page successfully removed */
|
||||
fun removePage(index: Int): Boolean {
|
||||
if (index !in 0 until pages.size) return false
|
||||
if (index == activePage) selectPage(-1)
|
||||
val page = pages.removeAt(index)
|
||||
header.getCell(page.button).clearActor()
|
||||
header.cells.removeIndex(index)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Remove a page by its caption.
|
||||
* @return `true` if page successfully removed */
|
||||
fun removePage(caption: String) = removePage(getPageIndex(caption))
|
||||
|
||||
/** Replace a page's content by its index. */
|
||||
fun replacePage(index: Int, content: Actor) {
|
||||
if (index !in 0 until pages.size) return
|
||||
val isActive = index == activePage
|
||||
if (isActive) selectPage(-1)
|
||||
pages[index].content = content
|
||||
if (isActive) selectPage(index)
|
||||
}
|
||||
|
||||
/** Replace a page's content by its caption. */
|
||||
fun replacePage(caption: String, content: Actor) = replacePage(getPageIndex(caption), content)
|
||||
|
||||
/** Add a page!
|
||||
* @param caption Text to be shown on the header button (automatically translated), can later be used to reference the page in other calls.
|
||||
* @param content Actor to show when this page is selected.
|
||||
* @param icon Actor, typically an [Image], to show before the caption.
|
||||
* @param iconSize Size for [icon] - if not zero, the icon is wrapped to allow a [setSize] even on [Image] which ignores size.
|
||||
* @param insertBefore -1 to add at the end or index of existing page to insert this before
|
||||
* @param secret Marks page as 'secret'. A password is asked once per [TabbedPager] and if it does not match the has passed in the constructor the page and all subsequent secret pages are dropped.
|
||||
* @param disabled Initial disabled state. Disabled pages cannot be selected even with [selectPage], their button is dimmed.
|
||||
* @param onActivation _Optional_ callback called when this page is shown (per actual change to this page, not per header click). Lambda arguments are page index and caption.
|
||||
* @return The new page's index or -1 if it could not be immediately added (secret).
|
||||
*/
|
||||
fun addPage(
|
||||
caption: String,
|
||||
content: Actor? = null,
|
||||
icon: Actor? = null,
|
||||
iconSize: Float = 0f,
|
||||
insertBefore: Int = -1,
|
||||
secret: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
onActivation: ((Int, String)->Unit)? = null
|
||||
): Int {
|
||||
// Build page descriptor and header button
|
||||
val page = PageState(content ?: Group(), disabled, onActivation)
|
||||
page.button.apply {
|
||||
name = caption // enable finding pages by untranslated caption without needing our own field
|
||||
if (icon != null) {
|
||||
if (iconSize != 0f) {
|
||||
val wrapper = Group().apply {
|
||||
isTransform =
|
||||
false // performance helper - nothing here is rotated or scaled
|
||||
setSize(iconSize, iconSize)
|
||||
icon.setSize(iconSize, iconSize)
|
||||
icon.center(this)
|
||||
addActor(icon)
|
||||
}
|
||||
add(wrapper).padRight(headerPadding * 0.5f)
|
||||
} else {
|
||||
add(icon)
|
||||
}
|
||||
}
|
||||
add(caption.toLabel(headerFontColor, headerFontSize))
|
||||
isEnabled = !disabled
|
||||
onClick {
|
||||
selectPage(page)
|
||||
}
|
||||
pack()
|
||||
if (height + 2 * headerPadding > headerHeight) {
|
||||
headerHeight = height + 2 * headerPadding
|
||||
if (activePage >= 0) this@TabbedPager.invalidateHierarchy()
|
||||
}
|
||||
}
|
||||
|
||||
// Support 'secret' pages
|
||||
if (secret) {
|
||||
deferredSecretPages.addLast(page)
|
||||
return -1
|
||||
}
|
||||
|
||||
return addAndShowPage(page, insertBefore)
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate any [secret][addPage] pages by asking for the password.
|
||||
*
|
||||
* If the parent of this Widget is a Popup, then this needs to be called _after_ the parent
|
||||
* is shown to ensure proper popup stacking.
|
||||
*/
|
||||
fun askForPassword(secretHashCode: Int = 0) {
|
||||
class PassPopup(screen: CameraStageBaseScreen, unlockAction: ()->Unit, lockAction: ()->Unit) : Popup(screen) {
|
||||
val passEntry = TextField("", CameraStageBaseScreen.skin)
|
||||
init {
|
||||
passEntry.isPasswordMode = true
|
||||
add(passEntry).row()
|
||||
addOKButton {
|
||||
if (passEntry.text.hashCode() == secretHashCode) unlockAction() else lockAction()
|
||||
}
|
||||
this.keyboardFocus = passEntry
|
||||
}
|
||||
}
|
||||
|
||||
if (!UncivGame.isCurrentInitialized() || askPasswordLock || deferredSecretPages.isEmpty()) return
|
||||
askPasswordLock = true // race condition: Popup closes _first_, then deferredSecretPages is emptied -> parent shows and calls us again
|
||||
|
||||
PassPopup(UncivGame.Current.screen as CameraStageBaseScreen, {
|
||||
addDeferredSecrets()
|
||||
}, {
|
||||
deferredSecretPages.clear()
|
||||
}).open(true)
|
||||
}
|
||||
|
||||
//endregion
|
||||
//region Helper routines
|
||||
|
||||
private fun getPageIndex(page: PageState) = pages.indexOf(page)
|
||||
|
||||
private fun addAndShowPage(page: PageState, insertBefore: Int): Int {
|
||||
// Update pages array and header table
|
||||
val newIndex: Int
|
||||
val buttonCell: Cell<Button>
|
||||
if (insertBefore >= 0 && insertBefore < pages.size) {
|
||||
newIndex = insertBefore
|
||||
pages.add(insertBefore, page)
|
||||
header.addActorAt(insertBefore, page.button)
|
||||
buttonCell = header.getCell(page.button)
|
||||
} else {
|
||||
newIndex = pages.size
|
||||
pages.add(page)
|
||||
buttonCell = header.add(page.button)
|
||||
}
|
||||
page.buttonX = if (newIndex == 0) 0f else pages[newIndex-1].run { buttonX + buttonW }
|
||||
page.buttonW = buttonCell.run { prefWidth + padLeft + padRight }
|
||||
for (i in newIndex + 1 until pages.size)
|
||||
pages[i].buttonX += page.buttonW
|
||||
|
||||
// Content Sizing
|
||||
if (page.content is WidgetGroup) {
|
||||
(page.content as WidgetGroup).packIfNeeded()
|
||||
val contentWidth = min(page.content.width, limitWidth)
|
||||
if (contentWidth > preferredWidth) {
|
||||
preferredWidth = contentWidth
|
||||
if (activePage >= 0) invalidateHierarchy()
|
||||
}
|
||||
val contentHeight = min(page.content.height, limitHeight)
|
||||
if (contentHeight > preferredHeight) {
|
||||
preferredHeight = contentHeight
|
||||
if (activePage >= 0) invalidateHierarchy()
|
||||
}
|
||||
page.scrollX = -1f // mark to center later when all pages are measured
|
||||
}
|
||||
if (growMaxWidth) maximumWidth = minimumWidth
|
||||
if (growMaxHeight) maximumHeight = minimumHeight
|
||||
|
||||
return newIndex
|
||||
}
|
||||
|
||||
private fun addDeferredSecrets() {
|
||||
while (true) {
|
||||
val page = deferredSecretPages.removeFirstOrNull() ?: return
|
||||
addAndShowPage(page, -1)
|
||||
}
|
||||
}
|
||||
}
|
44
core/src/com/unciv/ui/utils/WrappableLabel.kt
Normal file
44
core/src/com/unciv/ui/utils/WrappableLabel.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.unciv.models.translations.tr
|
||||
import kotlin.math.min
|
||||
|
||||
/** A [Label] that unlike the original participates correctly in layout
|
||||
* Caveat: You still need to turn wrap on _after_ instantiation, doing it here in init leads to hell.
|
||||
*
|
||||
* @param text Automatically translated text
|
||||
* @param expectedWidth Upper limit for the preferred width the Label will report
|
||||
*/
|
||||
class WrappableLabel(
|
||||
text: String,
|
||||
private val expectedWidth: Float,
|
||||
fontColor: Color = Color.WHITE,
|
||||
fontSize: Int = 18
|
||||
) : Label(text.tr(), CameraStageBaseScreen.skin) {
|
||||
private var _measuredWidth = 0f
|
||||
|
||||
init {
|
||||
if (fontColor != Color.WHITE || fontSize!=18) {
|
||||
val style = LabelStyle(this.style)
|
||||
style.fontColor = fontColor
|
||||
if (fontSize != 18) {
|
||||
style.font = Fonts.font
|
||||
setFontScale(fontSize / Fonts.ORIGINAL_FONT_SIZE)
|
||||
}
|
||||
setStyle(style)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setWrap(wrap: Boolean) {
|
||||
_measuredWidth = super.getPrefWidth()
|
||||
super.setWrap(wrap)
|
||||
}
|
||||
|
||||
private fun getMeasuredWidth(): Float = if (wrap) _measuredWidth else super.getPrefWidth()
|
||||
|
||||
override fun getMinWidth() = 48f // ~ 2 chars
|
||||
override fun getPrefWidth() = min(getMeasuredWidth(), expectedWidth)
|
||||
override fun getMaxWidth() = getMeasuredWidth()
|
||||
}
|
@ -3,93 +3,157 @@ package com.unciv.ui.worldscreen.mainmenu
|
||||
import com.badlogic.gdx.Application
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Input
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.metadata.BaseRuleset
|
||||
import com.unciv.models.ruleset.Ruleset.CheckModLinksStatus
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.tilesets.TileSetCache
|
||||
import com.unciv.models.translations.TranslationFileWriter
|
||||
import com.unciv.models.translations.Translations
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.civilopedia.FormattedLine
|
||||
import com.unciv.ui.civilopedia.MarkupRenderer
|
||||
import com.unciv.ui.civilopedia.SimpleCivilopediaText
|
||||
import com.unciv.ui.utils.*
|
||||
import com.unciv.ui.utils.LanguageTable.Companion.addLanguageTables
|
||||
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.min
|
||||
import com.badlogic.gdx.utils.Array as GdxArray
|
||||
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
|
||||
|
||||
class Language(val language:String, val percentComplete:Int){
|
||||
override fun toString(): String {
|
||||
val spaceSplitLang = language.replace("_"," ")
|
||||
return "$spaceSplitLang - $percentComplete%"
|
||||
}
|
||||
}
|
||||
|
||||
class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScreen) {
|
||||
private var selectedLanguage: String = "English"
|
||||
/**
|
||||
* The Options (Settings) Popup
|
||||
* @param previousScreen Tha caller - note if this is a [WorldScreen] or [MainMenuScreen] they will be rebuilt when major options change.
|
||||
*/
|
||||
//region Fields
|
||||
class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousScreen) {
|
||||
private val settings = previousScreen.game.settings
|
||||
private val optionsTable = Table(CameraStageBaseScreen.skin)
|
||||
private val resolutionArray = GdxArray(arrayOf("750x500", "900x600", "1050x700", "1200x800", "1500x1000"))
|
||||
private val tabs: TabbedPager
|
||||
private val resolutionArray = com.badlogic.gdx.utils.Array(arrayOf("750x500", "900x600", "1050x700", "1200x800", "1500x1000"))
|
||||
private var modCheckFirstRun = true // marker for automatic first run on selecting the page
|
||||
private var modCheckCheckBox: CheckBox? = null
|
||||
private var modCheckResultCell: Cell<Actor>? = null
|
||||
private val selectBoxMinWidth: Float
|
||||
|
||||
//endregion
|
||||
|
||||
init {
|
||||
settings.addCompletedTutorialTask("Open the options table")
|
||||
|
||||
optionsTable.defaults().pad(2.5f)
|
||||
rebuildOptionsTable()
|
||||
innerTable.pad(0f)
|
||||
val tabMaxWidth: Float
|
||||
val tabMinWidth: Float
|
||||
val tabMaxHeight: Float
|
||||
previousScreen.run {
|
||||
selectBoxMinWidth = if (stage.width < 600f) 200f else 240f
|
||||
tabMaxWidth = if (isPortrait()) stage.width - 10f else 0.8f * stage.width
|
||||
tabMinWidth = 0.6f * stage.width
|
||||
tabMaxHeight = (if (isPortrait()) 0.7f else 0.8f) * stage.height
|
||||
}
|
||||
tabs = TabbedPager(tabMinWidth, tabMaxWidth, 0f, tabMaxHeight,
|
||||
headerFontSize = 21, backgroundColor = Color.CLEAR, capacity = 8)
|
||||
add(tabs).pad(0f).grow().row()
|
||||
|
||||
val scrollPane = ScrollPane(optionsTable, skin)
|
||||
scrollPane.setOverscroll(false, false)
|
||||
scrollPane.fadeScrollBars = false
|
||||
scrollPane.setScrollingDisabled(true, false)
|
||||
add(scrollPane).maxHeight(screen.stage.height * 0.6f).row()
|
||||
tabs.addPage("About", getAboutTab(), ImageGetter.getExternalImage("Icon.png"), 24f)
|
||||
tabs.addPage("Display", getDisplayTab(), ImageGetter.getImage("UnitPromotionIcons/Scouting"), 24f)
|
||||
tabs.addPage("Gameplay", getGamePlayTab(), ImageGetter.getImage("OtherIcons/Options"), 24f)
|
||||
tabs.addPage("Language", getLanguageTab(), ImageGetter.getImage("FlagIcons/${settings.language}"), 24f)
|
||||
tabs.addPage("Sound", getSoundTab(), ImageGetter.getImage("OtherIcons/Speaker"), 24f)
|
||||
// at the moment the notification service only exists on Android
|
||||
if (Gdx.app.type == Application.ApplicationType.Android)
|
||||
tabs.addPage("Multiplayer", getMultiplayerTab(), ImageGetter.getImage("OtherIcons/Multiplayer"), 24f)
|
||||
tabs.addPage("Advanced", getAdvancedTab(), ImageGetter.getImage("OtherIcons/Settings"), 24f)
|
||||
if (RulesetCache.size > 1) {
|
||||
tabs.addPage("Locate mod errors", getModCheckTab(), ImageGetter.getImage("OtherIcons/Mods"), 24f) { _, _ ->
|
||||
if (modCheckFirstRun) runModChecker()
|
||||
}
|
||||
}
|
||||
if (Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT) && Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT)) {
|
||||
tabs.addPage("Debug", getDebugTab(), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
|
||||
}
|
||||
|
||||
addCloseButton {
|
||||
previousScreen.game.limitOrientationsHelper?.allowPortrait(settings.allowAndroidPortrait)
|
||||
if (previousScreen is WorldScreen)
|
||||
previousScreen.enableNextTurnButtonAfterOptions()
|
||||
}
|
||||
}.padBottom(10f)
|
||||
|
||||
pack() // Needed to show the background.
|
||||
center(previousScreen.stage)
|
||||
}
|
||||
|
||||
private fun addHeader(text: String) {
|
||||
optionsTable.add(text.toLabel(fontSize = 24)).colspan(2).padTop(if (optionsTable.cells.isEmpty) 0f else 20f).row()
|
||||
}
|
||||
|
||||
private fun addYesNoRow(text: String, initialValue: Boolean, updateWorld: Boolean = false, action: ((Boolean) -> Unit)) {
|
||||
optionsTable.add(text.toLabel())
|
||||
val button = YesNoButton(initialValue, CameraStageBaseScreen.skin) {
|
||||
action(it)
|
||||
settings.save()
|
||||
if (updateWorld && previousScreen is WorldScreen)
|
||||
previousScreen.shouldUpdate = true
|
||||
}
|
||||
optionsTable.add(button).row()
|
||||
override fun setVisible(visible: Boolean) {
|
||||
super.setVisible(visible)
|
||||
if (!visible) return
|
||||
tabs.askForPassword(secretHashCode = 2747985)
|
||||
if (tabs.activePage < 0) tabs.selectPage(2)
|
||||
}
|
||||
|
||||
/** Reload this Popup after major changes (resolution, tileset, language) */
|
||||
private fun reloadWorldAndOptions() {
|
||||
settings.save()
|
||||
if (previousScreen is WorldScreen) {
|
||||
previousScreen.game.worldScreen = WorldScreen(previousScreen.gameInfo, previousScreen.viewingCiv)
|
||||
previousScreen.game.setWorldScreen()
|
||||
|
||||
} else if (previousScreen is MainMenuScreen) {
|
||||
previousScreen.game.setScreen(MainMenuScreen())
|
||||
}
|
||||
(previousScreen.game.screen as CameraStageBaseScreen).openOptionsPopup()
|
||||
}
|
||||
|
||||
private fun rebuildOptionsTable() {
|
||||
settings.save()
|
||||
optionsTable.clear()
|
||||
//region Page builders
|
||||
|
||||
addHeader("Display options")
|
||||
private fun getAboutTab(): Table {
|
||||
defaults().pad(5f)
|
||||
val version = previousScreen.game.version
|
||||
val versionAnchor = version.replace(".","")
|
||||
val lines = sequence {
|
||||
yield(FormattedLine(extraImage = "banner", imageSize = 240f, centered = true))
|
||||
yield(FormattedLine())
|
||||
yield(FormattedLine("{Version}: $version", link = "https://github.com/yairm210/Unciv/blob/master/changelog.md#$versionAnchor"))
|
||||
yield(FormattedLine("See online Readme", link = "https://github.com/yairm210/Unciv/blob/master/README.md#unciv---foss-civ-v-for-androiddesktop"))
|
||||
yield(FormattedLine("Visit repository", link = "https://github.com/yairm210/Unciv"))
|
||||
}
|
||||
return MarkupRenderer.render(lines.toList()).pad(20f)
|
||||
}
|
||||
|
||||
private fun getLanguageTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
val languageTables = this.addLanguageTables(tabs.prefWidth * 0.9f - 10f)
|
||||
|
||||
var chosenLanguage = settings.language
|
||||
fun selectLanguage() {
|
||||
settings.language = chosenLanguage
|
||||
previousScreen.game.translations.tryReadTranslationForCurrentLanguage()
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
fun updateSelection() {
|
||||
languageTables.forEach { it.update(chosenLanguage) }
|
||||
if (chosenLanguage != settings.language)
|
||||
selectLanguage()
|
||||
}
|
||||
updateSelection()
|
||||
|
||||
languageTables.forEach {
|
||||
it.onClick {
|
||||
chosenLanguage = it.language
|
||||
updateSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDisplayTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(2.5f)
|
||||
|
||||
addYesNoRow("Show worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it }
|
||||
addYesNoRow("Show resources and improvements", settings.showResourcesAndImprovements, true) { settings.showResourcesAndImprovements = it }
|
||||
@ -100,8 +164,6 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
addYesNoRow("Show pixel units", settings.showPixelUnits, true) { settings.showPixelUnits = it }
|
||||
addYesNoRow("Show pixel improvements", settings.showPixelImprovements, true) { settings.showPixelImprovements = it }
|
||||
|
||||
addLanguageSelectBox()
|
||||
|
||||
addResolutionSelectBox()
|
||||
|
||||
addTileSetSelectBox()
|
||||
@ -112,16 +174,21 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
}
|
||||
|
||||
val continuousRenderingDescription = "When disabled, saves battery life but certain animations will be suspended"
|
||||
optionsTable.add(continuousRenderingDescription.toLabel(fontSize = 14)).colspan(2).padTop(20f).row()
|
||||
|
||||
addHeader("Gameplay options")
|
||||
val continuousRenderingLabel = WrappableLabel(continuousRenderingDescription,
|
||||
tabs.prefWidth, Color.ORANGE.cpy().lerp(Color.WHITE, 0.7f), 14)
|
||||
continuousRenderingLabel.wrap = true
|
||||
add(continuousRenderingLabel).colspan(2).padTop(10f).row()
|
||||
}
|
||||
|
||||
private fun getGamePlayTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
addYesNoRow("Check for idle units", settings.checkForDueUnits, true) { settings.checkForDueUnits = it }
|
||||
addYesNoRow("Move units with a single tap", settings.singleTapMove) { settings.singleTapMove = it }
|
||||
addYesNoRow("Auto-assign city production", settings.autoAssignCityProduction, true) {
|
||||
settings.autoAssignCityProduction = it
|
||||
if (it && previousScreen is WorldScreen &&
|
||||
previousScreen.viewingCiv.isCurrentPlayer() && previousScreen.viewingCiv.playerType == PlayerType.Human) {
|
||||
previousScreen.viewingCiv.isCurrentPlayer() && previousScreen.viewingCiv.playerType == PlayerType.Human) {
|
||||
previousScreen.gameInfo.currentPlayerCiv.cities.forEach { city ->
|
||||
city.cityConstructions.chooseNextConstruction()
|
||||
}
|
||||
@ -130,25 +197,54 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
addYesNoRow("Auto-build roads", settings.autoBuildingRoads) { settings.autoBuildingRoads = it }
|
||||
addYesNoRow("Automated workers replace improvements", settings.automatedWorkersReplaceImprovements) { settings.automatedWorkersReplaceImprovements = it }
|
||||
addYesNoRow("Order trade offers by amount", settings.orderTradeOffersByAmount) { settings.orderTradeOffersByAmount = it }
|
||||
}
|
||||
|
||||
private fun getSoundTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
|
||||
addSoundEffectsVolumeSlider()
|
||||
|
||||
val musicLocation = Gdx.files.local(previousScreen.game.musicLocation)
|
||||
if (musicLocation.exists())
|
||||
addMusicVolumeSlider()
|
||||
else
|
||||
addDownloadMusic(musicLocation)
|
||||
}
|
||||
|
||||
private fun getMultiplayerTab(): Table = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
|
||||
addYesNoRow("Enable out-of-game turn notifications", settings.multiplayerTurnCheckerEnabled) {
|
||||
settings.multiplayerTurnCheckerEnabled = it
|
||||
settings.save()
|
||||
tabs.replacePage("Multiplayer", getMultiplayerTab())
|
||||
}
|
||||
|
||||
if (settings.multiplayerTurnCheckerEnabled) {
|
||||
addMultiplayerTurnCheckerDelayBox()
|
||||
|
||||
addYesNoRow("Show persistent notification for turn notifier service", settings.multiplayerTurnCheckerPersistentNotificationEnabled)
|
||||
{ settings.multiplayerTurnCheckerPersistentNotificationEnabled = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAdvancedTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
|
||||
addAutosaveTurnsSelectBox()
|
||||
|
||||
// at the moment the notification service only exists on Android
|
||||
addNotificationOptions()
|
||||
|
||||
addHeader("Other options")
|
||||
|
||||
|
||||
addYesNoRow("{Show experimental world wrap for maps}\n{HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED!}".tr(),
|
||||
settings.showExperimentalWorldWrap) {
|
||||
addYesNoRow("{Show experimental world wrap for maps}\n{HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED!}",
|
||||
settings.showExperimentalWorldWrap) {
|
||||
settings.showExperimentalWorldWrap = it
|
||||
}
|
||||
addYesNoRow("{Enable experimental religion in start games}\n{HIGHLY EXPERIMENTAL - UPDATES WILL BREAK SAVES!}".tr(),
|
||||
settings.showExperimentalReligion) {
|
||||
addYesNoRow("{Enable experimental religion in start games}\n{HIGHLY EXPERIMENTAL - UPDATES WILL BREAK SAVES!}",
|
||||
settings.showExperimentalReligion) {
|
||||
settings.showExperimentalReligion = it
|
||||
}
|
||||
|
||||
|
||||
if (previousScreen.game.limitOrientationsHelper != null) {
|
||||
addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) {
|
||||
settings.allowAndroidPortrait = it
|
||||
@ -157,26 +253,77 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
}
|
||||
}
|
||||
|
||||
addSoundEffectsVolumeSlider()
|
||||
addMusicVolumeSlider()
|
||||
|
||||
addTranslationGeneration()
|
||||
addModCheckerPopup()
|
||||
addSetUserId()
|
||||
|
||||
optionsTable.add("Version".toLabel()).pad(10f)
|
||||
val versionLabel = previousScreen.game.version.toLabel()
|
||||
if (previousScreen.game.version[0] in '0'..'9')
|
||||
versionLabel.onClick {
|
||||
val url = "https://github.com/yairm210/Unciv/blob/master/changelog.md#" +
|
||||
previousScreen.game.version.replace(".","")
|
||||
Gdx.net.openURI(url)
|
||||
}
|
||||
optionsTable.add(versionLabel).pad(10f).row()
|
||||
addSetUserId()
|
||||
}
|
||||
|
||||
private fun addMinimapSizeSlider() {
|
||||
optionsTable.add("Show minimap".tr())
|
||||
private fun getModCheckTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
defaults().pad(10f).align(Align.top)
|
||||
modCheckCheckBox = "Check extension mods based on vanilla".toCheckBox {
|
||||
runModChecker(it)
|
||||
}
|
||||
add(modCheckCheckBox).row()
|
||||
modCheckResultCell = add("Checking mods for errors...".toLabel())
|
||||
}
|
||||
|
||||
private fun runModChecker(complex: Boolean = false) {
|
||||
modCheckFirstRun = false
|
||||
if (modCheckCheckBox == null) return
|
||||
modCheckCheckBox!!.disable()
|
||||
if (modCheckResultCell == null) return
|
||||
thread(name="ModChecker") {
|
||||
val lines = ArrayList<FormattedLine>()
|
||||
var noProblem = true
|
||||
for (mod in RulesetCache.values.sortedBy { it.name }) {
|
||||
val modLinks = if (complex) RulesetCache.checkCombinedModLinks(linkedSetOf(mod.name))
|
||||
else mod.checkModLinks()
|
||||
val color = when (modLinks.status) {
|
||||
CheckModLinksStatus.OK -> "#0F0"
|
||||
CheckModLinksStatus.Warning -> "#FF0"
|
||||
CheckModLinksStatus.Error -> "#F00"
|
||||
}
|
||||
val label = if (mod.name.isEmpty()) BaseRuleset.Civ_V_Vanilla.fullName else mod.name
|
||||
lines += FormattedLine("$label{}", starred = true, color = color, header = 3)
|
||||
if (modLinks.isNotOK()) {
|
||||
lines += FormattedLine(modLinks.message)
|
||||
noProblem = false
|
||||
}
|
||||
lines += FormattedLine()
|
||||
}
|
||||
if (noProblem) lines += FormattedLine("{No problems found}.")
|
||||
|
||||
Gdx.app.postRunnable {
|
||||
val result = SimpleCivilopediaText(lines).renderCivilopediaText(tabs.prefWidth - 25f)
|
||||
modCheckResultCell?.setActor(result)
|
||||
modCheckCheckBox!!.enable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDebugTab() = Table(CameraStageBaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
|
||||
val game = UncivGame.Current
|
||||
add("Supercharged".toCheckBox(game.superchargedForDebug) {
|
||||
game.superchargedForDebug = it
|
||||
}).row()
|
||||
add("View entire map".toCheckBox(game.viewEntireMapForDebug) {
|
||||
game.viewEntireMapForDebug = it
|
||||
}).row()
|
||||
if (game.isGameInfoInitialized()) {
|
||||
add("God mode (current game)".toCheckBox(game.gameInfo.gameParameters.godMode) {
|
||||
game.gameInfo.gameParameters.godMode = it
|
||||
}).row()
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
//region Row builders
|
||||
|
||||
private fun Table.addMinimapSizeSlider() {
|
||||
add("Show minimap".toLabel()).left().fillX()
|
||||
|
||||
// The meaning of the values needs a formula to be synchronized between here and
|
||||
// [Minimap.init]. It goes off-10%-11%..29%-30%-35%-40%-45%-50% - and the percentages
|
||||
@ -203,49 +350,161 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
if (previousScreen is WorldScreen)
|
||||
previousScreen.shouldUpdate = true
|
||||
}
|
||||
optionsTable.add(minimapSlider).pad(10f).row()
|
||||
add(minimapSlider).pad(10f).row()
|
||||
}
|
||||
|
||||
private fun addSetUserId() {
|
||||
val idSetLabel = "".toLabel()
|
||||
val takeUserIdFromClipboardButton = "Take user ID from clipboard".toTextButton()
|
||||
.onClick {
|
||||
try {
|
||||
val clipboardContents = Gdx.app.clipboard.contents.trim()
|
||||
UUID.fromString(clipboardContents)
|
||||
YesNoPopup("Doing this will reset your current user ID to the clipboard contents - are you sure?",
|
||||
{
|
||||
settings.userId = clipboardContents
|
||||
settings.save()
|
||||
idSetLabel.setFontColor(Color.WHITE).setText("ID successfully set!".tr())
|
||||
}, previousScreen).open(true)
|
||||
idSetLabel.isVisible = true
|
||||
} catch (ex: Exception) {
|
||||
idSetLabel.isVisible = true
|
||||
idSetLabel.setFontColor(Color.RED).setText("Invalid ID!".tr())
|
||||
private fun Table.addResolutionSelectBox() {
|
||||
add("Resolution".toLabel()).left().fillX()
|
||||
|
||||
val resolutionSelectBox = SelectBox<String>(skin)
|
||||
resolutionSelectBox.items = resolutionArray
|
||||
resolutionSelectBox.selected = settings.resolution
|
||||
add(resolutionSelectBox).minWidth(selectBoxMinWidth).pad(10f).row()
|
||||
|
||||
resolutionSelectBox.onChange {
|
||||
settings.resolution = resolutionSelectBox.selected
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Table.addTileSetSelectBox() {
|
||||
add("Tileset".toLabel()).left().fillX()
|
||||
|
||||
val tileSetSelectBox = SelectBox<String>(skin)
|
||||
val tileSetArray = GdxArray<String>()
|
||||
val tileSets = ImageGetter.getAvailableTilesets()
|
||||
for (tileset in tileSets) tileSetArray.add(tileset)
|
||||
tileSetSelectBox.items = tileSetArray
|
||||
tileSetSelectBox.selected = settings.tileSet
|
||||
add(tileSetSelectBox).minWidth(selectBoxMinWidth).pad(10f).row()
|
||||
|
||||
tileSetSelectBox.onChange {
|
||||
settings.tileSet = tileSetSelectBox.selected
|
||||
TileSetCache.assembleTileSetConfigs()
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Table.addSoundEffectsVolumeSlider() {
|
||||
add("Sound effects volume".tr()).left().fillX()
|
||||
|
||||
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
||||
initial = settings.soundEffectsVolume
|
||||
) {
|
||||
settings.soundEffectsVolume = it
|
||||
settings.save()
|
||||
}
|
||||
add(soundEffectsVolumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun Table.addMusicVolumeSlider() {
|
||||
add("Music volume".tr()).left().fillX()
|
||||
|
||||
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
||||
initial = settings.musicVolume,
|
||||
sound = UncivSound.Silent
|
||||
) {
|
||||
settings.musicVolume = it
|
||||
settings.save()
|
||||
|
||||
val music = previousScreen.game.music
|
||||
if (music == null) // restart music, if it was off at the app start
|
||||
thread(name = "Music") { previousScreen.game.startMusic() }
|
||||
|
||||
music?.volume = 0.4f * it
|
||||
}
|
||||
musicVolumeSlider.value = settings.musicVolume
|
||||
add(musicVolumeSlider).pad(5f).row()
|
||||
}
|
||||
|
||||
private fun Table.addDownloadMusic(musicLocation: FileHandle) {
|
||||
val downloadMusicButton = "Download music".toTextButton()
|
||||
add(downloadMusicButton).colspan(2).row()
|
||||
val errorTable = Table()
|
||||
add(errorTable).colspan(2).row()
|
||||
|
||||
downloadMusicButton.onClick {
|
||||
downloadMusicButton.disable()
|
||||
errorTable.clear()
|
||||
errorTable.add("Downloading...".toLabel())
|
||||
|
||||
// So the whole game doesn't get stuck while downloading the file
|
||||
thread(name = "Music") {
|
||||
try {
|
||||
val file = DropBox.downloadFile("/Music/thatched-villagers.mp3")
|
||||
musicLocation.write(file, false)
|
||||
Gdx.app.postRunnable {
|
||||
tabs.replacePage("Sound", getSoundTab())
|
||||
previousScreen.game.startMusic()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Gdx.app.postRunnable {
|
||||
errorTable.clear()
|
||||
errorTable.add("Could not download music!".toLabel(Color.RED))
|
||||
}
|
||||
}
|
||||
optionsTable.add(takeUserIdFromClipboardButton).pad(5f).colspan(2).row()
|
||||
optionsTable.add(idSetLabel).colspan(2).row()
|
||||
}
|
||||
|
||||
private fun addNotificationOptions() {
|
||||
if (Gdx.app.type == Application.ApplicationType.Android) {
|
||||
addHeader("Multiplayer options")
|
||||
|
||||
addYesNoRow("Enable out-of-game turn notifications", settings.multiplayerTurnCheckerEnabled)
|
||||
{ settings.multiplayerTurnCheckerEnabled = it }
|
||||
|
||||
if (settings.multiplayerTurnCheckerEnabled) {
|
||||
addMultiplayerTurnCheckerDelayBox()
|
||||
|
||||
addYesNoRow("Show persistent notification for turn notifier service", settings.multiplayerTurnCheckerPersistentNotificationEnabled)
|
||||
{ settings.multiplayerTurnCheckerPersistentNotificationEnabled = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTranslationGeneration() {
|
||||
private fun Table.addMultiplayerTurnCheckerDelayBox() {
|
||||
add("Time between turn checks out-of-game (in minutes)".toLabel()).left().fillX()
|
||||
|
||||
val checkDelaySelectBox = SelectBox<Int>(skin)
|
||||
val possibleDelaysArray = GdxArray<Int>()
|
||||
possibleDelaysArray.addAll(1, 2, 5, 15)
|
||||
checkDelaySelectBox.items = possibleDelaysArray
|
||||
checkDelaySelectBox.selected = settings.multiplayerTurnCheckerDelayInMinutes
|
||||
|
||||
add(checkDelaySelectBox).pad(10f).row()
|
||||
|
||||
checkDelaySelectBox.onChange {
|
||||
settings.multiplayerTurnCheckerDelayInMinutes = checkDelaySelectBox.selected
|
||||
settings.save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Table.addSetUserId() {
|
||||
val idSetLabel = "".toLabel()
|
||||
val takeUserIdFromClipboardButton = "Take user ID from clipboard".toTextButton()
|
||||
.onClick {
|
||||
try {
|
||||
val clipboardContents = Gdx.app.clipboard.contents.trim()
|
||||
UUID.fromString(clipboardContents)
|
||||
YesNoPopup("Doing this will reset your current user ID to the clipboard contents - are you sure?",
|
||||
{
|
||||
settings.userId = clipboardContents
|
||||
settings.save()
|
||||
idSetLabel.setFontColor(Color.WHITE).setText("ID successfully set!".tr())
|
||||
}, previousScreen).open(true)
|
||||
idSetLabel.isVisible = true
|
||||
} catch (ex: Exception) {
|
||||
idSetLabel.isVisible = true
|
||||
idSetLabel.setFontColor(Color.RED).setText("Invalid ID!".tr())
|
||||
}
|
||||
}
|
||||
add(takeUserIdFromClipboardButton).pad(5f).colspan(2).row()
|
||||
add(idSetLabel).colspan(2).row()
|
||||
}
|
||||
|
||||
private fun Table.addAutosaveTurnsSelectBox() {
|
||||
add("Turns between autosaves".toLabel()).left().fillX()
|
||||
|
||||
val autosaveTurnsSelectBox = SelectBox<Int>(skin)
|
||||
val autosaveTurnsArray = GdxArray<Int>()
|
||||
autosaveTurnsArray.addAll(1, 2, 5, 10)
|
||||
autosaveTurnsSelectBox.items = autosaveTurnsArray
|
||||
autosaveTurnsSelectBox.selected = settings.turnsBetweenAutosaves
|
||||
|
||||
add(autosaveTurnsSelectBox).pad(10f).row()
|
||||
|
||||
autosaveTurnsSelectBox.onChange {
|
||||
settings.turnsBetweenAutosaves = autosaveTurnsSelectBox.selected
|
||||
settings.save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Table.addTranslationGeneration() {
|
||||
if (Gdx.app.type == Application.ApplicationType.Desktop) {
|
||||
val generateTranslationsButton = "Generate translation files".toTextButton()
|
||||
val generateAction = {
|
||||
@ -259,218 +518,55 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr
|
||||
generateTranslationsButton.onClick(generateAction)
|
||||
keyPressDispatcher[Input.Keys.F12] = generateAction
|
||||
generateTranslationsButton.addTooltip("F12",18f)
|
||||
optionsTable.add(generateTranslationsButton).colspan(2).row()
|
||||
add(generateTranslationsButton).colspan(2).row()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addModCheckerPopup() {
|
||||
//if (RulesetCache.isEmpty()) return
|
||||
val modCheckerButton = "Locate mod errors".toTextButton()
|
||||
modCheckerButton.onClick {
|
||||
val lines = ArrayList<String>()
|
||||
for (mod in RulesetCache.values) {
|
||||
val modLinks = mod.checkModLinks()
|
||||
if (modLinks.isNotOK()) {
|
||||
lines += ""
|
||||
lines += mod.name
|
||||
lines += ""
|
||||
lines += modLinks.message
|
||||
lines += ""
|
||||
}
|
||||
}
|
||||
if (lines.isEmpty()) lines += "{No problems found}."
|
||||
val popup = Popup(screen)
|
||||
popup.name = "ModCheckerPopup"
|
||||
popup.add(ScrollPane(lines.joinToString("\n").toLabel()).apply { setOverscroll(false, false) })
|
||||
.maxHeight(screen.stage.height / 2).row()
|
||||
popup.addCloseButton()
|
||||
popup.open(true)
|
||||
}
|
||||
optionsTable.add(modCheckerButton).colspan(2).row()
|
||||
}
|
||||
|
||||
private fun addSoundEffectsVolumeSlider() {
|
||||
optionsTable.add("Sound effects volume".tr())
|
||||
|
||||
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
||||
initial = settings.soundEffectsVolume
|
||||
) {
|
||||
settings.soundEffectsVolume = it
|
||||
private fun Table.addYesNoRow(text: String, initialValue: Boolean, updateWorld: Boolean = false, action: ((Boolean) -> Unit)) {
|
||||
val wrapWidth = tabs.prefWidth - 60f
|
||||
add(WrappableLabel(text, wrapWidth).apply { wrap = true })
|
||||
.left().fillX()
|
||||
.maxWidth(wrapWidth)
|
||||
val button = YesNoButton(initialValue, CameraStageBaseScreen.skin) {
|
||||
action(it)
|
||||
settings.save()
|
||||
if (updateWorld && previousScreen is WorldScreen)
|
||||
previousScreen.shouldUpdate = true
|
||||
}
|
||||
optionsTable.add(soundEffectsVolumeSlider).pad(5f).row()
|
||||
add(button).row()
|
||||
}
|
||||
|
||||
private fun addMusicVolumeSlider() {
|
||||
val musicLocation = Gdx.files.local(previousScreen.game.musicLocation)
|
||||
if (musicLocation.exists()) {
|
||||
optionsTable.add("Music volume".tr())
|
||||
//endregion
|
||||
|
||||
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
||||
initial = settings.musicVolume,
|
||||
sound = UncivSound.Silent
|
||||
) {
|
||||
settings.musicVolume = it
|
||||
settings.save()
|
||||
/**
|
||||
* This TextButton subclass helps to keep looks and behaviour of our Yes/No
|
||||
* in one place, but it also helps keeping context for those action lambdas.
|
||||
*
|
||||
* Usage: YesNoButton(someSetting: Boolean, skin) { someSetting = it; sideEffects() }
|
||||
*/
|
||||
private class YesNoButton(
|
||||
initialValue: Boolean,
|
||||
skin: Skin,
|
||||
action: (Boolean) -> Unit
|
||||
) : TextButton (initialValue.toYesNo(), skin ) {
|
||||
|
||||
val music = previousScreen.game.music
|
||||
if (music == null) // restart music, if it was off at the app start
|
||||
thread(name = "Music") { previousScreen.game.startMusic() }
|
||||
|
||||
music?.volume = 0.4f * it
|
||||
var value = initialValue
|
||||
private set(value) {
|
||||
field = value
|
||||
setText(value.toYesNo())
|
||||
}
|
||||
musicVolumeSlider.value = settings.musicVolume
|
||||
optionsTable.add(musicVolumeSlider).pad(5f).row()
|
||||
} else {
|
||||
val downloadMusicButton = "Download music".toTextButton()
|
||||
optionsTable.add(downloadMusicButton).colspan(2).row()
|
||||
val errorTable = Table()
|
||||
optionsTable.add(errorTable).colspan(2).row()
|
||||
|
||||
downloadMusicButton.onClick {
|
||||
downloadMusicButton.disable()
|
||||
errorTable.clear()
|
||||
errorTable.add("Downloading...".toLabel())
|
||||
|
||||
// So the whole game doesn't get stuck while downloading the file
|
||||
thread(name = "Music") {
|
||||
try {
|
||||
val file = DropBox.downloadFile("/Music/thatched-villagers.mp3")
|
||||
musicLocation.write(file, false)
|
||||
Gdx.app.postRunnable {
|
||||
rebuildOptionsTable()
|
||||
previousScreen.game.startMusic()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Gdx.app.postRunnable {
|
||||
errorTable.clear()
|
||||
errorTable.add("Could not download music!".toLabel(Color.RED))
|
||||
}
|
||||
}
|
||||
}
|
||||
init {
|
||||
color = ImageGetter.getBlue()
|
||||
onClick {
|
||||
value = !value
|
||||
action.invoke(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addResolutionSelectBox() {
|
||||
optionsTable.add("Resolution".toLabel())
|
||||
|
||||
val resolutionSelectBox = SelectBox<String>(skin)
|
||||
resolutionSelectBox.items = resolutionArray
|
||||
resolutionSelectBox.selected = settings.resolution
|
||||
optionsTable.add(resolutionSelectBox).minWidth(240f).pad(10f).row()
|
||||
|
||||
resolutionSelectBox.onChange {
|
||||
settings.resolution = resolutionSelectBox.selected
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTileSetSelectBox() {
|
||||
optionsTable.add("Tileset".toLabel())
|
||||
|
||||
val tileSetSelectBox = SelectBox<String>(skin)
|
||||
val tileSetArray = GdxArray<String>()
|
||||
val tileSets = ImageGetter.getAvailableTilesets()
|
||||
for (tileset in tileSets) tileSetArray.add(tileset)
|
||||
tileSetSelectBox.items = tileSetArray
|
||||
tileSetSelectBox.selected = settings.tileSet
|
||||
optionsTable.add(tileSetSelectBox).minWidth(240f).pad(10f).row()
|
||||
|
||||
tileSetSelectBox.onChange {
|
||||
settings.tileSet = tileSetSelectBox.selected
|
||||
TileSetCache.assembleTileSetConfigs()
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAutosaveTurnsSelectBox() {
|
||||
optionsTable.add("Turns between autosaves".toLabel())
|
||||
|
||||
val autosaveTurnsSelectBox = SelectBox<Int>(skin)
|
||||
val autosaveTurnsArray = GdxArray<Int>()
|
||||
autosaveTurnsArray.addAll(1, 2, 5, 10)
|
||||
autosaveTurnsSelectBox.items = autosaveTurnsArray
|
||||
autosaveTurnsSelectBox.selected = settings.turnsBetweenAutosaves
|
||||
|
||||
optionsTable.add(autosaveTurnsSelectBox).pad(10f).row()
|
||||
|
||||
autosaveTurnsSelectBox.onChange {
|
||||
settings.turnsBetweenAutosaves = autosaveTurnsSelectBox.selected
|
||||
settings.save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMultiplayerTurnCheckerDelayBox() {
|
||||
optionsTable.add("Time between turn checks out-of-game (in minutes)".toLabel())
|
||||
|
||||
val checkDelaySelectBox = SelectBox<Int>(skin)
|
||||
val possibleDelaysArray = GdxArray<Int>()
|
||||
possibleDelaysArray.addAll(1, 2, 5, 15)
|
||||
checkDelaySelectBox.items = possibleDelaysArray
|
||||
checkDelaySelectBox.selected = settings.multiplayerTurnCheckerDelayInMinutes
|
||||
|
||||
optionsTable.add(checkDelaySelectBox).pad(10f).row()
|
||||
|
||||
checkDelaySelectBox.onChange {
|
||||
settings.multiplayerTurnCheckerDelayInMinutes = checkDelaySelectBox.selected
|
||||
settings.save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLanguageSelectBox() {
|
||||
val languageSelectBox = SelectBox<Language>(skin)
|
||||
val languageArray = GdxArray<Language>()
|
||||
previousScreen.game.translations.percentCompleteOfLanguages
|
||||
.map { Language(it.key, if (it.key == "English") 100 else it.value) }
|
||||
.sortedByDescending { it.percentComplete }
|
||||
.forEach { languageArray.add(it) }
|
||||
if (languageArray.size == 0) return
|
||||
|
||||
optionsTable.add("Language".toLabel())
|
||||
languageSelectBox.items = languageArray
|
||||
val matchingLanguage = languageArray.firstOrNull { it.language == settings.language }
|
||||
languageSelectBox.selected = matchingLanguage ?: languageArray.first()
|
||||
optionsTable.add(languageSelectBox).minWidth(240f).pad(10f).row()
|
||||
|
||||
languageSelectBox.onChange {
|
||||
// Sometimes the "changed" is triggered even when we didn't choose something
|
||||
selectedLanguage = languageSelectBox.selected.language
|
||||
|
||||
if (selectedLanguage != settings.language)
|
||||
selectLanguage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectLanguage() {
|
||||
settings.language = selectedLanguage
|
||||
previousScreen.game.translations.tryReadTranslationForCurrentLanguage()
|
||||
reloadWorldAndOptions()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
This TextButton subclass helps to keep looks and behaviour of our Yes/No
|
||||
in one place, but it also helps keeping context for those action lambdas.
|
||||
|
||||
Usage: YesNoButton(someSetting: Boolean, skin) { someSetting = it; sideEffects() }
|
||||
*/
|
||||
private fun Boolean.toYesNo(): String = (if (this) Constants.yes else Constants.no).tr()
|
||||
private class YesNoButton(initialValue: Boolean, skin: Skin, action: (Boolean) -> Unit)
|
||||
: TextButton (initialValue.toYesNo(), skin ) {
|
||||
|
||||
var value = initialValue
|
||||
private set(value) {
|
||||
field = value
|
||||
setText(value.toYesNo())
|
||||
}
|
||||
|
||||
init {
|
||||
color = ImageGetter.getBlue()
|
||||
onClick {
|
||||
value = !value
|
||||
action.invoke(value)
|
||||
companion object {
|
||||
fun Boolean.toYesNo(): String = (if (this) Constants.yes else Constants.no).tr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user