diff --git a/core/src/com/unciv/GUI.kt b/core/src/com/unciv/GUI.kt index 4be6dbb7ca..953fca3b9c 100644 --- a/core/src/com/unciv/GUI.kt +++ b/core/src/com/unciv/GUI.kt @@ -77,7 +77,7 @@ object GUI { } 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 get() { // defer decision if Gdx.input not yet initialized diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt index 37172f1072..965bf7a506 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsoleCommand.kt @@ -86,6 +86,10 @@ internal class ConsoleCommandRoot : ConsoleCommandNode { "unit" to ConsoleUnitCommands(), "city" to ConsoleCityCommands(), "tile" to ConsoleTileCommands(), - "civ" to ConsoleCivCommands() + "civ" to ConsoleCivCommands(), + "history" to ConsoleAction("history") { console, _ -> + console.showHistory() + DevConsoleResponse.hint("") // Trick console into staying open + } ) } diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt index 474b357092..9ab823fa40 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt @@ -2,15 +2,24 @@ package com.unciv.ui.screens.devconsole import com.badlogic.gdx.Input 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.ui.Label +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Scaling import com.unciv.Constants +import com.unciv.GUI import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapunit.MapUnit 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.toLabel import com.unciv.ui.components.input.KeyCharAndCode 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.screens.devconsole.CliInput.Companion.splitToCliInput 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 responseLabel = "".toLabel(Color.RED).apply { wrap = true } + private val inputWrapper = Table() private val commandRoot = ConsoleCommandRoot() internal val gameInfo = screen.gameInfo @@ -34,12 +44,16 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { init { // Use untranslated text here! The entire console, including messages, should stay English. // 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). - add("Developer Console {}".toLabel(fontSize = Constants.headingFontSize)).growX() // translation template is automatic via the keybinding + // The extensions still help, even with a "don't translate" kludge + add("Developer Console {}".toLabel(fontSize = Constants.headingFontSize)).growX() add("Keep open {}".toCheckBox(keepOpen) { keepOpen = it }).right().row() - add(textField).width(stageToShowOn.width / 2).colspan(2).row() - textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter) + inputWrapper.defaults().space(5f) + 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" 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() } clickBehindToClose = true - textField.keyShortcuts.add(KeyCharAndCode.TAB) { - getAutocomplete()?.also { - 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) - } - } + textField.keyShortcuts.add(Input.Keys.ENTER, ::onEnter) + textField.keyShortcuts.add(KeyCharAndCode.TAB, ::onAutocomplete) keyShortcuts.add(Input.Keys.UP) { navigateHistory(-1) } keyShortcuts.add(Input.Keys.DOWN) { navigateHistory(1) } @@ -65,6 +74,35 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { 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) { if (history.isEmpty()) return currentHistoryEntry = (currentHistoryEntry + delta).coerceIn(history.indices) @@ -72,6 +110,29 @@ class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { 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() { val handleCommandResponse = handleCommand() if (handleCommandResponse.isOK) {