mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-11 11:28:03 +07:00
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:
parent
f4dca2281e
commit
10caf8e93e
@ -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
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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() ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user