Merge remote-tracking branch 'origin/master'

This commit is contained in:
yairm210 2021-09-04 21:40:41 +03:00
commit a1fdb612a9
17 changed files with 1657 additions and 1082 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 KiB

After

Width:  |  Height:  |  Size: 997 KiB

View File

@ -415,9 +415,17 @@ Could not save game to custom location! = Speichern in externem Speicherort fehl
# Options
Options = Optionen
Display options = Anzeigeeinstellungen
Gameplay options = Spielmechanikeinstellungen
Other options = Andere Einstellungen
About = Über
Display = Anzeige
Gameplay = Spielmechanik
Sound = Sound
Multiplayer = Mehrspieler
Advanced = Erweitert
Locate mod errors = Mod-Probleme
Debug = Nur für Eingeweihte
See online Readme = Readme online öffnen
Visit repository = Repository der Entwickler besuchen
Turns between autosaves = Runden bis zum nächsten automatischen Speichern
Sound effects volume = Lautstärke Soundeffekte
Music volume = Lautstärke Musik
@ -444,13 +452,15 @@ Show tile yields = Felderträge anzeigen
Continuous rendering = Kontinuierliches Rendern
When disabled, saves battery life but certain animations will be suspended = Es spart Akku, wenn es deaktiviert ist, aber bestimmte Animationen werden nicht angezeigt.
Order trade offers by amount = Handelsangebote nach Menge sortieren
Check extension mods based on vanilla = Erweiterungs-Mods mit Vanilla-Regelsatz prüfen
Checking mods for errors... = Mods werden geprüft...
Show experimental world wrap for maps = 'World Wrap'-Option für neue Karten anbieten
HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED! = WARNUNG: HOCHGRADIG EXPERIMENTELL - DU WURDEST GEWARNT!
HIGHLY EXPERIMENTAL - UPDATES WILL BREAK SAVES! = WARNUNG: HOCHGRADIG EXPERIMENTELL - UPDATES WERDEN SPEICHERSTÄNDE ZERSTÖREN!
Enable portrait orientation = Hochkant-Orientierung zulassen
Generate translation files = Erstelle Übersetzungsdateien
Translation files are generated successfully. = Die Übersetzungsdateien wurden erfolgreich erstellt.
Locate mod errors = Mod-Fehler lokalisieren
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. = Bitte beachte, daß die Übersetzungen eine andauernde Leistung einer Gemeinschaft von Freiwilligen sind und damit oft unvollständig. Die angezeigte Prozentzahl bedeutet den Anteil übersetzter Texte im gesamten Spiel. Wenn Du helfen willst, die Übersetzungen zu verbessern - dies ist ein Link zur Anleitung.
# Notifications
@ -466,7 +476,7 @@ You have entered a Golden Age! = Ein Goldenes Zeitalter hat begonnen!
A [greatPerson] has been born in [cityName]! = [cityName] - Ein [greatPerson] wurde geboren!
We have encountered [civName]! = Wir sind auf [civName] getroffen!
[cityStateName] has given us [stats] as a token of goodwill for meeting us = [cityStateName] hat uns [stats] als Zeichen des guten Willens für unsere Begegnung übergeben
[cityStateName] has given us [stats] as we are the first major civ to meet them = [cityStateName] hat uns [stats] übergeben, da wir die erste bedeutende Zivilisation sind, die er getroffen hat
[cityStateName] has given us [stats] as we are the first major civ to meet them = [cityStateName] hat uns [stats] übergeben, da wir die erste bedeutende Zivilisation sind, die sie getroffen haben
Cannot provide unit upkeep for [unitName] - unit has been disbanded! = Der Unterhalt für [unitName] konnte nicht bezahlt werden - Einheit wurde aufgelöst!
[cityName] has grown! = [cityName] ist gewachsen!
[cityName] is starving! = [cityName] verhungert!
@ -1128,7 +1138,6 @@ Choose name for [unitName] = Wähle Namen für [unitName]
# Multiplayer Turn Checker Service
Multiplayer options = Mehrspieler Einstellungen
Enable out-of-game turn notifications = Aktiviere Zug Benachrichtigungen außerhalb des Spiels
Time between turn checks out-of-game (in minutes) = Intervall zwischen Zug Prüfungen (in Minuten)
Show persistent notification for turn notifier service = Zeige dauerhafte Benachrichtigung für den Zug-Benachrichtungsdienst

View File

@ -420,9 +420,16 @@ Could not save game to custom location! =
# Options
Options =
Display options =
Gameplay options =
Other options =
About =
Display =
Gameplay =
Sound =
Advanced =
Multiplayer =
Locate mod errors =
Debug =
See online Readme =
Turns between autosaves =
Sound effects volume =
Music volume =
@ -444,18 +451,19 @@ off =
Show pixel units =
Show pixel improvements =
Enable nuclear weapons =
Fontset =
Show tile yields =
Continuous rendering =
When disabled, saves battery life but certain animations will be suspended =
Order trade offers by amount =
Check extension mods based on vanilla =
Checking mods for errors... =
Show experimental world wrap for maps =
HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED! =
HIGHLY EXPERIMENTAL - UPDATES WILL BREAK SAVES! =
Enable portrait orientation =
Generate translation files =
Translation files are generated successfully. =
Locate mod errors =
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. =
# Notifications
@ -1135,7 +1143,6 @@ Choose name for [unitName] =
# Multiplayer Turn Checker Service
Multiplayer options =
Enable out-of-game turn notifications =
Time between turn checks out-of-game (in minutes) =
Show persistent notification for turn notifier service =

View File

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

View File

@ -367,8 +367,8 @@ class TileMap {
}
} else {
// Yes the map generator calls this repeatedly, and we don't want to end up with an oversized tileMatrix
// rightX is -leftX or -leftX + 1
if (tileMatrix.size != 1 - 2 * leftX && tileMatrix.size != 2 - 2 * leftX)
// rightX is -leftX or -leftX + 1 or -leftX + 2
if (tileMatrix.size !in (1 - 2 * leftX)..(3 - 2 * leftX))
throw(IllegalStateException("TileMap.setTransients called on existing tileMatrix of different size"))
}

View File

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

View 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
}
}
}

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

View 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()
}

View File

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