Refactor: UncivTextField (#11705)

* Refactor UncivTextField to be a normal Widget

* Remove UncivTextField `create` factory
This commit is contained in:
SomeTroglodyte 2024-06-22 21:14:54 +02:00 committed by GitHub
parent 149e90bfed
commit 05020a7d7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 297 additions and 291 deletions

View File

@ -19,7 +19,7 @@ import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.models.UncivSound
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.toLabel
@ -65,7 +65,7 @@ open class FileChooser(
}
// components
private val fileNameInput = UncivTextField.create("Please enter a file name")
private val fileNameInput = UncivTextField("Please enter a file name")
private val fileNameLabel = "File name:".toLabel()
private val fileNameWrapper = Table().apply {
defaults().space(10f)

View File

@ -1,240 +0,0 @@
package com.unciv.ui.components
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.scenes.scene2d.utils.FocusListener
import com.unciv.Constants
import com.unciv.logic.event.EventBus
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.getAscendant
import com.unciv.ui.components.extensions.getOverlap
import com.unciv.ui.components.extensions.right
import com.unciv.ui.components.extensions.stageBoundingBox
import com.unciv.ui.components.extensions.top
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.utils.Concurrency
import com.unciv.utils.withGLContext
import kotlinx.coroutines.delay
object UncivTextField {
/**
* Creates a text field that has nicer platform-specific input added compared to the default Gdx [TextField].
*
* - On Android, manages on-screen keyboard visibility and reacts to how the on-screen keyboard reduces screen space.
* - Tries to scroll the field into view when receiving focus using an ascendant ScrollPane.
* - If view of the field is still obscured, show am "emergency" Popup instead (can help when on Android the on-screen keyboard is large and the screen small).
* - If this TextField handles the Tab key (see [keyShortcuts]), its [focus navigation feature][TextField.next] is disabled (otherwise it would search for another TextField in the same parent to give focus to, and remove the on-screen keyboard if it finds none).
* - All parameters are supported on all platforms.
*
* @param hint The text that should be displayed in the text field when no text is entered and it does not have focus, will automatically be translated. Also shown as Label in the "emergency" Popup.
* @param preEnteredText the text initially entered within this text field.
* @param onFocusChange a callback that will be notified when this TextField loses or gains the [keyboardFocus][com.badlogic.gdx.scenes.scene2d.Stage.keyboardFocus].
* Receiver is the field, so you can simply use its elements. Parameter `it` is a Boolean indicating focus was received.
*/
fun create(hint: String, preEnteredText: String = "", onFocusChange: (TextField.(Boolean) -> Unit)? = null): TextField {
@Suppress("UNCIV_RAW_TEXTFIELD")
val textField = object : TextField(preEnteredText, BaseScreen.skin) {
override fun next(up: Boolean) {
if (KeyCharAndCode.TAB in keyShortcuts) return
super.next(up)
}
// Note - this way to force TextField to display `[]` characters normally is an incomplete hack.
// The complete way would either require overriding `updateDisplayText` which is private, or all methods calling it,
// which are many including the keyboard listener, or keep a separate font without markup enabled around and put that
// into the default style, including its own NativeBitmapFontData instance and texture - involves quite some redesign.
// That said, observing the deficiency is hard - the internal `glyphPositions` could theoretically get out of sync, affecting selection and caret display.
override fun layout() {
val oldEnable = style.font.data.markupEnabled
style.font.data.markupEnabled = false
super.layout()
style.font.data.markupEnabled = oldEnable
}
override fun drawText(batch: Batch, font: BitmapFont, x: Float, y: Float) {
val oldEnable = font.data.markupEnabled
font.data.markupEnabled = false
super.drawText(batch, font, x, y)
font.data.markupEnabled = oldEnable
}
}
val translatedHint = hint.tr()
textField.messageText = translatedHint
textField.addListener(object : FocusListener() {
override fun keyboardFocusChanged(event: FocusEvent, actor: Actor, focused: Boolean) {
if (focused) {
textField.scrollAscendantToTextField()
}
onFocusChange?.invoke(textField, focused)
}
})
if (Gdx.app.type == Application.ApplicationType.Android)
TextfieldImprovements.add(textField)
return textField
}
}
/**
* Tries to scroll a [ScrollPane] ascendant of the text field so that this text field is in the middle of the visible area.
*
* @return true if the text field is visible after this operation
*/
fun TextField.scrollAscendantToTextField(): Boolean {
val stage = this.stage
if (stage !is UncivStage) return false
val scrollPane = this.getAscendant { it is ScrollPane } as ScrollPane?
val visibleArea = stage.lastKnownVisibleArea
val textFieldStageBoundingBox = this.stageBoundingBox
if (scrollPane == null) return visibleArea.contains(textFieldStageBoundingBox)
val scrollPaneBounds = scrollPane.stageBoundingBox
val visibleScrollPaneArea = scrollPaneBounds.getOverlap(visibleArea)
if (visibleScrollPaneArea == null) {
return false
} else if (visibleScrollPaneArea.contains(textFieldStageBoundingBox)) {
return true
}
val scrollContent = scrollPane.actor
val textFieldScrollContentCoords = localToAscendantCoordinates(scrollContent, Vector2(0f, 0f))
// It's possible that our textField can't be (fully) scrolled to be within the visible scrollPane area
val pixelsNotVisibleOnLeftSide = (visibleScrollPaneArea.x - scrollPaneBounds.x).coerceAtLeast(0f)
val textFieldDistanceFromLeftSide = textFieldScrollContentCoords.x
val pixelsNotVisibleOnRightSide = (scrollPaneBounds.right - visibleScrollPaneArea.right).coerceAtLeast(0f)
val textFieldDistanceFromRightSide = scrollContent.width - (textFieldScrollContentCoords.x + this.width)
val pixelsNotVisibleOnTop = (scrollPaneBounds.top - visibleScrollPaneArea.top).coerceAtLeast(0f)
val textFieldDistanceFromTop = scrollContent.height - (textFieldScrollContentCoords.y + this.height)
val pixelsNotVisibleOnBottom = (visibleScrollPaneArea.y - scrollPaneBounds.y).coerceAtLeast(0f)
val textFieldDistanceFromBottom = textFieldScrollContentCoords.y
// If the visible scroll pane area is smaller than our text field, it will always be partly obscured
if (visibleScrollPaneArea.width < this.width || visibleScrollPaneArea.height < this.height
// If the amount of pixels obscured near a scrollContent edge is larger than the distance of the text field to that edge, it will always be (partly) obscured
|| pixelsNotVisibleOnLeftSide > textFieldDistanceFromLeftSide
|| pixelsNotVisibleOnRightSide > textFieldDistanceFromRightSide
|| pixelsNotVisibleOnTop > textFieldDistanceFromTop
|| pixelsNotVisibleOnBottom > textFieldDistanceFromBottom) {
return false
}
// We want to put the text field in the middle of the visible area
val scrollXMiddle = textFieldScrollContentCoords.x - this.width / 2 + visibleScrollPaneArea.width / 2
// If the visible area is to the right of the left edge of the scroll pane, we need to scroll that much farther to get to the real visible middle
scrollPane.scrollX = pixelsNotVisibleOnLeftSide + scrollXMiddle
// ScrollPane.scrollY has the origin at the top instead of at the bottom, so + for height / 2 instead of -
// We want to put the text field in the middle of the visible area
val scrollYMiddleGdxOrigin = textFieldScrollContentCoords.y + this.height / 2 + visibleScrollPaneArea.height / 2
// If the visible area is below the top edge of the scroll pane, we need to scroll that much farther to get to the real visible middle
// Also, convert to scroll pane origin (0 is on top instead of bottom)
scrollPane.scrollY = pixelsNotVisibleOnTop + scrollContent.height - scrollYMiddleGdxOrigin
return true
}
object TextfieldImprovements {
private val hideKeyboard = { Gdx.input.setOnscreenKeyboardVisible(false) }
fun add(textField: TextField): TextField {
textField.addListener(object : InputListener() {
private val events = EventBus.EventReceiver()
init {
events.receive(UncivStage.VisibleAreaChanged::class) {
if (textField.stage == null || !textField.hasKeyboardFocus()) return@receive
Concurrency.run {
// If anything resizes, it also does so with this event. So we need to wait for that to finish to update the scroll position.
delay(100)
withGLContext {
if (textField.stage == null) return@withGLContext
if (textField.scrollAscendantToTextField()) {
val scrollPane = textField.getAscendant { it is ScrollPane } as ScrollPane?
// when screen dimensions change, we don't want an animation for scrolling, just show, just show the textfield immediately
scrollPane?.updateVisualScroll()
} else {
// We can't scroll the text field into view, so we need to show a popup
TextfieldPopup(textField).open()
}
}
}
}
}
override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int): Boolean {
addPopupCloseListener(textField)
return false
}
})
textField.addListener(object : FocusListener() {
override fun keyboardFocusChanged(event: FocusEvent?, actor: Actor?, focused: Boolean) {
if (focused) {
addPopupCloseListener(textField)
Gdx.input.setOnscreenKeyboardVisible(true)
}
}
})
return textField
}
private fun addPopupCloseListener(textField: TextField) {
val popup = textField.getAscendant { it is Popup } as Popup?
if (popup != null && !popup.closeListeners.contains(hideKeyboard)) {
popup.closeListeners.add(hideKeyboard)
}
}
}
class TextfieldPopup(
textField: TextField
) : Popup(textField.stage) {
val popupTextfield = clone(textField)
init {
addGoodSizedLabel(popupTextfield.messageText)
.colspan(2)
.row()
add(popupTextfield)
.width(stageToShowOn.width / 2)
.colspan(2)
.row()
addCloseButton(Constants.cancel)
.left()
addOKButton { textField.text = popupTextfield.text }
.right()
.row()
showListeners.add {
stageToShowOn.keyboardFocus = popupTextfield
}
closeListeners.add {
stageToShowOn.keyboardFocus = null
Gdx.input.setOnscreenKeyboardVisible(false)
}
}
private fun clone(textField: TextField): TextField {
@Suppress("UNCIV_RAW_TEXTFIELD") // we are copying the existing text field
val copy = TextField(textField.text, textField.style)
copy.textFieldFilter = textField.textFieldFilter
copy.messageText = textField.messageText
copy.setSelection(textField.selectionStart, textField.selection.length)
copy.cursorPosition = textField.cursorPosition
copy.alignment = textField.alignment
copy.isPasswordMode = textField.isPasswordMode
copy.onscreenKeyboard = textField.onscreenKeyboard
return copy
}
}

View File

@ -17,7 +17,6 @@ import com.badlogic.gdx.scenes.scene2d.utils.Layout
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.darken
@ -593,7 +592,7 @@ open class TabbedPager(
*/
fun askForPassword(secretHashCode: Int = 0) {
class PassPopup(screen: BaseScreen, unlockAction: ()->Unit, lockAction: ()->Unit) : Popup(screen) {
val passEntry = UncivTextField.create("Password")
val passEntry = UncivTextField("Password")
init {
passEntry.isPasswordMode = true
add(passEntry).row()

View File

@ -0,0 +1,59 @@
package com.unciv.ui.components.widgets
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.keyShortcuts
/**
* Creates a text field with two deviations from the default Gdx [TextField]:
* - Turns off Gdx color markup support while drawing, so [] in the text display properly
* - If this TextField handles the Tab key (see [keyShortcuts]), its [focus navigation feature][TextField.next] is disabled (otherwise it would search for another TextField in the same parent to give focus to, and remove the on-screen keyboard if it finds none).
* - constructors same as standard TextField, plus a cloning constructor.
*/
open class TextFieldWithFixes private constructor(text: String, style: TextFieldStyle) : TextField(text, style) {
internal constructor(text: String, skin: Skin) : this(text, skin.get(TextFieldStyle::class.java))
internal constructor(textField: TextFieldWithFixes) : this(textField.text, textField.style) {
textFieldFilter = textField.textFieldFilter
messageText = textField.messageText
hasSelection = textField.hasSelection
cursor = textField.cursor
selectionStart = textField.selectionStart
alignment = textField.alignment
isPasswordMode = textField.isPasswordMode
onscreenKeyboard = textField.onscreenKeyboard
}
// Without this, the DeveloperConsole can't catch the Tab key for Autocomplete reliably
override fun next(up: Boolean) {
if (KeyCharAndCode.TAB in keyShortcuts) return
super.next(up)
}
// Note - this way to force TextField to display `[]` characters normally is an incomplete hack.
// The complete way would either require overriding `updateDisplayText` which is private, or all methods calling it,
// which are many including the keyboard listener, or keep a separate font without markup enabled around and put that
// into the default style, including its own NativeBitmapFontData instance and texture - involves quite some redesign.
// That said, observing the deficiency is hard - the internal `glyphPositions` could theoretically get out of sync, affecting selection and caret display.
override fun layout() {
val oldEnable = style.font.data.markupEnabled
style.font.data.markupEnabled = false
super.layout()
style.font.data.markupEnabled = oldEnable
}
override fun drawText(batch: Batch, font: BitmapFont, x: Float, y: Float) {
val oldEnable = font.data.markupEnabled
font.data.markupEnabled = false
super.drawText(batch, font, x, y)
font.data.markupEnabled = oldEnable
}
fun copyTextAndSelection(textField: TextFieldWithFixes) {
text = textField.text
hasSelection = textField.hasSelection
cursor = textField.cursor
selectionStart = textField.selectionStart
}
}

View File

@ -0,0 +1,192 @@
package com.unciv.ui.components.widgets
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.scenes.scene2d.utils.FocusListener
import com.unciv.Constants
import com.unciv.logic.event.EventBus
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.getAscendant
import com.unciv.ui.components.extensions.getOverlap
import com.unciv.ui.components.extensions.right
import com.unciv.ui.components.extensions.stageBoundingBox
import com.unciv.ui.components.extensions.top
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.utils.Concurrency
import com.unciv.utils.withGLContext
import kotlinx.coroutines.delay
/**
* Creates a text field that has nicer platform-specific input added compared to the default Gdx [TextField].
*
* - On Android, manages on-screen keyboard visibility and reacts to how the on-screen keyboard reduces screen space.
* - Tries to scroll the field into view when receiving focus using an ascendant ScrollPane.
* - If view of the field is still obscured, show am "emergency" Popup instead (can help when on Android the on-screen keyboard is large and the screen small).
* - If this TextField handles the Tab key (see [keyShortcuts]), its [focus navigation feature][TextField.next] is disabled (otherwise it would search for another TextField in the same parent to give focus to, and remove the on-screen keyboard if it finds none).
* - All parameters are supported on all platforms.
*
* @param hint The text that should be displayed in the text field when no text is entered and it does not have focus, will automatically be translated. Also shown as Label in the "emergency" Popup.
* @param preEnteredText the text initially entered within this text field.
* @param onFocusChange a callback that will be notified when this TextField loses or gains the [keyboardFocus][com.badlogic.gdx.scenes.scene2d.Stage.keyboardFocus].
* Receiver is the field, so you can simply use its elements. Parameter `it` is a Boolean indicating focus was received.
*/
class UncivTextField(
hint: String,
preEnteredText: String = "",
private val onFocusChange: (TextField.(Boolean) -> Unit)? = null
) : TextFieldWithFixes(preEnteredText, BaseScreen.skin) {
private val isAndroid = Gdx.app.type == Application.ApplicationType.Android
private val hideKeyboard = { Gdx.input.setOnscreenKeyboardVisible(false) }
init {
messageText = hint.tr()
addListener(UncivTextFieldFocusListener())
if (isAndroid) addListener(VisibleAreaChangedListener())
}
private inner class UncivTextFieldFocusListener : FocusListener() {
override fun keyboardFocusChanged(event: FocusEvent, actor: Actor, focused: Boolean) {
if (focused) {
scrollAscendantToTextField()
if (isAndroid) {
addPopupCloseListener()
Gdx.input.setOnscreenKeyboardVisible(true)
}
}
onFocusChange?.invoke(this@UncivTextField, focused)
}
}
private inner class VisibleAreaChangedListener : InputListener() {
private val events = EventBus.EventReceiver()
init {
events.receive(UncivStage.VisibleAreaChanged::class) {
if (stage == null || !hasKeyboardFocus()) return@receive
Concurrency.run {
// If anything resizes, it also does so with this event. So we need to wait for that to finish to update the scroll position.
delay(100)
withGLContext {
if (stage == null) return@withGLContext
if (scrollAscendantToTextField()) {
val scrollPane = getAscendant<ScrollPane>()
// when screen dimensions change, we don't want an animation for scrolling, just show the textfield immediately
scrollPane?.updateVisualScroll()
} else {
// We can't scroll the text field into view, so we need to show a popup
TextfieldPopup().open()
}
}
}
}
}
override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int): Boolean {
addPopupCloseListener()
return false
}
}
private fun addPopupCloseListener() {
val popup = getAscendant<Popup>()
if (popup != null && !popup.closeListeners.contains(hideKeyboard)) {
popup.closeListeners.add(hideKeyboard)
}
}
/**
* Tries to scroll a [ScrollPane] ascendant of the text field so that this text field is in the middle of the visible area.
*
* @return true if the text field is visible after this operation
*/
private fun scrollAscendantToTextField(): Boolean {
val stage = this.stage as? UncivStage ?: return false
val scrollPane = this.getAscendant<ScrollPane>()
val visibleArea = stage.lastKnownVisibleArea
val textFieldStageBoundingBox = this.stageBoundingBox
if (scrollPane == null) return visibleArea.contains(textFieldStageBoundingBox)
val scrollPaneBounds = scrollPane.stageBoundingBox
val visibleScrollPaneArea = scrollPaneBounds.getOverlap(visibleArea)
if (visibleScrollPaneArea == null) {
return false
} else if (visibleScrollPaneArea.contains(textFieldStageBoundingBox)) {
return true
}
val scrollContent = scrollPane.actor
val textFieldScrollContentCoords = localToAscendantCoordinates(scrollContent, Vector2(0f, 0f))
// It's possible that our textField can't be (fully) scrolled to be within the visible scrollPane area
val pixelsNotVisibleOnLeftSide = (visibleScrollPaneArea.x - scrollPaneBounds.x).coerceAtLeast(0f)
val textFieldDistanceFromLeftSide = textFieldScrollContentCoords.x
val pixelsNotVisibleOnRightSide = (scrollPaneBounds.right - visibleScrollPaneArea.right).coerceAtLeast(0f)
val textFieldDistanceFromRightSide = scrollContent.width - (textFieldScrollContentCoords.x + this.width)
val pixelsNotVisibleOnTop = (scrollPaneBounds.top - visibleScrollPaneArea.top).coerceAtLeast(0f)
val textFieldDistanceFromTop = scrollContent.height - (textFieldScrollContentCoords.y + this.height)
val pixelsNotVisibleOnBottom = (visibleScrollPaneArea.y - scrollPaneBounds.y).coerceAtLeast(0f)
val textFieldDistanceFromBottom = textFieldScrollContentCoords.y
// If the visible scroll pane area is smaller than our text field, it will always be partly obscured
if (visibleScrollPaneArea.width < this.width || visibleScrollPaneArea.height < this.height
// If the amount of pixels obscured near a scrollContent edge is larger than the distance of the text field to that edge, it will always be (partly) obscured
|| pixelsNotVisibleOnLeftSide > textFieldDistanceFromLeftSide
|| pixelsNotVisibleOnRightSide > textFieldDistanceFromRightSide
|| pixelsNotVisibleOnTop > textFieldDistanceFromTop
|| pixelsNotVisibleOnBottom > textFieldDistanceFromBottom) {
return false
}
// We want to put the text field in the middle of the visible area
val scrollXMiddle = textFieldScrollContentCoords.x - this.width / 2 + visibleScrollPaneArea.width / 2
// If the visible area is to the right of the left edge of the scroll pane, we need to scroll that much farther to get to the real visible middle
scrollPane.scrollX = pixelsNotVisibleOnLeftSide + scrollXMiddle
// ScrollPane.scrollY has the origin at the top instead of at the bottom, so + for height / 2 instead of -
// We want to put the text field in the middle of the visible area
val scrollYMiddleGdxOrigin = textFieldScrollContentCoords.y + this.height / 2 + visibleScrollPaneArea.height / 2
// If the visible area is below the top edge of the scroll pane, we need to scroll that much farther to get to the real visible middle
// Also, convert to scroll pane origin (0 is on top instead of bottom)
scrollPane.scrollY = pixelsNotVisibleOnTop + scrollContent.height - scrollYMiddleGdxOrigin
return true
}
private inner class TextfieldPopup : Popup(stage) {
val popupTextfield = TextFieldWithFixes(this@UncivTextField)
init {
addGoodSizedLabel(popupTextfield.messageText)
.colspan(2)
.row()
add(popupTextfield)
.width(stageToShowOn.width / 2)
.colspan(2)
.row()
addCloseButton(Constants.cancel)
.left()
addOKButton { this@UncivTextField.copyTextAndSelection(popupTextfield) }
.right()
.row()
showListeners.add {
stageToShowOn.keyboardFocus = popupTextfield
}
closeListeners.add {
stageToShowOn.keyboardFocus = null
Gdx.input.setOnscreenKeyboardVisible(false)
}
}
}
}

View File

@ -4,7 +4,7 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.input.onChange
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.surroundWithCircle
@ -62,7 +62,7 @@ class AskNumberPopup(
wrapper.add(label.toLabel())
add(wrapper).colspan(2).row()
val nameField = UncivTextField.create(label, defaultValue)
val nameField = UncivTextField(label, defaultValue)
nameField.textFieldFilter = TextField.TextFieldFilter { _, char -> char.isDigit() || char == '-' }
fun isValidInt(input: String): Boolean {

View File

@ -6,7 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel
@ -40,7 +40,7 @@ class AskTextPopup(
wrapper.add(label.toLabel())
add(wrapper).colspan(2).row()
val nameField = UncivTextField.create(label, defaultText)
val nameField = UncivTextField(label, defaultText)
nameField.textFieldFilter = TextField.TextFieldFilter { _, char -> char !in illegalChars}
nameField.maxLength = maxLength

View File

@ -3,7 +3,7 @@ package com.unciv.ui.popups
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.UncivGame
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.screens.basescreen.BaseScreen
@ -14,7 +14,7 @@ class AuthPopup(stage: Stage, authSuccessful: ((Boolean) -> Unit)? = null)
constructor(screen: BaseScreen, authSuccessful: ((Boolean) -> Unit)? = null) : this(screen.stage, authSuccessful)
init {
val passwordField = UncivTextField.create("Password")
val passwordField = UncivTextField("Password")
val button = "Authenticate".toTextButton()
val negativeButtonStyle = BaseScreen.skin.get("negative", TextButton.TextButtonStyle::class.java)

View File

@ -10,7 +10,7 @@ import com.unciv.logic.files.MapSaver
import com.unciv.logic.files.UncivFiles
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
@ -31,7 +31,7 @@ fun debugTab(
if (GUI.isWorldLoaded()) {
val simulateButton = "Simulate until turn:".toTextButton()
val simulateTextField = UncivTextField.create("Turn", DebugUtils.SIMULATE_UNTIL_TURN.toString())
val simulateTextField = UncivTextField("Turn", DebugUtils.SIMULATE_UNTIL_TURN.toString())
val invalidInputLabel = "This is not a valid integer!".toLabel().also { it.isVisible = false }
simulateButton.onClick {
val simulateUntilTurns = simulateTextField.text.toIntOrNull()

View File

@ -9,11 +9,9 @@ import com.unciv.logic.files.IMediaFinder
import com.unciv.logic.multiplayer.OnlineMultiplayer
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
import com.unciv.models.UncivSound
import com.unciv.models.metadata.GameSettings
import com.unciv.models.metadata.GameSettings.GameSetting
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.brighten
import com.unciv.ui.components.extensions.format
@ -113,7 +111,7 @@ private fun addMultiplayerServerOptions(
} else {
"https://"
}
val multiplayerServerTextField = UncivTextField.create("Server address", textToShowForMultiplayerAddress)
val multiplayerServerTextField = UncivTextField("Server address", textToShowForMultiplayerAddress)
multiplayerServerTextField.setTextFieldFilter { _, c -> c !in " \r\n\t\\" }
multiplayerServerTextField.programmaticChangeEvents = true
val serverIpTable = Table()
@ -160,7 +158,7 @@ private fun addMultiplayerServerOptions(
}).row()
if (UncivGame.Current.onlineMultiplayer.multiplayerServer.featureSet.authVersion > 0) {
val passwordTextField = UncivTextField.create(
val passwordTextField = UncivTextField(
settings.multiplayer.passwords[settings.multiplayer.server] ?: "Password"
)
val setPasswordButton = "Set password".toTextButton()

View File

@ -10,7 +10,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.stats.INamed
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.toLabel
@ -34,7 +34,7 @@ class CivilopediaSearchPopup(
private val linkAction: (String) -> Unit
) : Popup(pediaScreen) {
private var ruleset = pediaScreen.ruleset
private val searchText = UncivTextField.create("") // Always focused, "hint" never seen
private val searchText = UncivTextField("") // Always focused, "hint" never seen
private val modSelect = ModSelectBox()
private lateinit var resultExpander: ExpanderTab
private val resultCell: Cell<Actor?>

View File

@ -11,7 +11,7 @@ import com.unciv.Constants
import com.unciv.GUI
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
@ -34,7 +34,7 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
private var currentHistoryEntry = history.size
private val textField = UncivTextField.create("", "") // always has focus, so a hint won't show
private val textField = UncivTextField("") // always has focus, so a hint won't show
private val responseLabel = "".toLabel(Color.RED).apply { wrap = true }
private val inputWrapper = Table()

View File

@ -21,7 +21,7 @@ import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.KeyboardPanningListener
@ -85,7 +85,7 @@ class MapEditorScreen(map: TileMap? = null) : BaseScreen(), RecreateOnResize {
val tabs: MapEditorMainTabs
var tileClickHandler: ((tile: Tile)->Unit)? = null
private var zoomController: ZoomButtonPair? = null
val descriptionTextField = UncivTextField.create("Enter a description for the users of this map")
val descriptionTextField = UncivTextField("Enter a description for the users of this map")
private val highlightedTileGroups = mutableListOf<TileGroup>()

View File

@ -9,7 +9,7 @@ import com.unciv.logic.files.MapSaver
import com.unciv.logic.map.MapGeneratedMainType
import com.unciv.logic.map.TileMap
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.KeyCharAndCode
@ -45,7 +45,7 @@ class MapEditorSaveTab(
private val deleteButton = "Delete map".toTextButton()
private val quitButton = "Exit map editor".toTextButton()
private val mapNameTextField = UncivTextField.create("Map Name")
private val mapNameTextField = UncivTextField("Map Name")
private var chosenMap: FileHandle? = null

View File

@ -7,7 +7,7 @@ import com.unciv.Constants
import com.unciv.models.metadata.ModCategories
import com.unciv.models.translations.tr
import com.unciv.ui.components.widgets.ExpanderTab
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel
@ -78,7 +78,7 @@ internal class ModManagementOptions(private val modManagementScreen: ModManageme
return Filter(textField.text, category.topic)
}
private val textField = UncivTextField.create("Enter search text")
private val textField = UncivTextField("Enter search text")
var category = ModCategories.default()

View File

@ -12,8 +12,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.badlogic.gdx.utils.SerializationException
import com.unciv.UncivGame
import com.unciv.json.fromJsonFile
import com.unciv.json.json
import com.unciv.logic.UncivShowableException
import com.unciv.logic.github.Github
import com.unciv.logic.github.Github.repoNameToFolderName
@ -22,7 +20,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.enable
@ -409,7 +407,7 @@ class ModManagementScreen private constructor(
downloadButton.onClick {
val popup = Popup(this)
popup.addGoodSizedLabel("Please enter the mod repository -or- archive zip -or- branch -or- release url:").row()
val textField = UncivTextField.create("").apply { maxLength = 666 }
val textField = UncivTextField("").apply { maxLength = 666 }
popup.add(textField).width(stage.width / 2).row()
val pasteLinkButton = "Paste from clipboard".toTextButton()
pasteLinkButton.onClick {

View File

@ -8,7 +8,7 @@ import com.unciv.logic.multiplayer.FriendList
import com.unciv.models.translations.tr
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel
@ -17,9 +17,9 @@ import java.util.UUID
class AddFriendScreen : PickerScreen() {
init {
val friendNameTextField = UncivTextField.create("Please input a name for your friend!")
val friendNameTextField = UncivTextField("Please input a name for your friend!")
val pastePlayerIDButton = "Paste player ID from clipboard".toTextButton()
val playerIDTextField = UncivTextField.create("Please input a player ID for your friend!")
val playerIDTextField = UncivTextField("Please input a player ID for your friend!")
val friendlist = FriendList()
topTable.add("Friend name".toLabel()).row()

View File

@ -5,7 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants
import com.unciv.logic.IdChecker
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
@ -23,8 +23,8 @@ import java.util.UUID
class AddMultiplayerGameScreen(multiplayerScreen: MultiplayerScreen) : PickerScreen() {
init {
val gameNameTextField = UncivTextField.create("Game name")
val gameIDTextField = UncivTextField.create("GameID")
val gameNameTextField = UncivTextField("Game name")
val gameIDTextField = UncivTextField("GameID")
val pasteGameIDButton = "Paste gameID from clipboard".toTextButton()
pasteGameIDButton.onClick {
gameIDTextField.text = Gdx.app.clipboard.contents

View File

@ -10,7 +10,7 @@ import com.unciv.models.translations.tr
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.popups.ConfirmPopup
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel
@ -19,9 +19,9 @@ import java.util.UUID
class EditFriendScreen(selectedFriend: FriendList.Friend) : PickerScreen() {
init {
val friendNameTextField = UncivTextField.create("Please input a name for your friend!", selectedFriend.name)
val friendNameTextField = UncivTextField("Please input a name for your friend!", selectedFriend.name)
val pastePlayerIDButton = "Player ID from clipboard".toTextButton()
val playerIDTextField = UncivTextField.create("Please input a player ID for your friend!", selectedFriend.playerID)
val playerIDTextField = UncivTextField("Please input a player ID for your friend!", selectedFriend.playerID)
val deleteFriendButton = "Delete".toTextButton()
val friendlist = FriendList()

View File

@ -7,7 +7,7 @@ import com.unciv.Constants
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.toTextButton
@ -190,7 +190,7 @@ class MultiplayerScreen : PickerScreen() {
val btn = "Rename".toTextButton().apply { disable() }
btn.onClick {
Popup(this).apply {
val textField = UncivTextField.create("Game name", selectedGame!!.name)
val textField = UncivTextField("Game name", selectedGame!!.name)
add(textField).width(stageToShowOn.width / 2).row()
val saveButton = "Save".toTextButton()

View File

@ -13,7 +13,7 @@ import com.unciv.logic.map.mapgenerator.MapResourceSetting
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize
import com.unciv.logic.map.MapType
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
@ -205,7 +205,7 @@ class MapParametersTable(
private fun addHexagonalSizeTable() {
val defaultRadius = mapParameters.mapSize.radius.toString()
customMapSizeRadius = UncivTextField.create("Radius", defaultRadius).apply {
customMapSizeRadius = UncivTextField("Radius", defaultRadius).apply {
textFieldFilter = DigitsOnlyFilter()
}
customMapSizeRadius.onChange {
@ -219,12 +219,12 @@ class MapParametersTable(
private fun addRectangularSizeTable() {
val defaultWidth = mapParameters.mapSize.width.toString()
customMapWidth = UncivTextField.create("Width", defaultWidth).apply {
customMapWidth = UncivTextField("Width", defaultWidth).apply {
textFieldFilter = DigitsOnlyFilter()
}
val defaultHeight = mapParameters.mapSize.height.toString()
customMapHeight = UncivTextField.create("Height", defaultHeight).apply {
customMapHeight = UncivTextField("Height", defaultHeight).apply {
textFieldFilter = DigitsOnlyFilter()
}
@ -360,7 +360,7 @@ class MapParametersTable(
private fun addAdvancedControls(table: Table) {
table.defaults().pad(5f)
seedTextField = UncivTextField.create("RNG Seed", mapParameters.seed.toString())
seedTextField = UncivTextField("RNG Seed", mapParameters.seed.toString())
seedTextField.textFieldFilter = DigitsOnlyFilter()
// If the field is empty, fallback seed value to 0

View File

@ -17,7 +17,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.translations.tr
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.widgets.WrappableLabel
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.isEnabled
@ -229,7 +229,7 @@ class PlayerPickerTable(
private fun Table.addPlayerTableMultiplayerControls(player: Player) {
row()
val playerIdTextField = UncivTextField.create("Please input Player ID!", player.playerId)
val playerIdTextField = UncivTextField("Please input Player ID!", player.playerId)
add(playerIdTextField).colspan(2).fillX().pad(5f)
val errorLabel = "".toLabel(Color.RED)
add(errorLabel).pad(5f).row()

View File

@ -9,7 +9,7 @@ import com.unciv.logic.GameInfo
import com.unciv.logic.files.PlatformSaverLoader
import com.unciv.logic.files.UncivFiles
import com.unciv.models.translations.tr
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.enable
@ -32,7 +32,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
const val saveToCustomText = "Save to custom location"
}
private val gameNameTextField = UncivTextField.create(nameFieldLabelText)
private val gameNameTextField = UncivTextField(nameFieldLabelText)
init {
setDefaultCloseAction()