mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 17:28:57 +07:00
Make popups and text fields nicer to interact with on Android (#7211)
* Make popups and text fields nicer to interact with on Android * Refactor: Rename createTextField to UncivTextField.create * Fix multiplayer save delete text
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@ -24,7 +24,10 @@ www-test/
|
||||
/android/libs/x86/
|
||||
/android/libs/x86_64/
|
||||
/android/gen/
|
||||
.idea/
|
||||
.idea/*
|
||||
!/.idea/inspectionProfiles
|
||||
/.idea/inspectionProfiles/*
|
||||
!/.idea/inspectionProfiles/Project_Default.xml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
@ -151,3 +154,4 @@ android/assets/music/
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
/.github/workflows/node_modules/*
|
||||
|
||||
|
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="000ce796-10c4-3e92-bd42-fa6f0bd6ec8b" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="SSBasedInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<searchConfiguration name="Raw TextField usage" description="We want to add some extra behavior to all text fields. This behavior will not be added by instantiating a TextField directly, so use our general factory function UncivTextField.create instead." suppressId="UNCIV_RAW_TEXTFIELD" problemDescriptor="Use UncivTextField.createTextField instead!" text="TextField($Params$)" recursive="true" caseInsensitive="true" type="Kotlin" pattern_context="default">
|
||||
<constraint name="__context__" within="" contains="" />
|
||||
<constraint name="Params" minCount="0" maxCount="2147483647" within="" contains="" />
|
||||
</searchConfiguration>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
@ -1,19 +1,27 @@
|
||||
package com.unciv.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.WorkManager
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.backends.android.AndroidApplication
|
||||
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
|
||||
import com.badlogic.gdx.backends.android.AndroidGraphics
|
||||
import com.badlogic.gdx.math.Rectangle
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.UncivGameParameters
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import java.io.File
|
||||
|
||||
open class AndroidLauncher : AndroidApplication() {
|
||||
@ -53,6 +61,41 @@ open class AndroidLauncher : AndroidApplication() {
|
||||
initialize(game, config)
|
||||
|
||||
setDeepLinkedGame(intent)
|
||||
|
||||
addScreenObscuredListener((Gdx.graphics as AndroidGraphics).view)
|
||||
}
|
||||
|
||||
private fun addScreenObscuredListener(contentView: View) {
|
||||
contentView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
/** [onGlobalLayout] gets triggered not only when the [windowVisibleDisplayFrame][View.getWindowVisibleDisplayFrame] changes, but also on other things.
|
||||
* So we need to check if that was actually the thing that changed. */
|
||||
private var lastVisibleDisplayFrame: Rect? = null
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
if (!UncivGame.isCurrentInitialized() || UncivGame.Current.screen == null) {
|
||||
return
|
||||
}
|
||||
val r = Rect()
|
||||
contentView.getWindowVisibleDisplayFrame(r)
|
||||
if (r.equals(lastVisibleDisplayFrame)) return
|
||||
lastVisibleDisplayFrame = r
|
||||
|
||||
val stage = (UncivGame.Current.screen as BaseScreen).stage
|
||||
|
||||
val horizontalRatio = stage.width / contentView.width
|
||||
val verticalRatio = stage.height / contentView.height
|
||||
|
||||
val visibleStage = Rectangle(
|
||||
r.left * horizontalRatio,
|
||||
(contentView.height - r.bottom) * verticalRatio, // Android coordinate system has the origin in the top left, while GDX uses bottom left
|
||||
r.width() * horizontalRatio,
|
||||
r.height() * verticalRatio
|
||||
)
|
||||
Concurrency.runOnGLThread {
|
||||
EventBus.send(UncivStage.VisibleAreaChanged(visibleStage))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,7 +5,7 @@ import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.ui.utils.GeneralPlatformSpecificHelpers
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@ -66,4 +66,8 @@ Sources for Info about current orientation in case need:
|
||||
thread { throw ex } // this will kill the app but report the exception to the Google Play Console if the user allows it
|
||||
return true
|
||||
}
|
||||
|
||||
override fun addImprovements(textField: TextField): TextField {
|
||||
return TextfieldImprovements.add(textField)
|
||||
}
|
||||
}
|
||||
|
113
android/src/com/unciv/app/TextfieldImprovements.kt
Normal file
113
android/src/com/unciv/app/TextfieldImprovements.kt
Normal file
@ -0,0 +1,113 @@
|
||||
package com.unciv.app
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
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.logic.event.EventBus
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.extensions.getAscendant
|
||||
import com.unciv.ui.utils.scrollAscendantToTextField
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.withGLContext
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
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("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
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package com.unciv.ui
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.g2d.Batch
|
||||
import com.badlogic.gdx.math.Rectangle
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.utils.viewport.Viewport
|
||||
import com.unciv.logic.event.Event
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.ui.crashhandling.wrapCrashHandling
|
||||
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit
|
||||
import com.unciv.utils.Log
|
||||
|
||||
|
||||
/** Main stage for the game. Catches all exceptions or errors thrown by event handlers, calling [com.unciv.UncivGame.handleUncaughtThrowable] with the thrown exception or error. */
|
||||
@ -17,6 +20,23 @@ class UncivStage(viewport: Viewport) : Stage(viewport) {
|
||||
*/
|
||||
var performPointerEnterExitEvents: Boolean = true
|
||||
|
||||
var lastKnownVisibleArea: Rectangle
|
||||
private set
|
||||
|
||||
private val events = EventBus.EventReceiver()
|
||||
init {
|
||||
lastKnownVisibleArea = Rectangle(0f, 0f, width, height)
|
||||
events.receive(VisibleAreaChanged::class) {
|
||||
Log.debug("Visible stage area changed: %s", it.visibleArea)
|
||||
lastKnownVisibleArea = it.visibleArea
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
events.stopReceiving()
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
override fun draw() =
|
||||
{ super.draw() }.wrapCrashHandlingUnit()()
|
||||
|
||||
@ -60,4 +80,7 @@ class UncivStage(viewport: Viewport) : Stage(viewport) {
|
||||
override fun keyTyped(character: Char) =
|
||||
{ super.keyTyped(character) }.wrapCrashHandling()() ?: true
|
||||
|
||||
class VisibleAreaChanged(
|
||||
val visibleArea: Rectangle
|
||||
) : Event
|
||||
}
|
||||
|
@ -9,13 +9,14 @@ import com.unciv.logic.MapSaver
|
||||
import com.unciv.logic.map.MapType
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.utils.AutoScrollPane
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.TabbedPager
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.isEnabled
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.onActivation
|
||||
@ -37,7 +38,7 @@ class MapEditorSaveTab(
|
||||
private val deleteButton = "Delete map".toTextButton()
|
||||
private val quitButton = "Exit map editor".toTextButton()
|
||||
|
||||
private val mapNameTextField = TextField("", skin)
|
||||
private val mapNameTextField = UncivTextField.create("Map Name")
|
||||
|
||||
private var chosenMap: FileHandle? = null
|
||||
|
||||
|
@ -2,13 +2,13 @@ package com.unciv.ui.multiplayer
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.IdChecker
|
||||
import com.unciv.logic.multiplayer.FriendList
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
@ -17,13 +17,12 @@ import java.util.*
|
||||
|
||||
class AddFriendScreen : PickerScreen() {
|
||||
init {
|
||||
val friendNameTextField = TextField("", skin)
|
||||
val friendNameTextField = UncivTextField.create("Please input a name for your friend!")
|
||||
val pastePlayerIDButton = "Paste player ID from clipboard".toTextButton()
|
||||
val playerIDTextField = TextField("", skin)
|
||||
val playerIDTextField = UncivTextField.create("Please input a player ID for your friend!")
|
||||
val friendlist = FriendList()
|
||||
|
||||
topTable.add("Friend name".toLabel()).row()
|
||||
friendNameTextField.messageText = "Please input a name for your friend!".tr()
|
||||
topTable.add(friendNameTextField).pad(10f).padBottom(30f).width(stage.width/2).row()
|
||||
|
||||
pastePlayerIDButton.onClick {
|
||||
@ -32,7 +31,6 @@ class AddFriendScreen : PickerScreen() {
|
||||
|
||||
topTable.add("Player ID".toLabel()).row()
|
||||
val gameIDTable = Table()
|
||||
playerIDTextField.messageText = "Please input a player ID for your friend!".tr()
|
||||
gameIDTable.add(playerIDTextField).pad(10f).width(2*stage.width/3 - pastePlayerIDButton.width)
|
||||
gameIDTable.add(pastePlayerIDButton)
|
||||
topTable.add(gameIDTable).padBottom(30f).row()
|
||||
|
@ -2,12 +2,12 @@ package com.unciv.ui.multiplayer
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.logic.IdChecker
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
@ -18,8 +18,8 @@ import java.util.*
|
||||
|
||||
class AddMultiplayerGameScreen : PickerScreen() {
|
||||
init {
|
||||
val gameNameTextField = TextField("", skin)
|
||||
val gameIDTextField = TextField("", skin)
|
||||
val gameNameTextField = UncivTextField.create("Game name")
|
||||
val gameIDTextField = UncivTextField.create("GameID")
|
||||
val pasteGameIDButton = "Paste gameID from clipboard".toTextButton()
|
||||
pasteGameIDButton.onClick {
|
||||
gameIDTextField.text = Gdx.app.clipboard.contents
|
||||
|
@ -3,7 +3,6 @@ package com.unciv.ui.multiplayer
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.IdChecker
|
||||
import com.unciv.logic.multiplayer.FriendList
|
||||
@ -11,6 +10,7 @@ import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
@ -19,14 +19,13 @@ import java.util.*
|
||||
|
||||
class EditFriendScreen(selectedFriend: FriendList.Friend) : PickerScreen() {
|
||||
init {
|
||||
val friendNameTextField = TextField(selectedFriend.name, skin)
|
||||
val friendNameTextField = UncivTextField.create("Please input a name for your friend!", selectedFriend.name)
|
||||
val pastePlayerIDButton = "Player ID from clipboard".toTextButton()
|
||||
val playerIDTextField = TextField(selectedFriend.playerID, skin)
|
||||
val playerIDTextField = UncivTextField.create("Please input a player ID for your friend!", selectedFriend.playerID)
|
||||
val deleteFriendButton = "Delete".toTextButton()
|
||||
val friendlist = FriendList()
|
||||
|
||||
topTable.add("Friend name".toLabel()).row()
|
||||
friendNameTextField.messageText = "Please input a name for your friend!".tr()
|
||||
topTable.add(friendNameTextField).pad(10f).padBottom(30f).width(stage.width/2).row()
|
||||
|
||||
pastePlayerIDButton.onClick {
|
||||
@ -35,7 +34,6 @@ class EditFriendScreen(selectedFriend: FriendList.Friend) : PickerScreen() {
|
||||
|
||||
topTable.add("Player ID".toLabel()).row()
|
||||
val gameIDTable = Table()
|
||||
playerIDTextField.messageText = "Please input a player ID for your friend!".tr()
|
||||
gameIDTable.add(playerIDTextField).pad(10f).width(2*stage.width/3 - pastePlayerIDButton.width)
|
||||
gameIDTable.add(pastePlayerIDButton)
|
||||
topTable.add(gameIDTable).padBottom(30f).row()
|
||||
|
@ -2,13 +2,13 @@ package com.unciv.ui.multiplayer
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
@ -21,7 +21,7 @@ import com.unciv.utils.concurrency.launchOnGLThread
|
||||
* backScreen is used for getting back to the MultiplayerScreen so it doesn't have to be created over and over again */
|
||||
class EditMultiplayerGameInfoScreen(val multiplayerGame: OnlineMultiplayerGame) : PickerScreen() {
|
||||
init {
|
||||
val textField = TextField(multiplayerGame.name, skin)
|
||||
val textField = UncivTextField.create("Game name", multiplayerGame.name)
|
||||
|
||||
topTable.add("Rename".toLabel()).row()
|
||||
topTable.add(textField).pad(10f).padBottom(30f).width(stage.width / 2).row()
|
||||
@ -31,8 +31,8 @@ class EditMultiplayerGameInfoScreen(val multiplayerGame: OnlineMultiplayerGame)
|
||||
deleteButton.onClick {
|
||||
val askPopup = ConfirmPopup(
|
||||
this,
|
||||
"Are you sure you want to delete this map?",
|
||||
"Delete map",
|
||||
"Are you sure you want to delete this save?",
|
||||
"Delete save",
|
||||
) {
|
||||
try {
|
||||
game.onlineMultiplayer.deleteGame(multiplayerGame)
|
||||
|
@ -4,7 +4,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.unciv.logic.multiplayer.FriendList
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
|
@ -15,6 +15,7 @@ import com.unciv.logic.map.MapType
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.ExpanderTab
|
||||
import com.unciv.ui.utils.UncivSlider
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.onChange
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.pad
|
||||
@ -132,7 +133,7 @@ class MapParametersTable(
|
||||
|
||||
private fun addHexagonalSizeTable() {
|
||||
val defaultRadius = mapParameters.mapSize.radius.toString()
|
||||
customMapSizeRadius = TextField(defaultRadius, skin).apply {
|
||||
customMapSizeRadius = UncivTextField.create("Radius", defaultRadius).apply {
|
||||
textFieldFilter = DigitsOnlyFilter()
|
||||
}
|
||||
customMapSizeRadius.onChange {
|
||||
@ -146,12 +147,12 @@ class MapParametersTable(
|
||||
|
||||
private fun addRectangularSizeTable() {
|
||||
val defaultWidth = mapParameters.mapSize.width.toString()
|
||||
customMapWidth = TextField(defaultWidth, skin).apply {
|
||||
customMapWidth = UncivTextField.create("Width", defaultWidth).apply {
|
||||
textFieldFilter = DigitsOnlyFilter()
|
||||
}
|
||||
|
||||
val defaultHeight = mapParameters.mapSize.height.toString()
|
||||
customMapHeight = TextField(defaultHeight, skin).apply {
|
||||
customMapHeight = UncivTextField.create("Height", defaultHeight).apply {
|
||||
textFieldFilter = DigitsOnlyFilter()
|
||||
}
|
||||
|
||||
@ -252,7 +253,7 @@ class MapParametersTable(
|
||||
private fun addAdvancedControls(table: Table) {
|
||||
table.defaults().pad(5f)
|
||||
|
||||
seedTextField = TextField(mapParameters.seed.toString(), skin)
|
||||
seedTextField = UncivTextField.create("RNG Seed", mapParameters.seed.toString())
|
||||
seedTextField.textFieldFilter = DigitsOnlyFilter()
|
||||
|
||||
// If the field is empty, fallback seed value to 0
|
||||
|
@ -6,12 +6,12 @@ import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.IdChecker
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.multiplayer.FriendList
|
||||
import com.unciv.models.metadata.GameParameters
|
||||
import com.unciv.models.metadata.Player
|
||||
import com.unciv.models.ruleset.Nation
|
||||
@ -21,7 +21,6 @@ import com.unciv.ui.audio.MusicMood
|
||||
import com.unciv.ui.audio.MusicTrackChooserFlags
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.mapeditor.GameParametersScreen
|
||||
import com.unciv.logic.multiplayer.FriendList
|
||||
import com.unciv.ui.multiplayer.FriendPickerList
|
||||
import com.unciv.ui.pickerscreens.PickerPane
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
@ -165,8 +164,7 @@ class PlayerPickerTable(
|
||||
}
|
||||
if (gameParameters.isOnlineMultiplayer && player.playerType == PlayerType.Human) {
|
||||
|
||||
val playerIdTextField = TextField(player.playerId, BaseScreen.skin)
|
||||
playerIdTextField.messageText = "Please input Player ID!".tr()
|
||||
val playerIdTextField = UncivTextField.create("Please input Player ID!", player.playerId)
|
||||
playerTable.add(playerIdTextField).colspan(2).fillX().pad(5f)
|
||||
val errorLabel = "✘".toLabel(Color.RED)
|
||||
playerTable.add(errorLabel).pad(5f).row()
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.unciv.ui.options
|
||||
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.logic.MapSaver
|
||||
@ -9,6 +8,7 @@ import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.UncivSlider
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toCheckBox
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
@ -22,7 +22,7 @@ fun debugTab() = Table(BaseScreen.skin).apply {
|
||||
val worldScreen = game.worldScreen
|
||||
if (worldScreen != null) {
|
||||
val simulateButton = "Simulate until turn:".toTextButton()
|
||||
val simulateTextField = TextField(game.simulateUntilTurnForDebug.toString(), BaseScreen.skin)
|
||||
val simulateTextField = UncivTextField.create("Turn", game.simulateUntilTurnForDebug.toString())
|
||||
val invalidInputLabel = "This is not a valid integer!".toLabel().also { it.isVisible = false }
|
||||
simulateButton.onClick {
|
||||
val simulateUntilTurns = simulateTextField.text.toIntOrNull()
|
||||
|
@ -15,6 +15,7 @@ import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.addSeparator
|
||||
import com.unciv.ui.utils.extensions.brighten
|
||||
import com.unciv.ui.utils.extensions.format
|
||||
@ -131,7 +132,7 @@ private fun addMultiplayerServerOptions(
|
||||
} else {
|
||||
"https://"
|
||||
}
|
||||
val multiplayerServerTextField = TextField(textToShowForMultiplayerAddress, BaseScreen.skin)
|
||||
val multiplayerServerTextField = UncivTextField.create("Server address", textToShowForMultiplayerAddress)
|
||||
multiplayerServerTextField.setTextFieldFilter { _, c -> c !in " \r\n\t\\" }
|
||||
multiplayerServerTextField.programmaticChangeEvents = true
|
||||
val serverIpTable = Table()
|
||||
|
@ -30,7 +30,6 @@ import com.unciv.ui.utils.extensions.toGdxArray
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.Dispatcher
|
||||
import com.unciv.utils.concurrency.withGLContext
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
@ -43,7 +42,7 @@ class OptionsPopup(
|
||||
screen: BaseScreen,
|
||||
private val selectPage: Int = defaultPage,
|
||||
private val onClose: () -> Unit = {}
|
||||
) : Popup(screen) {
|
||||
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = false ) {
|
||||
val settings = screen.game.settings
|
||||
val tabs: TabbedPager
|
||||
val selectBoxMinWidth: Float
|
||||
|
@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
@ -15,6 +14,7 @@ import com.unciv.ui.newgamescreen.TranslatedSelectBox
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.ExpanderTab
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.onActivation
|
||||
@ -72,7 +72,7 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
||||
}
|
||||
}
|
||||
|
||||
private val textField = TextField("", BaseScreen.skin)
|
||||
private val textField = UncivTextField.create("Enter search text")
|
||||
fun getFilterText(): String = textField.text
|
||||
|
||||
var sortInstalled = SortType.Name
|
||||
@ -85,8 +85,6 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
||||
val expander: ExpanderTab
|
||||
|
||||
init {
|
||||
textField.messageText = "Enter search text".tr()
|
||||
|
||||
val searchIcon = ImageGetter.getImage("OtherIcons/Search")
|
||||
.surroundWithCircle(50f, color = Color.CLEAR)
|
||||
|
||||
|
@ -8,7 +8,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Button
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextArea
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.MainMenuScreen
|
||||
@ -20,22 +19,23 @@ import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.pickerscreens.ModManagementOptions.SortType
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.utils.AutoScrollPane
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.ExpanderTab
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.RecreateOnResize
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.WrappableLabel
|
||||
import com.unciv.ui.utils.extensions.UncivDateFormat.formatDate
|
||||
import com.unciv.ui.utils.extensions.UncivDateFormat.parseDate
|
||||
import com.unciv.ui.utils.extensions.addSeparator
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.isEnabled
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.onActivation
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toCheckBox
|
||||
@ -370,13 +370,13 @@ class ModManagementScreen(
|
||||
downloadButton.onClick {
|
||||
val popup = Popup(this)
|
||||
popup.addGoodSizedLabel("Please enter the mod repository -or- archive zip url:").row()
|
||||
val textArea = TextArea("https://github.com/...", skin)
|
||||
popup.add(textArea).width(stage.width / 2).row()
|
||||
val textField = UncivTextField.create("")
|
||||
popup.add(textField).width(stage.width / 2).row()
|
||||
val actualDownloadButton = "Download".toTextButton()
|
||||
actualDownloadButton.onClick {
|
||||
actualDownloadButton.setText("Downloading...".tr())
|
||||
actualDownloadButton.disable()
|
||||
downloadMod(Github.Repo().parseUrl(textArea.text)) { popup.close() }
|
||||
downloadMod(Github.Repo().parseUrl(textField.text)) { popup.close() }
|
||||
}
|
||||
popup.add(actualDownloadButton).row()
|
||||
popup.addCloseButton()
|
||||
@ -541,13 +541,12 @@ class ModManagementScreen(
|
||||
screen = this,
|
||||
question = "Are you SURE you want to delete this mod?",
|
||||
confirmText = deleteText,
|
||||
action = {
|
||||
deleteMod(mod.ruleset)
|
||||
modActionTable.clear()
|
||||
rightSideButton.setText("[${mod.name}] was deleted.".tr())
|
||||
},
|
||||
restoreDefault = { rightSideButton.isEnabled = true }
|
||||
).open()
|
||||
) {
|
||||
deleteMod(mod.ruleset)
|
||||
modActionTable.clear()
|
||||
rightSideButton.setText("[${mod.name}] was deleted.".tr())
|
||||
}.open()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.ui.images.IconCircleGroup
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.onChange
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
@ -60,7 +61,7 @@ class AskNumberPopup(
|
||||
wrapper.add(label.toLabel())
|
||||
add(wrapper).colspan(2).row()
|
||||
|
||||
val nameField = TextField(defaultValue, skin)
|
||||
val nameField = UncivTextField.create(label, defaultValue)
|
||||
nameField.textFieldFilter = TextField.TextFieldFilter { _, char -> char.isDigit() || char == '-' }
|
||||
|
||||
fun isValidInt(input: String): Boolean {
|
||||
|
@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.ui.images.IconCircleGroup
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
|
||||
@ -39,7 +40,7 @@ class AskTextPopup(
|
||||
wrapper.add(label.toLabel())
|
||||
add(wrapper).colspan(2).row()
|
||||
|
||||
val nameField = TextField(defaultText, skin)
|
||||
val nameField = UncivTextField.create(label, defaultText)
|
||||
nameField.textFieldFilter = TextField.TextFieldFilter { _, char -> char !in illegalChars}
|
||||
nameField.maxLength = maxLength
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.unciv.ui.popup
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.math.Rectangle
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
@ -13,6 +14,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.AutoScrollPane
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
@ -30,7 +33,8 @@ import com.unciv.ui.utils.extensions.toTextButton
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class Popup(
|
||||
val stageToShowOn: Stage
|
||||
val stageToShowOn: Stage,
|
||||
scrollable: Boolean = true
|
||||
): Table(BaseScreen.skin) {
|
||||
|
||||
constructor(screen: BaseScreen) : this(screen.stage)
|
||||
@ -39,22 +43,22 @@ open class Popup(
|
||||
// from the 'screen blocking' part of the popup (which covers the entire screen)
|
||||
val innerTable = Table(BaseScreen.skin)
|
||||
|
||||
val showListeners = mutableListOf<() -> Unit>()
|
||||
val closeListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
val scrollPane: AutoScrollPane
|
||||
val events = EventBus.EventReceiver()
|
||||
|
||||
init {
|
||||
// Set actor name for debugging
|
||||
name = javaClass.simpleName
|
||||
|
||||
scrollPane = AutoScrollPane(innerTable, BaseScreen.skin)
|
||||
|
||||
background = ImageGetter.getBackground(Color.GRAY.cpy().apply { a=.5f })
|
||||
innerTable.background = ImageGetter.getBackground(ImageGetter.getBlue().darken(0.5f))
|
||||
|
||||
innerTable.pad(20f)
|
||||
innerTable.defaults().pad(5f)
|
||||
super.add(scrollPane)
|
||||
|
||||
super.add(if (scrollable) AutoScrollPane(innerTable, BaseScreen.skin) else innerTable)
|
||||
|
||||
this.isVisible = false
|
||||
touchable = Touchable.enabled // don't allow clicking behind
|
||||
@ -70,20 +74,34 @@ open class Popup(
|
||||
innerTable.pack()
|
||||
pack()
|
||||
center(stageToShowOn)
|
||||
events.receive(UncivStage.VisibleAreaChanged::class) {
|
||||
fitContentIntoVisibleArea(it.visibleArea)
|
||||
}
|
||||
fitContentIntoVisibleArea((stageToShowOn as UncivStage).lastKnownVisibleArea)
|
||||
if (force || !stageToShowOn.hasOpenPopups()) {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fitContentIntoVisibleArea(visibleArea: Rectangle) {
|
||||
padLeft(visibleArea.x)
|
||||
padBottom(visibleArea.y)
|
||||
padRight(stageToShowOn.width - visibleArea.x - visibleArea.width)
|
||||
padTop(stageToShowOn.height - visibleArea.y - visibleArea.height)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/** Subroutine for [open] handles only visibility */
|
||||
private fun show() {
|
||||
this.isVisible = true
|
||||
for (listener in showListeners) listener()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this popup and - if any other popups are pending - display the next one.
|
||||
*/
|
||||
open fun close() {
|
||||
events.stopReceiving()
|
||||
for (listener in closeListeners) listener()
|
||||
remove()
|
||||
val nextPopup = stageToShowOn.actors.firstOrNull { it is Popup }
|
||||
|
@ -4,14 +4,14 @@ import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.popup.ConfirmPopup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.UncivTextField
|
||||
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
@ -25,7 +25,7 @@ import com.unciv.utils.concurrency.launchOnGLThread
|
||||
|
||||
|
||||
class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") {
|
||||
private val gameNameTextField = TextField("", skin)
|
||||
private val gameNameTextField = UncivTextField.create("Saved game name")
|
||||
|
||||
init {
|
||||
setDefaultCloseAction()
|
||||
|
@ -1,9 +1,7 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.UncivGame
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.ui.crashhandling.CrashScreen
|
||||
|
||||
/** Interface to support various platform-specific tools */
|
||||
interface GeneralPlatformSpecificHelpers {
|
||||
@ -35,4 +33,10 @@ interface GeneralPlatformSpecificHelpers {
|
||||
* @return true if the throwable was handled.
|
||||
*/
|
||||
fun handleUncaughtThrowable(ex: Throwable): Boolean = false
|
||||
|
||||
/**
|
||||
* Adds platform-specific improvements to the given text field, making it nicer to interact with on this platform.
|
||||
*/
|
||||
fun addImprovements(textField: TextField): TextField = textField
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.Input
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.EventListener
|
||||
@ -12,7 +11,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener
|
||||
import com.badlogic.gdx.utils.Align
|
||||
@ -22,10 +20,10 @@ import com.unciv.ui.images.IconTextButton
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.addSeparator
|
||||
import com.unciv.ui.utils.extensions.darken
|
||||
import com.unciv.ui.utils.extensions.isEnabled
|
||||
import com.unciv.ui.utils.extensions.keyShortcuts
|
||||
import com.unciv.ui.utils.extensions.onActivation
|
||||
import com.unciv.ui.utils.extensions.packIfNeeded
|
||||
import com.unciv.ui.utils.extensions.pad
|
||||
@ -328,7 +326,7 @@ open class TabbedPager(
|
||||
}
|
||||
override fun getMinWidth() = dimW.min
|
||||
override fun getMaxWidth() = dimW.max
|
||||
override fun getMinHeight() = dimH.min + headerHeight
|
||||
override fun getMinHeight() = headerHeight
|
||||
override fun getMaxHeight() = dimH.max + headerHeight
|
||||
|
||||
//endregion
|
||||
@ -574,7 +572,7 @@ open class TabbedPager(
|
||||
*/
|
||||
fun askForPassword(secretHashCode: Int = 0) {
|
||||
class PassPopup(screen: BaseScreen, unlockAction: ()->Unit, lockAction: ()->Unit) : Popup(screen) {
|
||||
val passEntry = TextField("", BaseScreen.skin)
|
||||
val passEntry = UncivTextField.create("Password")
|
||||
init {
|
||||
passEntry.isPasswordMode = true
|
||||
add(passEntry).row()
|
||||
|
97
core/src/com/unciv/ui/utils/UncivTextField.kt
Normal file
97
core/src/com/unciv/ui/utils/UncivTextField.kt
Normal file
@ -0,0 +1,97 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
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.UncivGame
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.utils.extensions.getAscendant
|
||||
import com.unciv.ui.utils.extensions.getOverlap
|
||||
import com.unciv.ui.utils.extensions.right
|
||||
import com.unciv.ui.utils.extensions.stageBoundingBox
|
||||
import com.unciv.ui.utils.extensions.top
|
||||
|
||||
object UncivTextField {
|
||||
/**
|
||||
* Creates a text field that has nicer platform-specific input added compared to the default gdx [TextField].
|
||||
* @param hint The text that should be displayed in the text field when no text is entered, will automatically be translated
|
||||
* @param preEnteredText the text already entered within this text field. Supported on all platforms.
|
||||
*/
|
||||
fun create(hint: String, preEnteredText: String = ""): TextField {
|
||||
@Suppress("UNCIV_RAW_TEXTFIELD")
|
||||
val textField = TextField(preEnteredText, BaseScreen.skin)
|
||||
val translatedHint = hint.tr()
|
||||
textField.messageText = translatedHint
|
||||
textField.addListener(object : FocusListener() {
|
||||
override fun keyboardFocusChanged(event: FocusEvent, actor: Actor, focused: Boolean) {
|
||||
if (focused) {
|
||||
textField.scrollAscendantToTextField()
|
||||
}
|
||||
}
|
||||
})
|
||||
UncivGame.Current.platformSpecificHelper?.addImprovements(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
|
||||
}
|
@ -3,6 +3,8 @@ package com.unciv.ui.utils.extensions
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Input
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.math.Rectangle
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.InputEvent
|
||||
@ -31,10 +33,10 @@ import com.unciv.ui.images.IconCircleGroup
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.KeyShortcut
|
||||
import com.unciv.ui.utils.KeyShortcutDispatcher
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
/**
|
||||
* Collection of extension functions mostly for libGdx widgets
|
||||
@ -309,6 +311,55 @@ fun Actor.addBorder(size:Float, color: Color, expandCell:Boolean = false): Table
|
||||
return table
|
||||
}
|
||||
|
||||
/** Gets a parent of this actor that matches the [predicate], or null if none of its parents match the [predicate]. */
|
||||
fun Actor.getAscendant(predicate: (Actor) -> Boolean): Actor? {
|
||||
var curParent = parent
|
||||
while (curParent != null) {
|
||||
if (predicate(curParent)) return curParent
|
||||
curParent = curParent.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** The actors bounding box in stage coordinates */
|
||||
val Actor.stageBoundingBox: Rectangle get() {
|
||||
val bottomleft = localToStageCoordinates(Vector2(0f, 0f))
|
||||
val topright = localToStageCoordinates(Vector2(width, height))
|
||||
return Rectangle(
|
||||
bottomleft.x,
|
||||
bottomleft.y,
|
||||
topright.x - bottomleft.x,
|
||||
topright.y - bottomleft.y
|
||||
)
|
||||
}
|
||||
|
||||
/** @return the area where this [Rectangle] overlaps with [other], or `null` if it doesn't overlap. */
|
||||
fun Rectangle.getOverlap(other: Rectangle): Rectangle? {
|
||||
val overlapX = if (x > other.x) x else other.x
|
||||
|
||||
val rightX = x + width
|
||||
val otherRightX = other.x + other.width
|
||||
val overlapWidth = (if (rightX < otherRightX) rightX else otherRightX) - overlapX
|
||||
|
||||
val overlapY = if (y > other.y) y else other.y
|
||||
|
||||
val topY = y + height
|
||||
val otherTopY = other.y + other.height
|
||||
val overlapHeight = (if (topY < otherTopY) topY else otherTopY) - overlapY
|
||||
|
||||
val noOverlap = overlapWidth <= 0 || overlapHeight <= 0
|
||||
if (noOverlap) return null
|
||||
return Rectangle(
|
||||
overlapX,
|
||||
overlapY,
|
||||
overlapWidth,
|
||||
overlapHeight
|
||||
)
|
||||
}
|
||||
|
||||
val Rectangle.top get() = y + height
|
||||
val Rectangle.right get() = x + width
|
||||
|
||||
fun Group.addBorderAllowOpacity(size:Float, color: Color): Group {
|
||||
val group = this
|
||||
fun getTopBottomBorder() = ImageGetter.getDot(color).apply { width=group.width; height=size }
|
||||
|
Reference in New Issue
Block a user