KeyPressManager now manages listener (and cityscreen usage) (#3966)

* KeyPressManager now manages listener (and cityscreen usage)

* KeyPressManager now manages listener - patch 1
This commit is contained in:
SomeTroglodyte 2021-05-19 23:25:31 +02:00 committed by GitHub
parent 3e3bda42e5
commit 591087ec25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 53 deletions

View File

@ -2,8 +2,6 @@ package com.unciv.ui.cityscreen
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
@ -19,7 +17,6 @@ import com.unciv.ui.utils.AutoScrollPane as ScrollPane
class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
var selectedTile: TileInfo? = null var selectedTile: TileInfo? = null
var selectedConstruction: IConstruction? = null var selectedConstruction: IConstruction? = null
var keyListener: InputListener? = null
/** Toggles or adds/removes all state changing buttons */ /** Toggles or adds/removes all state changing buttons */
val canChangeState = UncivGame.Current.worldScreen.canChangeState val canChangeState = UncivGame.Current.worldScreen.canChangeState
@ -75,8 +72,8 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
stage.addActor(cityInfoTable) stage.addActor(cityInfoTable)
update() update()
keyListener = getKeyboardListener() keyPressDispatcher[Input.Keys.LEFT] = { page(-1) }
stage.addListener(keyListener) keyPressDispatcher[Input.Keys.RIGHT] = { page(1) }
} }
internal fun update() { internal fun update() {
@ -238,7 +235,6 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
} }
fun exit() { fun exit() {
stage.removeListener(keyListener)
game.setWorldScreen() game.setWorldScreen()
game.worldScreen.mapHolder.setCenterPosition(city.location) game.worldScreen.mapHolder.setCenterPosition(city.location)
game.worldScreen.bottomUnitTable.selectUnit() game.worldScreen.bottomUnitTable.selectUnit()
@ -250,23 +246,10 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
if (numCities == 0) return if (numCities == 0) return
val indexOfCity = civInfo.cities.indexOf(city) val indexOfCity = civInfo.cities.indexOf(city)
val indexOfNextCity = (indexOfCity + delta + numCities) % numCities val indexOfNextCity = (indexOfCity + delta + numCities) % numCities
// not entirely sure this is necessary, since we're changing screens we're changing stages as well?
stage.removeListener(keyListener)
val newCityScreen = CityScreen(civInfo.cities[indexOfNextCity]) val newCityScreen = CityScreen(civInfo.cities[indexOfNextCity])
newCityScreen.showConstructionsTable = showConstructionsTable // stay on stats drilldown between cities newCityScreen.showConstructionsTable = showConstructionsTable // stay on stats drilldown between cities
newCityScreen.update() newCityScreen.update()
game.setScreen(newCityScreen) game.setScreen(newCityScreen)
} }
private fun getKeyboardListener(): InputListener = object : InputListener() {
override fun keyDown(event: InputEvent?, keyCode: Int): Boolean {
if (event == null) return super.keyDown(event, keyCode)
when (event.keyCode) {
Input.Keys.LEFT -> page(-1)
Input.Keys.RIGHT -> page(1)
else -> return super.keyDown(event, keyCode)
}
return true
}
}
} }

View File

@ -37,22 +37,7 @@ open class CameraStageBaseScreen : Screen {
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */ /** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
stage = Stage(ExtendViewport(height, height), SpriteBatch()) stage = Stage(ExtendViewport(height, height), SpriteBatch())
stage.addListener( keyPressDispatcher.install(stage, this.javaClass.simpleName) { hasOpenPopups() }
object : InputListener() {
override fun keyTyped(event: InputEvent?, character: Char): Boolean {
val key = KeyCharAndCode(event, character)
if (key !in keyPressDispatcher || hasOpenPopups())
return super.keyTyped(event, character)
//try-catch mainly for debugging. Breakpoints in the vicinity can make the event fire twice in rapid succession, second time the context can be invalid
try {
keyPressDispatcher[key]?.invoke()
} catch (ex: Exception) {}
return true
}
}
)
} }
override fun show() {} override fun show() {}
@ -75,7 +60,9 @@ open class CameraStageBaseScreen : Screen {
override fun hide() {} override fun hide() {}
override fun dispose() {} override fun dispose() {
keyPressDispatcher.uninstall()
}
fun displayTutorial(tutorial: Tutorial, test: (() -> Boolean)? = null) { fun displayTutorial(tutorial: Tutorial, test: (() -> Boolean)? = null) {
if (!game.settings.showTutorials) return if (!game.settings.showTutorials) return
@ -107,19 +94,9 @@ open class CameraStageBaseScreen : Screen {
internal var batch: Batch = SpriteBatch() internal var batch: Batch = SpriteBatch()
} }
/** It returns the assigned [InputListener] */ fun onBackButtonClicked(action: () -> Unit) {
fun onBackButtonClicked(action: () -> Unit): InputListener { keyPressDispatcher[Input.Keys.BACK] = action
val listener = object : InputListener() { keyPressDispatcher['\u001B'] = action
override fun keyDown(event: InputEvent?, keycode: Int): Boolean {
if (keycode == Input.Keys.BACK || keycode == Input.Keys.ESCAPE) {
action()
return true
}
return false
}
}
stage.addListener(listener)
return listener
} }
fun isPortrait() = stage.viewport.screenHeight > stage.viewport.screenWidth fun isPortrait() = stage.viewport.screenHeight > stage.viewport.screenWidth

View File

@ -1,7 +1,10 @@
package com.unciv.ui.utils package com.unciv.ui.utils
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.scenes.scene2d.EventListener
import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.Stage
import java.util.HashMap import java.util.HashMap
/* /*
@ -31,19 +34,37 @@ data class KeyCharAndCode(val char: Char, val code: Int) {
if (character == Char.MIN_VALUE && event!=null) event.keyCode else 0 if (character == Char.MIN_VALUE && event!=null) event.keyCode else 0
) )
@ExperimentalStdlibApi // From Kotlin 1.5 on the Ctrl- line will need Char(char.code+64)
// see https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/char-int-conversions.md
override fun toString(): String { override fun toString(): String {
// debug helper // debug helper
return when { return when {
char == Char.MIN_VALUE -> Input.Keys.toString(code) char == Char.MIN_VALUE -> Input.Keys.toString(code)
char < ' ' -> "Ctrl-" + Char(char.toInt()+64) char < ' ' -> "Ctrl-" + (char.toInt()+64).toChar()
else -> "\"$char\"" else -> "\"$char\""
} }
} }
} }
/** A manager for a [keyTyped][InputListener.keyTyped] [InputListener], based on [HashMap].
* Uses [KeyCharAndCode] as keys to express bindings for both Ascii and function keys.
*
* [install] and [uninstall] handle adding the listener to a [Stage].
* Use indexed assignments to react to specific keys, e.g.:
* ```
* keyPressDispatcher[Input.Keys.F1] = { showHelp() }
* keyPressDispatcher['+'] = { zoomIn() }
* ```
* Optionally use [setCheckpoint] and [revertToCheckPoint] to remember and restore one state.
*/
class KeyPressDispatcher: HashMap<KeyCharAndCode, (() -> Unit)>() { class KeyPressDispatcher: HashMap<KeyCharAndCode, (() -> Unit)>() {
private var checkpoint: Set<KeyCharAndCode> = setOf() private var checkpoint: Set<KeyCharAndCode> = setOf() // set of keys marking a checkpoint
private var listener: EventListener? = null // holds listener code, captures its params in install() function
private var listenerInstalled = false // flag for lazy Stage.addListener()
private var installStage: Stage? = null // Keep stage passed by install() for lazy addListener and uninstall
var name: String? = null // optional debug label
private set
// access by Char // access by Char
operator fun get(char: Char) = this[KeyCharAndCode(char)] operator fun get(char: Char) = this[KeyCharAndCode(char)]
@ -61,14 +82,98 @@ class KeyPressDispatcher: HashMap<KeyCharAndCode, (() -> Unit)>() {
operator fun contains(code: Int) = this.contains(KeyCharAndCode(code)) operator fun contains(code: Int) = this.contains(KeyCharAndCode(code))
fun remove(code: Int) = this.remove(KeyCharAndCode(code)) fun remove(code: Int) = this.remove(KeyCharAndCode(code))
// access by KeyCharAndCode
operator fun set(key: KeyCharAndCode, action: () -> Unit) {
super.put(key, action)
checkInstall()
}
override fun remove(key: KeyCharAndCode): (() -> Unit)? {
val result = super.remove(key)
checkInstall()
return result
}
override fun toString(): String {
return (if (name==null) "" else "$name.") +
"KeyPressDispatcher(" + keys.joinToString(limit = 6){ it.toString() } + ")"
}
/** Removes all of the mappings, including a checkpoint if set. */
override fun clear() { override fun clear() {
checkpoint = setOf() checkpoint = setOf()
super.clear() super.clear()
checkInstall()
} }
/** Set a checkpoint: The current set of keys will not be removed on a subsequent [revertToCheckPoint] */
fun setCheckpoint() { fun setCheckpoint() {
checkpoint = keys.toSet() checkpoint = keys.toSet()
} }
/** Revert to a checkpoint: Remove all mappings except those that existed on a previous [setCheckpoint] call.
* If no checkpoint has been set, this is equivalent to [clear] */
fun revertToCheckPoint() { fun revertToCheckPoint() {
keys.minus(checkpoint).forEach { remove(it) } keys.minus(checkpoint).forEach { remove(it) }
checkInstall()
} }
/** install our [EventListener] on a stage with optional inhibitor
* @param stage The [Stage] to add the listener to
* @param checkIgnoreKeys An optional lambda - when it returns true all keys are ignored
*/
fun install(stage: Stage, name: String? = null, checkIgnoreKeys: (() -> Boolean)? = null) {
this.name = name
if (installStage != null) uninstall()
listener =
object : InputListener() {
override fun keyTyped(event: InputEvent?, character: Char): Boolean {
val key = KeyCharAndCode(event, character)
// see if we want to handle this key, and if not, let it propagate
if (!contains(key) || (checkIgnoreKeys?.invoke() == true))
return super.keyTyped(event, character)
//try-catch mainly for debugging. Breakpoints in the vicinity can make the event fire twice in rapid succession, second time the context can be invalid
try {
this@KeyPressDispatcher[key]?.invoke()
} catch (ex: Exception) {}
return true
}
}
installStage = stage
checkInstall()
}
/** uninstall our [EventListener] from the stage it was installed on. */
fun uninstall() {
checkInstall(forceRemove = true)
listener = null
installStage = null
}
/** Implements lazy hooking of the listener into the stage.
*
* The listener will be added to the stage's listeners only when - and as soon as -
* [this][KeyPressDispatcher] contains mappings.
* When all mappings are removed or cleared the listener is removed from the stage.
*/
private fun checkInstall(forceRemove: Boolean = false) {
if (listener == null || installStage == null) return
if (listenerInstalled && (isEmpty() || isPaused || forceRemove)) {
println(toString() + ": Removing listener" + (if(forceRemove) " for uninstall" else ""))
listenerInstalled = false
installStage!!.removeListener(listener)
} else if (!listenerInstalled && !(isEmpty() || isPaused)) {
println(toString() + ": Adding listener")
installStage!!.addListener(listener)
listenerInstalled = true
}
}
/** Allows temporarily suspending this [KeyPressDispatcher] */
var isPaused: Boolean = false
set(value) {
field = value
checkInstall()
}
} }