Key bindings Step 2 (#8872)

* Configurable key bindings - Enable via Hidden debug-option

* Configurable key bindings - better Help

* Configurable key bindings - better Widget step 1
This commit is contained in:
SomeTroglodyte 2023-03-12 21:45:06 +01:00 committed by GitHub
parent f4dca2281e
commit 10caf8e93e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 22 deletions

View File

@ -390,5 +390,27 @@
"steps": [
"One of your cities is under a naval blockade! When all adjacent water tiles of a coastal city are blocked - city loses harbor connection to all other cities, including capital. Make sure to de-blockade cities by deploying friendly military naval units to fight off invaders."
]
}
},
{
"name": "Keyboard Bindings",
"civilopediaText": [
{"text":"Limitations","header":3},
{"text":"This is a work in progress.","color":"#b22222","starred":true},
{"text":"For technical reasons, only direct keys or Ctrl-Letter combinations can be used.","starred":true},
{"text":"The Escape key is intentionally excluded from being reassigned.","starred":true},
{"text":"Currently, there are no checks to prevent conflicting assignments.","starred":true},
{},
{"text":"Using the Keys page","header":3},
{"text":"Each binding has a label, a text field, and a key button looking like this:"},
{"extraImage":"OtherIcons/Keyboard","imageSize":36},
{"text":"While hovering the mouse over the key button, you can press a desired key directly to assign it."},
{"text":"Alternatively, you can enter the key's name in the text field."},
{"text":"To reset a binding to its default, simply clear the text field."},
{},
{"text":"Bindings mapped to their default keys are displayed in gray, those reassigned by you in white."},
{},
{"text":"For discussion about missing entries, see the linked github issue.","link":"https://github.com/yairm210/Unciv/issues/8862"}
],
// "uniques": ["Will not be displayed in Civilopedia"] // would prevent use for help link
}
]

View File

@ -621,7 +621,14 @@ open class TabbedPager(
if (insertBefore >= 0 && insertBefore < pages.size) {
newIndex = insertBefore
pages.add(insertBefore, page)
header.addActorAt(insertBefore, page.button)
// Table.addActorAt breaks the Table, it's a Group method that updates children but not cells
// So we add an empty cell and move cell actors around
header.add()
for (i in header.cells.size - 1 downTo insertBefore + 1) {
val actor = header.removeActorAt(i - 1, true) as Button
header.cells[i].setActor<Button>(actor)
}
header.cells[insertBefore].setActor<Button>(page.button)
buttonCell = header.getCell(page.button)
} else {
newIndex = pages.size

View File

@ -9,7 +9,6 @@ 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.UncivGame
import com.unciv.logic.event.EventBus
import com.unciv.models.translations.tr
import com.unciv.ui.screens.basescreen.UncivStage
@ -30,7 +29,7 @@ object UncivTextField {
* @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 {
fun create(hint: String, preEnteredText: String = "", onFocusChange: ((Boolean) -> Unit)? = null): TextField {
@Suppress("UNCIV_RAW_TEXTFIELD")
val textField = TextField(preEnteredText, BaseScreen.skin)
val translatedHint = hint.tr()
@ -40,6 +39,7 @@ object UncivTextField {
if (focused) {
textField.scrollAscendantToTextField()
}
onFocusChange?.invoke(focused)
}
})

View File

@ -16,7 +16,9 @@ import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.utils.DebugUtils
fun debugTab() = Table(BaseScreen.skin).apply {
fun debugTab(
optionsPopup: OptionsPopup
) = Table(BaseScreen.skin).apply {
pad(10f)
defaults().pad(5f)
val game = UncivGame.Current
@ -59,6 +61,7 @@ fun debugTab() = Table(BaseScreen.skin).apply {
add("Enable espionage option".toCheckBox(game.settings.enableEspionageOption) {
game.settings.enableEspionageOption = it
}).colspan(2).row()
add("Save games compressed".toCheckBox(UncivFiles.saveZipped) {
UncivFiles.saveZipped = it
}).colspan(2).row()
@ -66,6 +69,13 @@ fun debugTab() = Table(BaseScreen.skin).apply {
MapSaver.saveZipped = it
}).colspan(2).row()
if (GUI.keyboardAvailable) {
add("Show keyboard bindings".toCheckBox(optionsPopup.enableKeyBindingsTab) {
optionsPopup.enableKeyBindingsTab = it
optionsPopup.showOrHideKeyBindings()
}).colspan(2).row()
}
add("Gdx Scene2D debug".toCheckBox(BaseScreen.enableSceneDebug) {
BaseScreen.enableSceneDebug = it
}).colspan(2).row()

View File

@ -1,12 +1,26 @@
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.KeyCharAndCode
import com.unciv.ui.components.KeyboardBinding
import com.unciv.ui.components.KeyboardBindings
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
@ -15,22 +29,25 @@ class KeyBindingsTab(
labelWidth: Float
) : Table(BaseScreen.skin), TabbedPager.IPageExtensions {
private val keyBindings = optionsPopup.settings.keyBindings
private val keyFields = HashMap<KeyboardBinding, TextField>(KeyboardBinding.values().size)
private val keyFields = HashMap<KeyboardBinding, KeyboardBindingWidget>(KeyboardBinding.values().size)
private val disclaimer = MarkupRenderer.render(listOf(
FormattedLine("This is a work in progress.", color = "#b22222", centered = true), // FIREBRICK
FormattedLine(),
// FormattedLine("Do not pester the developers for missing entries!"), // little joke
FormattedLine("For discussion about missing entries, see the linked issue.",
link = "https://github.com/yairm210/Unciv/issues/8862"),
FormattedLine("Please see the Tutorial.", link = "Tutorial/Keyboard Bindings"),
FormattedLine(separator = true),
), labelWidth)
), labelWidth) {
// This ruleset is a kludge - but since OptionPopup can be called from anywhere, getting the relevant one is a chore
//TODO better pedia call architecture, or a tutorial render method once that has markup capability
GUI.pushScreen(CivilopediaScreen(RulesetCache.getVanillaRuleset(), link = it))
}
init {
pad(10f)
defaults().pad(5f)
for (binding in KeyboardBinding.values()) {
keyFields[binding] = UncivTextField.create(binding.defaultKey.toString())
keyFields[binding] = KeyboardBindingWidget(binding)
}
}
@ -40,9 +57,8 @@ class KeyBindingsTab(
for (binding in KeyboardBinding.values()) {
add(binding.label.toLabel())
keyFields[binding]!!.text = if (binding !in keyBindings) "" // show default = hint grayed
else keyBindings[binding].toString()
add(keyFields[binding]).row()
keyFields[binding]!!.update(keyBindings)
}
}
@ -58,4 +74,115 @@ class KeyBindingsTab(
override fun deactivated(index: Int, caption: String, pager: TabbedPager) {
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 button = KeyCapturingButton { code, control ->
boundKey = if (control)
KeyCharAndCode.ctrlFromCode(code)
else KeyCharAndCode(code)
resetText()
}
private var boundKey: KeyCharAndCode? = null
init {
pad(0f)
defaults().pad(0f)
textField.setScale(0.1f)
add(textField)
addActor(button)
}
val text: String
get() = textField.text
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)
}
private fun validateText() {
val value = text
val parsedKey = KeyCharAndCode.parse(value)
if (parsedKey == KeyCharAndCode.UNKNOWN) {
resetText()
} else {
boundKey = parsedKey
}
}
private fun resetText() {
if (boundKey == binding.defaultKey) boundKey = null
textField.text = boundKey?.toString() ?: ""
}
}
}

View File

@ -16,6 +16,7 @@ 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
@ -35,12 +36,19 @@ class OptionsPopup(
val settings = screen.game.settings
val tabs: TabbedPager
val selectBoxMinWidth: Float
private val tabMinWidth: Float
private var keyBindingsTab: KeyBindingsTab? = null
/** Enable the still experimental Keyboard Bindings page in OptionsPopup */
var enableKeyBindingsTab: Boolean = false
//endregion
companion object {
const val defaultPage = 2 // Gameplay
const val keysTabCaption = "Keys"
const val keysTabBeforeCaption = "Advanced"
}
init {
@ -49,7 +57,6 @@ class OptionsPopup(
innerTable.pad(0f)
val tabMaxWidth: Float
val tabMinWidth: Float
val tabMaxHeight: Float
screen.run {
selectBoxMinWidth = if (stage.width < 600f) 200f else 240f
@ -93,26 +100,19 @@ class OptionsPopup(
multiplayerTab(this),
ImageGetter.getImage("OtherIcons/Multiplayer"), 24f
)
tabs.addPage(
"Advanced",
advancedTab(this, ::reloadWorldAndOptions),
ImageGetter.getImage("OtherIcons/Settings"), 24f
)
if (GUI.keyboardAvailable && false) {
keyBindingsTab = KeyBindingsTab(this, tabMinWidth - 40f) // 40 = padding
tabs.addPage(
"Keys", keyBindingsTab,
ImageGetter.getImage("OtherIcons/Keyboard"), 24f
)
}
if (RulesetCache.size > BaseRuleset.values().size) {
val content = ModCheckTab(screen)
tabs.addPage("Locate mod errors", content, ImageGetter.getImage("OtherIcons/Mods"), 24f)
}
if (Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT) && (Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT) || Gdx.input.isKeyPressed(Input.Keys.ALT_RIGHT))) {
tabs.addPage("Debug", debugTab(), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
tabs.addPage("Debug", debugTab(this), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
}
addCloseButton {
@ -122,6 +122,10 @@ class OptionsPopup(
onClose()
}.padBottom(10f)
if (GUI.keyboardAvailable) {
showOrHideKeyBindings() // Do this late because it looks for the page to insert before
}
pack() // Needed to show the background.
center(screen.stage)
}
@ -180,4 +184,22 @@ class OptionsPopup(
}
}
internal fun showOrHideKeyBindings() {
// At the moment, the Key bindings Tab exists only on-demand. To refactor it back to permanent,
// move the `keyBindingsTab =` line and addPage call to before the Advanced Tab creation,
// then delete this function, delete the enableKeyBindingsTab flag and clean up what is flagged by the compiler as missing or unused.
val existingIndex = tabs.getPageIndex(keysTabCaption)
if (enableKeyBindingsTab && existingIndex < 0) {
if (keyBindingsTab == null)
keyBindingsTab = KeyBindingsTab(this, tabMinWidth - 40f) // 40 = padding
val beforeIndex = tabs.getPageIndex(keysTabBeforeCaption)
tabs.addPage(
keysTabCaption, keyBindingsTab,
ImageGetter.getImage("OtherIcons/Keyboard"), 24f,
insertBefore = beforeIndex
)
} else if (!enableKeyBindingsTab && existingIndex >= 0) {
tabs.removePage(existingIndex)
}
}
}