Key bindings Step 3 - Better UI (#8891)

* Configurable key bindings - better Widget step 2

* Configurable key bindings - step 2 updated help
This commit is contained in:
SomeTroglodyte
2023-03-15 08:05:50 +01:00
committed by GitHub
parent f1c891252e
commit aab975a61b
5 changed files with 234 additions and 93 deletions

View File

@ -0,0 +1,69 @@
package com.unciv.ui.components
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
import com.badlogic.gdx.utils.Align
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.images.ImageGetter
/** A button that captures keyboard keys and reports them through [onKeyHit] */
class KeyCapturingButton(
private val onKeyHit: (keyCode: Int, control: Boolean) -> Unit
) : ImageButton(getStyle()) {
companion object {
private const val buttonSize = 36f
private const val buttonImage = "OtherIcons/Keyboard"
private val controlKeys = setOf(Input.Keys.CONTROL_LEFT, Input.Keys.CONTROL_RIGHT)
private fun getStyle() = ImageButtonStyle().apply {
val image = ImageGetter.getDrawable(buttonImage)
imageUp = image
imageOver = image.tint(Color.LIME)
}
}
private var savedFocus: Actor? = null
init {
setSize(buttonSize, buttonSize)
addTooltip("Hit the desired key now", 18f, targetAlign = Align.bottomRight)
addListener(ButtonListener(this))
}
private class ButtonListener(private val myButton: KeyCapturingButton) : ClickListener() {
private var controlDown = false
override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) {
if (myButton.stage == null) return
myButton.savedFocus = myButton.stage.keyboardFocus
myButton.stage.keyboardFocus = myButton
}
override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) {
if (myButton.stage == null) return
myButton.stage.keyboardFocus = myButton.savedFocus
myButton.savedFocus = null
}
override fun keyDown(event: InputEvent?, keycode: Int): Boolean {
if (keycode == Input.Keys.ESCAPE) return false
if (keycode in controlKeys) {
controlDown = true
} else {
myButton.onKeyHit(keycode, controlDown)
}
return true
}
override fun keyUp(event: InputEvent?, keycode: Int): Boolean {
if (keycode == Input.Keys.ESCAPE) return false
if (keycode in controlKeys)
controlDown = false
return true
}
}
}

View File

@ -0,0 +1,140 @@
package com.unciv.ui.components
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener
import com.unciv.models.translations.tr
import com.unciv.ui.screens.basescreen.BaseScreen
import com.badlogic.gdx.utils.Array as GdxArray
/**
* A Widget to allow selecting keys from a [SelectBox].
*
* Added value:
* * Easier change callback as direct [changeCallback] parameter
* * Pulls existing keys from Gdx.[Input.Keys] with an exclusion parameter
* * Key names made translatable as e.g. `Key-"A"` or `Key-Space`
* * Supports TranslationFileWriter by providing those translation keys
*/
// Note this has similarities with TranslatedSelectBox, but having entries in the translation
// files that consist only of a punctuation character wouldn't be pretty
class KeysSelectBox(
private val default: String = "",
excludeKeys: Set<Int> = defaultExclusions,
private val changeCallback: (() -> Unit)? = null
) : SelectBox<KeysSelectBox.KeysSelectBoxEntry>(BaseScreen.skin) {
class KeysSelectBoxEntry(val name: String) {
val translation = run {
val translationKey = getTranslationKey(name)
val translation = translationKey.tr()
if (translation == translationKey) name else translation
}
override fun toString() = translation
override fun equals(other: Any?) = other is KeysSelectBoxEntry && other.name == this.name
override fun hashCode() = name.hashCode()
}
companion object {
// Gdx.Input.Keys has a keyNames map internally that would serve,
// but it's private and only used for valueOf() - So we do the same here...
private val keyCodeMap = LinkedHashMap<String, Int>(200).apply {
for (code in 0..Input.Keys.MAX_KEYCODE) {
val name = Input.Keys.toString(code) ?: continue
put(name, code)
}
}
val defaultExclusions = setOf(
Input.Keys.UNKNOWN, // Any key GLFW or Gdx fail to translate fire this - and we assign our own meaning
Input.Keys.ESCAPE, // Married to Android Back, we want to keep this closing the Options
Input.Keys.CONTROL_LEFT, // Captured to support control-letter combos
Input.Keys.CONTROL_RIGHT,
Input.Keys.PICTSYMBOLS, // Ugly toString and unknown who could have such a key
Input.Keys.SWITCH_CHARSET,
Input.Keys.SYM,
Input.Keys.PRINT_SCREEN, // Not captured by Gdx by default, these are handled by the OS
Input.Keys.VOLUME_UP,
Input.Keys.VOLUME_DOWN,
Input.Keys.MUTE,
)
fun getKeyNames(excludeKeys: Set<Int>): Sequence<String> {
val keyCodeNames = keyCodeMap.asSequence()
.filterNot { it.value in excludeKeys }
.map { it.key }
val controlNames = ('A'..'Z').asSequence()
.map { "Ctrl-$it" }
return (keyCodeNames + controlNames)
.sortedWith(
compareBy<String> {
when {
it.length == 1 -> 0 // Characters first
it.length in 2..3 && it[0] == 'F' && it[1].isDigit() ->
it.drop(1).toInt() // then F-Keys numerically
it.startsWith("Ctrl-") -> 100 // Then Ctrl-*
else -> 999 // Rest last
}
}.thenBy { it } // should be by translated, but - later
)
}
private fun getTranslationKey(keyName: String): String {
val noSpaces = keyName.replace(' ','-')
if (noSpaces.last().isLetterOrDigit()) return "Key-$noSpaces"
return "Key-${noSpaces.dropLast(1)}\"${noSpaces.last()}\""
}
@Suppress("unused") // TranslationFileWriter integration will be done later
fun getAllTranslationKeys() = getKeyNames(defaultExclusions).map { getTranslationKey(it) }
}
private val normalStyle: SelectBoxStyle
private val defaultStyle: SelectBoxStyle
init {
//TODO:
// * Dropdown doesn't listen to mouse wheel until after some focus setting click
// (like any other SelectBox, but here it's more noticeable)
items = GdxArray<KeysSelectBoxEntry>(200).apply {
for (name in getKeyNames(excludeKeys))
add(KeysSelectBoxEntry(name))
}
setSelected(default)
maxListCount = 12 // or else the dropdown will fill as much vertical space as it can, including upwards
addListener(object : ChangeListener() {
override fun changed(event: ChangeEvent?, actor: Actor?) {
setColorForDefault()
changeCallback?.invoke()
}
})
// clone style to prevent bleeding our changes to other Widgets
normalStyle = SelectBoxStyle(style)
// Another instance of SelectBoxStyle for displaying whether the selection is the default one
defaultStyle = SelectBoxStyle(normalStyle)
defaultStyle.fontColor = Color.GRAY.cpy()
setColorForDefault()
}
fun setSelected(name: String) {
val newSelection = items.firstOrNull { it.name == name }
?: return
selected = newSelection
}
/** Update fontColor to show whether the selection is the default one (as grayed) */
private fun setColorForDefault() {
// See Javadoc of underlying style - setStyle is needed to update visually
@Suppress("UsePropertyAccessSyntax")
setStyle(if (selected.name == default) defaultStyle else normalStyle)
}
}

View File

@ -1,29 +1,21 @@
package com.unciv.ui.popups.options
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent
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.scenes.scene2d.utils.ClickListener
import com.badlogic.gdx.utils.Align
import com.unciv.GUI
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.KeyCapturingButton
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardBinding
import com.unciv.ui.components.KeyboardBindings
import com.unciv.ui.components.KeysSelectBox
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.civilopediascreen.MarkupRenderer
class KeyBindingsTab(
optionsPopup: OptionsPopup,
labelWidth: Float
@ -75,114 +67,56 @@ class KeyBindingsTab(
save()
}
/** A button that captures keyboard keys and reports them through [onKeyHit] */
class KeyCapturingButton(
private val onKeyHit: (keyCode: Int, control: Boolean) -> Unit
) : ImageButton(getStyle()) {
companion object {
private const val buttonSize = 36f
private const val buttonImage = "OtherIcons/Keyboard"
private val controlKeys = setOf(Input.Keys.CONTROL_LEFT, Input.Keys.CONTROL_RIGHT)
private fun getStyle() = ImageButtonStyle().apply {
val image = ImageGetter.getDrawable(buttonImage)
imageUp = image
imageOver = image.tint(Color.LIME)
}
}
private var savedFocus: Actor? = null
init {
setSize(buttonSize, buttonSize)
addTooltip("Hit the desired key now", 18f, targetAlign = Align.bottomRight)
addListener(ButtonListener(this))
}
class ButtonListener(private val myButton: KeyCapturingButton) : ClickListener() {
private var controlDown = false
override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) {
if (myButton.stage == null) return
myButton.savedFocus = myButton.stage.keyboardFocus
myButton.stage.keyboardFocus = myButton
}
override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) {
if (myButton.stage == null) return
myButton.stage.keyboardFocus = myButton.savedFocus
myButton.savedFocus = null
}
override fun keyDown(event: InputEvent?, keycode: Int): Boolean {
if (keycode == Input.Keys.ESCAPE) return false
if (keycode in controlKeys) {
controlDown = true
} else {
myButton.onKeyHit(keycode, controlDown)
}
return true
}
override fun keyUp(event: InputEvent?, keycode: Int): Boolean {
if (keycode == Input.Keys.ESCAPE) return false
if (keycode in controlKeys)
controlDown = false
return true
}
}
}
class KeyboardBindingWidget(
/** The specific binding to edit */
private val binding: KeyboardBinding
) : Table(BaseScreen.skin) {
private val textField: TextField =
UncivTextField.create(binding.defaultKey.toString()) { focused ->
if (!focused) validateText()
}
private val selectBox = KeysSelectBox(binding.defaultKey.toString()) {
validateSelection()
}
private val button = KeyCapturingButton { code, control ->
selectBox.hideScrollPane()
boundKey = if (control)
KeyCharAndCode.ctrlFromCode(code)
else KeyCharAndCode(code)
resetText()
resetSelection()
}
private var boundKey: KeyCharAndCode? = null
private var boundKey: KeyCharAndCode = binding.defaultKey
init {
pad(0f)
defaults().pad(0f)
textField.setScale(0.1f)
add(textField)
addActor(button)
add(selectBox)
add(button).size(36f).padLeft(2f)
}
/** Get the (untranslated) key name selected by the Widget */
// we let the KeysSelectBox handle undesired mappings
val text: String
get() = textField.text
get() = selectBox.selected.name
/** Update control to show current binding */
fun update(keyBindings: KeyboardBindings) {
boundKey = keyBindings[binding]
resetText()
// Since the TextField itself is temporary, this is only quick & dirty
button.setPosition(textField.width - (textField.height - button.height) / 2, textField.height / 2, Align.right)
resetSelection()
}
private fun validateText() {
/** Set boundKey from selectBox */
private fun validateSelection() {
val value = text
val parsedKey = KeyCharAndCode.parse(value)
if (parsedKey == KeyCharAndCode.UNKNOWN) {
resetText()
resetSelection()
} else {
boundKey = parsedKey
}
}
private fun resetText() {
if (boundKey == binding.defaultKey) boundKey = null
textField.text = boundKey?.toString() ?: ""
/** Set selectBox from boundKey */
private fun resetSelection() {
selectBox.setSelected(boundKey.toString())
}
}
}

View File

@ -16,7 +16,6 @@ import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.utils.DebugUtils
import com.unciv.utils.concurrency.Concurrency
import com.unciv.utils.concurrency.withGLContext
import kotlin.reflect.KMutableProperty0