diff --git a/.gitignore b/.gitignore index 402126c984..0def93ff83 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000..374dbefc60 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 58f4518d0b..634d5b13d3 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -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)) + } + } + }) } /** diff --git a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt b/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt index 74a4530184..48f1a93cef 100644 --- a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt +++ b/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt @@ -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) + } } diff --git a/android/src/com/unciv/app/TextfieldImprovements.kt b/android/src/com/unciv/app/TextfieldImprovements.kt new file mode 100644 index 0000000000..38428b3b3f --- /dev/null +++ b/android/src/com/unciv/app/TextfieldImprovements.kt @@ -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 + } +} diff --git a/core/src/com/unciv/ui/UncivStage.kt b/core/src/com/unciv/ui/UncivStage.kt index 4caf25a718..981a4556dc 100644 --- a/core/src/com/unciv/ui/UncivStage.kt +++ b/core/src/com/unciv/ui/UncivStage.kt @@ -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 } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt index e1241eb7e4..32db7f262e 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt @@ -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 diff --git a/core/src/com/unciv/ui/multiplayer/AddFriendScreen.kt b/core/src/com/unciv/ui/multiplayer/AddFriendScreen.kt index 10320938c2..aff5a56e21 100644 --- a/core/src/com/unciv/ui/multiplayer/AddFriendScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/AddFriendScreen.kt @@ -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() diff --git a/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt b/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt index 58ebab96e7..24a475716b 100644 --- a/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/multiplayer/EditFriendScreen.kt b/core/src/com/unciv/ui/multiplayer/EditFriendScreen.kt index 7bb970a78c..679919f587 100644 --- a/core/src/com/unciv/ui/multiplayer/EditFriendScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/EditFriendScreen.kt @@ -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() diff --git a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt index d7f8c5a261..1c08eec30f 100644 --- a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt @@ -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) diff --git a/core/src/com/unciv/ui/multiplayer/ViewFriendsListScreen.kt b/core/src/com/unciv/ui/multiplayer/ViewFriendsListScreen.kt index 68ab25c9fe..a64543f8a8 100644 --- a/core/src/com/unciv/ui/multiplayer/ViewFriendsListScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/ViewFriendsListScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 8232c4b844..e5ee389aa4 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -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 diff --git a/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt b/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt index 51b482ee55..bae37bdd3e 100644 --- a/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt @@ -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() diff --git a/core/src/com/unciv/ui/options/DebugTab.kt b/core/src/com/unciv/ui/options/DebugTab.kt index 79f74870b0..80475289c9 100644 --- a/core/src/com/unciv/ui/options/DebugTab.kt +++ b/core/src/com/unciv/ui/options/DebugTab.kt @@ -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() diff --git a/core/src/com/unciv/ui/options/MultiplayerTab.kt b/core/src/com/unciv/ui/options/MultiplayerTab.kt index 6c9a181de5..fc9e9e7bb2 100644 --- a/core/src/com/unciv/ui/options/MultiplayerTab.kt +++ b/core/src/com/unciv/ui/options/MultiplayerTab.kt @@ -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() diff --git a/core/src/com/unciv/ui/options/OptionsPopup.kt b/core/src/com/unciv/ui/options/OptionsPopup.kt index 87ca00330d..6cb3536d55 100644 --- a/core/src/com/unciv/ui/options/OptionsPopup.kt +++ b/core/src/com/unciv/ui/options/OptionsPopup.kt @@ -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 diff --git a/core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt b/core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt index c25d384616..6bcf48c50d 100644 --- a/core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt +++ b/core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt @@ -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) diff --git a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt index 95a9da9a75..2f787546af 100644 --- a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt @@ -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() } } diff --git a/core/src/com/unciv/ui/popup/AskNumberPopup.kt b/core/src/com/unciv/ui/popup/AskNumberPopup.kt index 4ca7d5149b..89a5b16f03 100644 --- a/core/src/com/unciv/ui/popup/AskNumberPopup.kt +++ b/core/src/com/unciv/ui/popup/AskNumberPopup.kt @@ -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 { diff --git a/core/src/com/unciv/ui/popup/AskTextPopup.kt b/core/src/com/unciv/ui/popup/AskTextPopup.kt index 83f255162f..35ad651e0a 100644 --- a/core/src/com/unciv/ui/popup/AskTextPopup.kt +++ b/core/src/com/unciv/ui/popup/AskTextPopup.kt @@ -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 diff --git a/core/src/com/unciv/ui/popup/Popup.kt b/core/src/com/unciv/ui/popup/Popup.kt index c782525609..60b08a5a3c 100644 --- a/core/src/com/unciv/ui/popup/Popup.kt +++ b/core/src/com/unciv/ui/popup/Popup.kt @@ -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 } diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 0c3fa4a580..764f6816f2 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -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() diff --git a/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt b/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt index 1a015b5f64..53b1a7bef8 100644 --- a/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt +++ b/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt @@ -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 + } diff --git a/core/src/com/unciv/ui/utils/TabbedPager.kt b/core/src/com/unciv/ui/utils/TabbedPager.kt index a7542d10cf..268e150a00 100644 --- a/core/src/com/unciv/ui/utils/TabbedPager.kt +++ b/core/src/com/unciv/ui/utils/TabbedPager.kt @@ -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() diff --git a/core/src/com/unciv/ui/utils/UncivTextField.kt b/core/src/com/unciv/ui/utils/UncivTextField.kt new file mode 100644 index 0000000000..912d05d7f0 --- /dev/null +++ b/core/src/com/unciv/ui/utils/UncivTextField.kt @@ -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 +} diff --git a/core/src/com/unciv/ui/utils/extensions/Scene2dExtensions.kt b/core/src/com/unciv/ui/utils/extensions/Scene2dExtensions.kt index 2616c998e6..610bef4e50 100644 --- a/core/src/com/unciv/ui/utils/extensions/Scene2dExtensions.kt +++ b/core/src/com/unciv/ui/utils/extensions/Scene2dExtensions.kt @@ -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 }