Popup gets a KeyPressDispatcher (#3973)

* Popup gets a KeyPressDispatcher

* Popup gets a KeyPressDispatcher - patch1
This commit is contained in:
SomeTroglodyte 2021-05-27 20:49:40 +02:00 committed by GitHub
parent 5f523cb548
commit cf2be25c73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 34 deletions

View File

@ -52,6 +52,7 @@ Remove from queue =
Show stats drilldown =
Show construction queue =
Save =
Cancel =
Diplomacy =
War =

View File

@ -7,6 +7,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.utils.Align
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() {
fun update() {
@ -16,7 +18,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
clear()
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")
image.color = civInfo.nation.getInnerColor()
prevCityButton.add(image).size(25f).pad(10f)
@ -26,6 +28,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
} else add()
val cityNameTable = Table()
if (city.isBeingRazed) {
val fireImage = ImageGetter.getImage("OtherIcons/Fire")
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)
}
if (city.isInResistance()) {
val resistanceImage = ImageGetter.getImage("StatIcons/Resistance")
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)
textArea.alignment = Align.center
editCityNamePopup.add(textArea).colspan(2).row()
editCityNamePopup.addButton("Save") {
//editCityNamePopup.name = "CityNamePopup" // debug help
editCityNamePopup.addButtonInRow("Save", '\r') {
city.name = textArea.text
cityScreen.game.setScreen(CityScreen(city))
}
editCityNamePopup.addCloseButton()
editCityNamePopup.addCloseButton("Cancel")
editCityNamePopup.keyboardFocus = textArea
editCityNamePopup.open()
}
@ -65,9 +69,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
add(cityNameTable).width(stage.width / 4)
if (civInfo.cities.size > 1) {
val nextCityButton = Table() // so we gt a wider clickable area than just the image itself
val image = ImageGetter.getImage("OtherIcons/BackArrow")
image.setSize(25f, 25f)
@ -79,6 +81,7 @@ class CityScreenCityPickerTable(private val cityScreen: CityScreen) : Table() {
nextCityButton.onClick { cityScreen.page(1) }
add(nextCityButton).pad(10f)
} else add()
pack()
}

View File

@ -85,10 +85,20 @@ class KeyPressDispatcher: HashMap<KeyCharAndCode, (() -> Unit)>() {
// access by KeyCharAndCode
operator fun set(key: KeyCharAndCode, action: () -> Unit) {
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()
}
override fun remove(key: KeyCharAndCode): (() -> Unit)? {
val result = super.remove(key)
if (key == KeyCharAndCode('\r'))
super.remove(KeyCharAndCode('\n'))
if (key == KeyCharAndCode(Input.Keys.BACK))
super.remove(KeyCharAndCode('\u001B'))
checkInstall()
return result
}

View File

@ -1,23 +1,32 @@
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.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Cell
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.scenes.scene2d.ui.*
import com.badlogic.gdx.utils.Align
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
*/
@Suppress("MemberVisibilityCanBePrivate")
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)
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 {
// Set actor name for debugging
name = javaClass.simpleName
background = ImageGetter.getBackground(Color.GRAY.cpy().apply { a=.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
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) {
if (force || !screen.hasOpenPopups()) {
this.isVisible = true
show()
}
screen.stage.addActor(this)
@ -45,17 +54,33 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
center(screen.stage)
}
open fun close() {
remove()
val nextPopup = screen.stage.actors.firstOrNull { it is Popup }
if (nextPopup != null) nextPopup.isVisible = true
/** Subroutine for [open] handles only visibility and [keyPressDispatcher] */
private fun show() {
this.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)
override fun row() = innerTable.row()
/**
* Close this popup and - if any other popups are pending - display the next one.
*/
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()
/**
* 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> {
val label = text.toLabel(fontSize = size)
label.wrap = true
@ -63,25 +88,88 @@ open class Popup(val screen: CameraStageBaseScreen): Table(CameraStageBaseScreen
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() }
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> {
return if (action==null)
addButton(Constants.close) { close() }
else
addButton(Constants.close) { close(); action() }
}
/**
* Sets or retrieves the [Actor] that currently has keyboard focus.
*
* Setting focus on a [TextField] will select all contained text unless a
* [FocusListener][com.badlogic.gdx.scenes.scene2d.utils.FocusListener] cancels the event.
*/
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 }
/** Closes all [Popup]s. */
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
/** @return A [List] of currently active or pending [Popup] screens. */
val CameraStageBaseScreen.popups: List<Popup>
get() = stage.actors.filterIsInstance<Popup>()

View File

@ -1,12 +1,28 @@
package com.unciv.ui.utils
import com.badlogic.gdx.Input
import com.unciv.UncivGame
class YesNoPopup(question:String, action:()->Unit,
screen: CameraStageBaseScreen = UncivGame.Current.worldScreen, restoreDefault:()->Unit = {}) : Popup(screen){
init{
/** Variant of [Popup] pre-populated with one label, plus yes and no buttons
* @param question The text for the label
* @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("No".toTextButton().onClick { close(); restoreDefault() })
add("Yes".toTextButton().onClick { close(); action() })
addButtonInRow("Yes", 'y', yes)
addButtonInRow("No", 'n', no)
keyPressDispatcher['\r'] = yes
keyPressDispatcher[Input.Keys.BACK] = no
}
}