UX: Dev Console easier to use without installing keyboard apps (#11706)

* UI replacements for Tab, Up, Down in DevConsolePopup

* DevConsole history display via command or longpress on the Android UI
This commit is contained in:
SomeTroglodyte
2024-06-08 20:59:33 +02:00
committed by GitHub
parent 68e29e9c53
commit aa74c557d2
3 changed files with 78 additions and 13 deletions

View File

@ -77,7 +77,7 @@ object GUI {
} }
private var keyboardAvailableCache: Boolean? = null private var keyboardAvailableCache: Boolean? = null
/** Tests availability of a physical keyboard */ /** Tests availability of a physical keyboard - cached (connecting a keyboard while the game is running won't be recognized until relaunch) */
val keyboardAvailable: Boolean val keyboardAvailable: Boolean
get() { get() {
// defer decision if Gdx.input not yet initialized // defer decision if Gdx.input not yet initialized

View File

@ -86,6 +86,10 @@ internal class ConsoleCommandRoot : ConsoleCommandNode {
"unit" to ConsoleUnitCommands(), "unit" to ConsoleUnitCommands(),
"city" to ConsoleCityCommands(), "city" to ConsoleCityCommands(),
"tile" to ConsoleTileCommands(), "tile" to ConsoleTileCommands(),
"civ" to ConsoleCivCommands() "civ" to ConsoleCivCommands(),
"history" to ConsoleAction("history") { console, _ ->
console.showHistory()
DevConsoleResponse.hint("") // Trick console into staying open
}
) )
} }

View File

@ -2,15 +2,24 @@ package com.unciv.ui.screens.devconsole
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.Actor
import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Scaling
import com.unciv.Constants import com.unciv.Constants
import com.unciv.GUI
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.ui.components.UncivTextField import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toCheckBox import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.input.onRightClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.devconsole.CliInput.Companion.splitToCliInput import com.unciv.ui.screens.devconsole.CliInput.Companion.splitToCliInput
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
@ -27,6 +36,7 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
private val textField = UncivTextField.create("", "") // always has focus, so a hint won't show private val textField = UncivTextField.create("", "") // always has focus, so a hint won't show
private val responseLabel = "".toLabel(Color.RED).apply { wrap = true } private val responseLabel = "".toLabel(Color.RED).apply { wrap = true }
private val inputWrapper = Table()
private val commandRoot = ConsoleCommandRoot() private val commandRoot = ConsoleCommandRoot()
internal val gameInfo = screen.gameInfo internal val gameInfo = screen.gameInfo
@ -34,12 +44,16 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
init { init {
// Use untranslated text here! The entire console, including messages, should stay English. // Use untranslated text here! The entire console, including messages, should stay English.
// But "Developer Console" *has* a translation from KeyboardBinding.DeveloperConsole. // But "Developer Console" *has* a translation from KeyboardBinding.DeveloperConsole.
// The extensions still help, even with a "don't translate" kludge ("Keep open" has no template but might in the future). // The extensions still help, even with a "don't translate" kludge
add("Developer Console {}".toLabel(fontSize = Constants.headingFontSize)).growX() // translation template is automatic via the keybinding add("Developer Console {}".toLabel(fontSize = Constants.headingFontSize)).growX()
add("Keep open {}".toCheckBox(keepOpen) { keepOpen = it }).right().row() add("Keep open {}".toCheckBox(keepOpen) { keepOpen = it }).right().row()
add(textField).width(stageToShowOn.width / 2).colspan(2).row() inputWrapper.defaults().space(5f)
textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter) if (!GUI.keyboardAvailable) inputWrapper.add(getAutocompleteButton())
inputWrapper.add(textField).growX()
if (!GUI.keyboardAvailable) inputWrapper.add(getHistoryButtons())
add(inputWrapper).minWidth(stageToShowOn.width / 2).growX().colspan(2).row()
// Without this, console popup will always contain the key used to open it - won't work perfectly if it's configured to a "dead key" // Without this, console popup will always contain the key used to open it - won't work perfectly if it's configured to a "dead key"
textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" })) textField.addAction(Actions.delay(0.05f, Actions.run { textField.text = "" }))
@ -49,13 +63,8 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
keyShortcuts.add(KeyCharAndCode.BACK) { close() } keyShortcuts.add(KeyCharAndCode.BACK) { close() }
clickBehindToClose = true clickBehindToClose = true
textField.keyShortcuts.add(KeyCharAndCode.TAB) { textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter)
getAutocomplete()?.also { textField.keyShortcuts.add(KeyCharAndCode.TAB, ::onAutocomplete)
fun String.removeFromEnd(n: Int) = substring(0, (length - n).coerceAtLeast(0))
textField.text = textField.text.removeFromEnd(it.first) + it.second
textField.cursorPosition = Int.MAX_VALUE // because the setText implementation actively resets it after the paste it uses (auto capped at length)
}
}
keyShortcuts.add(Input.Keys.UP) { navigateHistory(-1) } keyShortcuts.add(Input.Keys.UP) { navigateHistory(-1) }
keyShortcuts.add(Input.Keys.DOWN) { navigateHistory(1) } keyShortcuts.add(Input.Keys.DOWN) { navigateHistory(1) }
@ -65,6 +74,35 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
screen.stage.keyboardFocus = textField screen.stage.keyboardFocus = textField
} }
private fun getAutocompleteButton() = ImageGetter.getArrowImage()
.surroundWithCircle(50f, color = Color.DARK_GRAY).onClick(::onAutocomplete)
private fun getHistoryButtons(): Actor {
val group = Table()
fun getArrow(rotation: Float, delta: Int) = ImageGetter.getImage("OtherIcons/ForwardArrow").apply {
name = if (delta > 0) "down" else "up"
setScaling(Scaling.fillX)
setSize(36f, 16f)
scaleX = 0.75f // no idea why this works
setOrigin(18f, 8f)
this.rotation = rotation
onClick {
navigateHistory(delta)
}
}
group.add(getArrow(90f, -1)).size(36f, 16f).padBottom(4f).row()
group.add(getArrow(-90f, 1)).size(36f, 16f)
group.setSize(40f, 40f)
return group.surroundWithCircle(50f, false, Color.DARK_GRAY).onRightClick(action = ::showHistory)
}
private fun onAutocomplete() {
val (toRemove, toAdd) = getAutocomplete() ?: return
fun String.removeFromEnd(n: Int) = substring(0, (length - n).coerceAtLeast(0))
textField.text = textField.text.removeFromEnd(toRemove) + toAdd
textField.cursorPosition = Int.MAX_VALUE // because the setText implementation actively resets it after the paste it uses (auto capped at length)
}
private fun navigateHistory(delta: Int) { private fun navigateHistory(delta: Int) {
if (history.isEmpty()) return if (history.isEmpty()) return
currentHistoryEntry = (currentHistoryEntry + delta).coerceIn(history.indices) currentHistoryEntry = (currentHistoryEntry + delta).coerceIn(history.indices)
@ -72,6 +110,29 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) {
textField.cursorPosition = textField.text.length textField.cursorPosition = textField.text.length
} }
internal fun showHistory() {
if (history.isEmpty()) return
val popup = object : Popup(stageToShowOn) {
init {
for ((index, entry) in history.withIndex()) {
val label = Label(entry, skin)
label.onClick {
currentHistoryEntry = index
navigateHistory(0)
close()
}
add(label).row()
}
clickBehindToClose = true
if (screen.game.settings.forbidPopupClickBehindToClose) addCloseButton()
showListeners.add {
getScrollPane()?.run { scrollY = maxY }
}
}
}
popup.open(true)
}
private fun onEnter() { private fun onEnter() {
val handleCommandResponse = handleCommand() val handleCommandResponse = handleCommand()
if (handleCommandResponse.isOK) { if (handleCommandResponse.isOK) {