mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-11 00:08:58 +07:00
Popup gets a KeyPressDispatcher (#3973)
* Popup gets a KeyPressDispatcher * Popup gets a KeyPressDispatcher - patch1
This commit is contained in:
@ -52,6 +52,7 @@ Remove from queue =
|
|||||||
Show stats drilldown =
|
Show stats drilldown =
|
||||||
Show construction queue =
|
Show construction queue =
|
||||||
Save =
|
Save =
|
||||||
|
Cancel =
|
||||||
|
|
||||||
Diplomacy =
|
Diplomacy =
|
||||||
War =
|
War =
|
||||||
|
@ -7,6 +7,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
|||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.ui.utils.*
|
import com.unciv.ui.utils.*
|
||||||
|
|
||||||
|
/** Widget for the City Screen -
|
||||||
|
* the panel at bottom center showing the city name and offering arrows to cycle through the cities. */
|
||||||
class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
@ -16,7 +18,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
clear()
|
clear()
|
||||||
|
|
||||||
if (civInfo.cities.size > 1) {
|
if (civInfo.cities.size > 1) {
|
||||||
val prevCityButton = Table() // so we gt a wider clickable area than just the image itself
|
val prevCityButton = Table() // so we get a wider clickable area than just the image itself
|
||||||
val image = ImageGetter.getImage("OtherIcons/BackArrow")
|
val image = ImageGetter.getImage("OtherIcons/BackArrow")
|
||||||
image.color = civInfo.nation.getInnerColor()
|
image.color = civInfo.nation.getInnerColor()
|
||||||
prevCityButton.add(image).size(25f).pad(10f)
|
prevCityButton.add(image).size(25f).pad(10f)
|
||||||
@ -26,6 +28,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
} else add()
|
} else add()
|
||||||
|
|
||||||
val cityNameTable = Table()
|
val cityNameTable = Table()
|
||||||
|
|
||||||
if (city.isBeingRazed) {
|
if (city.isBeingRazed) {
|
||||||
val fireImage = ImageGetter.getImage("OtherIcons/Fire")
|
val fireImage = ImageGetter.getImage("OtherIcons/Fire")
|
||||||
cityNameTable.add(fireImage).size(20f).padRight(5f)
|
cityNameTable.add(fireImage).size(20f).padRight(5f)
|
||||||
@ -41,7 +44,6 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
cityNameTable.add(starImage).size(20f).padRight(5f)
|
cityNameTable.add(starImage).size(20f).padRight(5f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (city.isInResistance()) {
|
if (city.isInResistance()) {
|
||||||
val resistanceImage = ImageGetter.getImage("StatIcons/Resistance")
|
val resistanceImage = ImageGetter.getImage("StatIcons/Resistance")
|
||||||
cityNameTable.add(resistanceImage).size(20f).padRight(5f)
|
cityNameTable.add(resistanceImage).size(20f).padRight(5f)
|
||||||
@ -53,11 +55,13 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
val textArea = TextField(city.name, CameraStageBaseScreen.skin)
|
val textArea = TextField(city.name, CameraStageBaseScreen.skin)
|
||||||
textArea.alignment = Align.center
|
textArea.alignment = Align.center
|
||||||
editCityNamePopup.add(textArea).colspan(2).row()
|
editCityNamePopup.add(textArea).colspan(2).row()
|
||||||
editCityNamePopup.addButton("Save") {
|
//editCityNamePopup.name = "CityNamePopup" // debug help
|
||||||
|
editCityNamePopup.addButtonInRow("Save", '\r') {
|
||||||
city.name = textArea.text
|
city.name = textArea.text
|
||||||
cityScreen.game.setScreen(CityScreen(city))
|
cityScreen.game.setScreen(CityScreen(city))
|
||||||
}
|
}
|
||||||
editCityNamePopup.addCloseButton()
|
editCityNamePopup.addCloseButton("Cancel")
|
||||||
|
editCityNamePopup.keyboardFocus = textArea
|
||||||
editCityNamePopup.open()
|
editCityNamePopup.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,9 +69,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
|
|
||||||
add(cityNameTable).width(stage.width / 4)
|
add(cityNameTable).width(stage.width / 4)
|
||||||
|
|
||||||
|
|
||||||
if (civInfo.cities.size > 1) {
|
if (civInfo.cities.size > 1) {
|
||||||
|
|
||||||
val nextCityButton = Table() // so we gt a wider clickable area than just the image itself
|
val nextCityButton = Table() // so we gt a wider clickable area than just the image itself
|
||||||
val image = ImageGetter.getImage("OtherIcons/BackArrow")
|
val image = ImageGetter.getImage("OtherIcons/BackArrow")
|
||||||
image.setSize(25f, 25f)
|
image.setSize(25f, 25f)
|
||||||
@ -79,6 +81,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
|
|||||||
nextCityButton.onClick { cityScreen.page(1) }
|
nextCityButton.onClick { cityScreen.page(1) }
|
||||||
add(nextCityButton).pad(10f)
|
add(nextCityButton).pad(10f)
|
||||||
} else add()
|
} else add()
|
||||||
|
|
||||||
pack()
|
pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,10 +85,20 @@ class KeyPressDispatcher: HashMap<KeyCharAndCode, (() -> Unit)>() {
|
|||||||
// access by KeyCharAndCode
|
// access by KeyCharAndCode
|
||||||
operator fun set(key: KeyCharAndCode, action: () -> Unit) {
|
operator fun set(key: KeyCharAndCode, action: () -> Unit) {
|
||||||
super.put(key, action)
|
super.put(key, action)
|
||||||
|
// On Android the Enter key will fire with Ascii code `Linefeed`, on desktop as `Carriage Return`
|
||||||
|
if (key == KeyCharAndCode('\r'))
|
||||||
|
super.put(KeyCharAndCode('\n'), action)
|
||||||
|
// Likewise always match Back to ESC
|
||||||
|
if (key == KeyCharAndCode(Input.Keys.BACK))
|
||||||
|
super.put(KeyCharAndCode('\u001B'), action)
|
||||||
checkInstall()
|
checkInstall()
|
||||||
}
|
}
|
||||||
override fun remove(key: KeyCharAndCode): (() -> Unit)? {
|
override fun remove(key: KeyCharAndCode): (() -> Unit)? {
|
||||||
val result = super.remove(key)
|
val result = super.remove(key)
|
||||||
|
if (key == KeyCharAndCode('\r'))
|
||||||
|
super.remove(KeyCharAndCode('\n'))
|
||||||
|
if (key == KeyCharAndCode(Input.Keys.BACK))
|
||||||
|
super.remove(KeyCharAndCode('\u001B'))
|
||||||
checkInstall()
|
checkInstall()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
package com.unciv.ui.utils
|
package com.unciv.ui.utils
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Input
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all Popups, i.e. Tables that get rendered in the middle of a screen and on top of everything else
|
* Base class for all Popups, i.e. Tables that get rendered in the middle of a screen and on top of everything else
|
||||||
*/
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen.skin) {
|
open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen.skin) {
|
||||||
val innerTable = Table(CameraStageBaseScreen.skin) // This exists to differentiate the actual popup (the inner table)
|
// This exists to differentiate the actual popup (the inner table)
|
||||||
// from the 'screen blocking' part of the popup (which covers the entire screen)
|
// from the 'screen blocking' part of the popup (which covers the entire screen)
|
||||||
|
val innerTable = Table(CameraStageBaseScreen.skin)
|
||||||
|
|
||||||
|
/** The [KeyPressDispatcher] for the popup - Key handlers from the parent screen are inactive
|
||||||
|
* while the popup is active through the [hasOpenPopups][CameraStageBaseScreen.hasOpenPopups] mechanism.
|
||||||
|
* @see [KeyPressDispatcher.install]
|
||||||
|
*/
|
||||||
|
val keyPressDispatcher = KeyPressDispatcher()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
// Set actor name for debugging
|
||||||
|
name = javaClass.simpleName
|
||||||
|
|
||||||
background = ImageGetter.getBackground(Color.GRAY.cpy().apply { a=.5f })
|
background = ImageGetter.getBackground(Color.GRAY.cpy().apply { a=.5f })
|
||||||
innerTable.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK, 0.5f))
|
innerTable.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK, 0.5f))
|
||||||
|
|
||||||
@ -27,7 +36,7 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
|
|||||||
|
|
||||||
this.isVisible = false
|
this.isVisible = false
|
||||||
touchable = Touchable.enabled // don't allow clicking behind
|
touchable = Touchable.enabled // don't allow clicking behind
|
||||||
setFillParent(true)
|
this.setFillParent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +45,7 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
|
|||||||
*/
|
*/
|
||||||
fun open(force: Boolean = false) {
|
fun open(force: Boolean = false) {
|
||||||
if (force || !screen.hasOpenPopups()) {
|
if (force || !screen.hasOpenPopups()) {
|
||||||
this.isVisible = true
|
show()
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.stage.addActor(this)
|
screen.stage.addActor(this)
|
||||||
@ -45,17 +54,33 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
|
|||||||
center(screen.stage)
|
center(screen.stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun close() {
|
/** Subroutine for [open] handles only visibility and [keyPressDispatcher] */
|
||||||
remove()
|
private fun show() {
|
||||||
val nextPopup = screen.stage.actors.firstOrNull { it is Popup }
|
this.isVisible = true
|
||||||
if (nextPopup != null) nextPopup.isVisible = true
|
keyPressDispatcher.install(screen.stage, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** All additions to the popup are to the inner table - we shouldn't care that there's an inner table at all */
|
/**
|
||||||
override fun <T : Actor?> add(actor: T) = innerTable.add(actor)
|
* Close this popup and - if any other popups are pending - display the next one.
|
||||||
override fun row() = innerTable.row()
|
*/
|
||||||
|
open fun close() {
|
||||||
|
keyPressDispatcher.uninstall()
|
||||||
|
remove()
|
||||||
|
val nextPopup = screen.stage.actors.firstOrNull { it is Popup }
|
||||||
|
if (nextPopup != null) (nextPopup as Popup).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All additions to the popup are to the inner table - we shouldn't care that there's an inner table at all */
|
||||||
|
override fun <T : Actor?> add(actor: T): Cell<T> = innerTable.add(actor)
|
||||||
|
override fun row(): Cell<Actor> = innerTable.row()
|
||||||
fun addSeparator() = innerTable.addSeparator()
|
fun addSeparator() = innerTable.addSeparator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a [caption][text] label: A label with word wrap enabled over half the stage width.
|
||||||
|
* Will be larger than normal text if the [size] parameter is set to >18f.
|
||||||
|
* @param text The caption text.
|
||||||
|
* @param size The font size for the label.
|
||||||
|
*/
|
||||||
fun addGoodSizedLabel(text: String, size:Int=18): Cell<Label> {
|
fun addGoodSizedLabel(text: String, size:Int=18): Cell<Label> {
|
||||||
val label = text.toLabel(fontSize = size)
|
val label = text.toLabel(fontSize = size)
|
||||||
label.wrap = true
|
label.wrap = true
|
||||||
@ -63,25 +88,88 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
|
|||||||
return add(label).width(screen.stage.width / 2)
|
return add(label).width(screen.stage.width / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addButton(text: String, action: () -> Unit): Cell<TextButton> {
|
/**
|
||||||
|
* Adds an inline [TextButton].
|
||||||
|
* @param text The button's caption.
|
||||||
|
* @param key Associate a key with this button's action.
|
||||||
|
* @param action A lambda to be executed when the button is clicked.
|
||||||
|
* @return The new [Cell]
|
||||||
|
*/
|
||||||
|
fun addButtonInRow(text: String, key: KeyCharAndCode? = null, action: () -> Unit): Cell<TextButton> {
|
||||||
val button = text.toTextButton().apply { color = ImageGetter.getBlue() }
|
val button = text.toTextButton().apply { color = ImageGetter.getBlue() }
|
||||||
button.onClick(action)
|
button.onClick(action)
|
||||||
return add(button).apply { row() }
|
if (key != null) {
|
||||||
|
keyPressDispatcher[key] = action
|
||||||
|
}
|
||||||
|
return add(button)
|
||||||
|
}
|
||||||
|
fun addButtonInRow(text: String, key: Char, action: () -> Unit)
|
||||||
|
= addButtonInRow(text, KeyCharAndCode(key), action)
|
||||||
|
fun addButtonInRow(text: String, key: Int, action: () -> Unit)
|
||||||
|
= addButtonInRow(text, KeyCharAndCode(key), action)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a [TextButton] and ends the current row.
|
||||||
|
* @param text The button's caption.
|
||||||
|
* @param key Associate a key with this button's action.
|
||||||
|
* @param action A lambda to be executed when the button is clicked.
|
||||||
|
* @return The new [Cell]
|
||||||
|
*/
|
||||||
|
fun addButton(text: String, key: KeyCharAndCode? = null, action: () -> Unit)
|
||||||
|
= addButtonInRow(text, key, action).apply { row() }
|
||||||
|
/** @link [addButton] */
|
||||||
|
fun addButton(text: String, key: Char, action: () -> Unit)
|
||||||
|
= addButtonInRow(text, key, action).apply { row() }
|
||||||
|
fun addButton(text: String, key: Int, action: () -> Unit)
|
||||||
|
= addButtonInRow(text, key, action).apply { row() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a [TextButton] that closes the popup.
|
||||||
|
* @param text The button's caption, defaults to "Close".
|
||||||
|
* @param additionalKey An additional key that should close the popup, Back and ESC are assigned by default.
|
||||||
|
* @param action A lambda to be executed after closing the popup when the button is clicked.
|
||||||
|
* @return The new [Cell]
|
||||||
|
*/
|
||||||
|
fun addCloseButton(
|
||||||
|
text: String = Constants.close,
|
||||||
|
additionalKey: KeyCharAndCode? = null,
|
||||||
|
action: (()->Unit)? = null
|
||||||
|
): Cell<TextButton> {
|
||||||
|
val closeAction = { close(); if(action!=null) action() }
|
||||||
|
keyPressDispatcher[Input.Keys.BACK] = closeAction
|
||||||
|
return addButton(text, additionalKey, closeAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
fun addCloseButton(action: (()->Unit)? = null): Cell<TextButton> {
|
* Sets or retrieves the [Actor] that currently has keyboard focus.
|
||||||
return if (action==null)
|
*
|
||||||
addButton(Constants.close) { close() }
|
* Setting focus on a [TextField] will select all contained text unless a
|
||||||
else
|
* [FocusListener][com.badlogic.gdx.scenes.scene2d.utils.FocusListener] cancels the event.
|
||||||
addButton(Constants.close) { close(); action() }
|
*/
|
||||||
}
|
var keyboardFocus: Actor?
|
||||||
|
get() = screen.stage.keyboardFocus
|
||||||
|
set(value) {
|
||||||
|
if (screen.stage.setKeyboardFocus(value))
|
||||||
|
(value as? TextField)?.selectAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are visible [Popup]s.
|
||||||
|
* @return `true` if any were found.
|
||||||
|
*/
|
||||||
fun CameraStageBaseScreen.hasOpenPopups(): Boolean = stage.actors.any { it is Popup && it.isVisible }
|
fun CameraStageBaseScreen.hasOpenPopups(): Boolean = stage.actors.any { it is Popup && it.isVisible }
|
||||||
|
|
||||||
|
/** Closes all [Popup]s. */
|
||||||
fun CameraStageBaseScreen.closeAllPopups() = popups.forEach { it.close() }
|
fun CameraStageBaseScreen.closeAllPopups() = popups.forEach { it.close() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the topmost visible [Popup].
|
||||||
|
* @return The [name][Popup.name] of the closed [Popup] if any popup was closed and if it had a name.
|
||||||
|
*/
|
||||||
fun CameraStageBaseScreen.closeOneVisiblePopup() = popups.lastOrNull { it.isVisible }?.apply { close() }?.name
|
fun CameraStageBaseScreen.closeOneVisiblePopup() = popups.lastOrNull { it.isVisible }?.apply { close() }?.name
|
||||||
|
|
||||||
|
/** @return A [List] of currently active or pending [Popup] screens. */
|
||||||
val CameraStageBaseScreen.popups: List<Popup>
|
val CameraStageBaseScreen.popups: List<Popup>
|
||||||
get() = stage.actors.filterIsInstance<Popup>()
|
get() = stage.actors.filterIsInstance<Popup>()
|
||||||
|
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
package com.unciv.ui.utils
|
package com.unciv.ui.utils
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Input
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
|
|
||||||
class YesNoPopup(question:String, action:()->Unit,
|
/** Variant of [Popup] pre-populated with one label, plus yes and no buttons
|
||||||
screen: CameraStageBaseScreen = UncivGame.Current.worldScreen, restoreDefault:()->Unit = {}) : Popup(screen){
|
* @param question The text for the label
|
||||||
init{
|
* @param action A lambda to execute when "Yes" is chosen
|
||||||
|
* @param screen The parent screen - see [Popup.screen]. Optional, defaults to the current [WorldScreen][com.unciv.ui.worldscreen.WorldScreen]
|
||||||
|
* @param restoreDefault A lambda to execute when "No" is chosen
|
||||||
|
*/
|
||||||
|
class YesNoPopup (
|
||||||
|
question:String,
|
||||||
|
action:()->Unit,
|
||||||
|
screen: CameraStageBaseScreen = UncivGame.Current.worldScreen,
|
||||||
|
restoreDefault:()->Unit = {}
|
||||||
|
) : Popup(screen) {
|
||||||
|
private val yes = { close(); action() }
|
||||||
|
private val no = { close(); restoreDefault() }
|
||||||
|
|
||||||
|
init {
|
||||||
add(question.toLabel()).colspan(2).row()
|
add(question.toLabel()).colspan(2).row()
|
||||||
add("No".toTextButton().onClick { close(); restoreDefault() })
|
addButtonInRow("Yes", 'y', yes)
|
||||||
add("Yes".toTextButton().onClick { close(); action() })
|
addButtonInRow("No", 'n', no)
|
||||||
|
keyPressDispatcher['\r'] = yes
|
||||||
|
keyPressDispatcher[Input.Keys.BACK] = no
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user