diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index a2864af862..a1d473b5dc 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -128,6 +128,11 @@ class GameSettings { /** Size of automatic display of UnitSet art in Civilopedia - 0 to disable */ var pediaUnitArtSize = 0f + /** Don't close developer console after a successful command */ + var keepConsoleOpen = false + /** Persist the history of successful developer console commands */ + val consoleCommandHistory = ArrayList() + /** used to migrate from older versions of the settings */ var version: Int? = null diff --git a/core/src/com/unciv/ui/components/UncivTextField.kt b/core/src/com/unciv/ui/components/UncivTextField.kt index 3380c50f60..867cf383bb 100644 --- a/core/src/com/unciv/ui/components/UncivTextField.kt +++ b/core/src/com/unciv/ui/components/UncivTextField.kt @@ -17,6 +17,7 @@ import com.unciv.ui.components.extensions.getOverlap import com.unciv.ui.components.extensions.right import com.unciv.ui.components.extensions.stageBoundingBox import com.unciv.ui.components.extensions.top +import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.popups.Popup import com.unciv.ui.screens.basescreen.BaseScreen @@ -42,7 +43,12 @@ object UncivTextField { */ fun create(hint: String, preEnteredText: String = "", onFocusChange: (TextField.(Boolean) -> Unit)? = null): TextField { @Suppress("UNCIV_RAW_TEXTFIELD") - val textField = TextField(preEnteredText, BaseScreen.skin) + val textField = object : TextField(preEnteredText, BaseScreen.skin) { + override fun next(up: Boolean) { + if (KeyCharAndCode.TAB in keyShortcuts) return + super.next(up) + } + } val translatedHint = hint.tr() textField.messageText = translatedHint textField.addListener(object : FocusListener() { diff --git a/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt b/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt index a3e1bd01ea..bafe196f43 100644 --- a/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt +++ b/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt @@ -61,6 +61,15 @@ open class KeyShortcutDispatcher { shortcuts.removeAll { it.shortcut.key.code == keyCode } } + operator fun contains(binding: KeyboardBinding) = + shortcuts.any { it.shortcut.binding == binding } + operator fun contains(key: KeyCharAndCode) = + shortcuts.any { it.shortcut.key == key || it.shortcut.binding.defaultKey == key } + operator fun contains(char: Char) = + shortcuts.any { it.shortcut.key.char == char } + operator fun contains(keyCode: Int) = + shortcuts.any { it.shortcut.key.code == keyCode } + open fun isActive(): Boolean = true diff --git a/core/src/com/unciv/ui/popups/Popup.kt b/core/src/com/unciv/ui/popups/Popup.kt index 1beb840213..b5a1e9e212 100644 --- a/core/src/com/unciv/ui/popups/Popup.kt +++ b/core/src/com/unciv/ui/popups/Popup.kt @@ -68,8 +68,8 @@ open class Popup( */ enum class Scrollability { None, All, WithoutButtons } - private val maxPopupWidth = stageToShowOn.width * maxSizePercentage - private val maxPopupHeight = stageToShowOn.height * maxSizePercentage + protected val maxPopupWidth = stageToShowOn.width * maxSizePercentage + protected val maxPopupHeight = stageToShowOn.height * maxSizePercentage /** This exists to differentiate the actual popup (this table) * from the 'screen blocking' part of the popup (which covers the entire screen). diff --git a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt index ab29a7a142..5942af4b34 100644 --- a/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt +++ b/core/src/com/unciv/ui/screens/devconsole/DevConsolePopup.kt @@ -3,81 +3,100 @@ package com.unciv.ui.screens.devconsole import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.actions.Actions -import com.badlogic.gdx.scenes.scene2d.ui.TextField +import com.unciv.Constants import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.ui.components.UncivTextField +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.popups.Popup -import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.worldscreen.WorldScreen class DevConsolePopup(val screen: WorldScreen) : Popup(screen) { companion object { - val history = ArrayList() + private const val maxHistorySize = 42 } + private val history by screen.game.settings::consoleCommandHistory + private var keepOpen by screen.game.settings::keepConsoleOpen + private var currentHistoryEntry = history.size - private val textField = TextField("", BaseScreen.skin) + 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 commandRoot = ConsoleCommandRoot() internal val gameInfo = screen.gameInfo init { - add(textField).width(stageToShowOn.width / 2).row() + // 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 + 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) - // Without this, console popup will always contain a ` + // 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 = "" })) - add(responseLabel).maxWidth(screen.stage.width * 0.8f) - - open(true) + add(responseLabel).colspan(2).maxWidth(screen.stage.width * 0.8f) keyShortcuts.add(KeyCharAndCode.BACK) { close() } + clickBehindToClose = true - keyShortcuts.add(KeyCharAndCode.TAB) { + textField.keyShortcuts.add(KeyCharAndCode.TAB) { val textToAdd = getAutocomplete() textField.appendText(textToAdd) } - if (history.isNotEmpty()) { - keyShortcuts.add(Input.Keys.UP) { - if (currentHistoryEntry > 0) currentHistoryEntry-- - textField.text = history[currentHistoryEntry] - textField.cursorPosition = textField.text.length - } - keyShortcuts.add(Input.Keys.DOWN) { - if (currentHistoryEntry == history.size) currentHistoryEntry-- - if (currentHistoryEntry < history.lastIndex) currentHistoryEntry++ - textField.text = history[currentHistoryEntry] - textField.cursorPosition = textField.text.length - } - } + keyShortcuts.add(Input.Keys.UP) { navigateHistory(-1) } + keyShortcuts.add(Input.Keys.DOWN) { navigateHistory(1) } + + open(true) screen.stage.keyboardFocus = textField } + private fun navigateHistory(delta: Int) { + if (history.isEmpty()) return + currentHistoryEntry = (currentHistoryEntry + delta).coerceIn(history.indices) + textField.text = history[currentHistoryEntry] + textField.cursorPosition = textField.text.length + } + private fun onEnter() { val handleCommandResponse = handleCommand() if (handleCommandResponse.isOK) { screen.shouldUpdate = true - if (history.isEmpty() || history.last() != textField.text) - history.add(textField.text) - close() + addHistory() + if (!keepOpen) close() else textField.text = "" return } showResponse(handleCommandResponse.message, handleCommandResponse.color) } + private fun addHistory() { + val text = textField.text + if (text.isBlank()) return + if (history.isNotEmpty() && history.last().equals(text, true)) return + if (history.size >= maxHistorySize) { + history.removeAll { it.equals(text, true) } + if (history.size >= maxHistorySize) + history.removeAt(0) + } + history.add(textField.text) + currentHistoryEntry = history.size + } + internal fun showResponse(message: String?, color: Color) { responseLabel.setText(message) responseLabel.style.fontColor = color } - val splitStringRegex = Regex("\"([^\"]+)\"|\\S+") // Read: "(phrase)" OR non-whitespace + private val splitStringRegex = Regex("\"([^\"]+)\"|\\S+") // Read: "(phrase)" OR non-whitespace private fun getParams(text: String): List { return splitStringRegex.findAll(text).map { it.value.removeSurrounding("\"") }.filter { it.isNotEmpty() }.toList() } diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index 7c6e0e71c7..f57eec7248 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -21,7 +21,6 @@ import com.unciv.logic.multiplayer.storage.MultiplayerAuthException import com.unciv.logic.trade.TradeEvaluation import com.unciv.models.TutorialTrigger import com.unciv.models.metadata.GameSetupInfo -import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.extensions.centerX @@ -38,7 +37,6 @@ import com.unciv.ui.popups.ToastPopup import com.unciv.ui.popups.hasOpenPopups import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.cityscreen.CityScreen -import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen import com.unciv.ui.screens.devconsole.DevConsolePopup import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.newgamescreen.NewGameScreen @@ -285,11 +283,13 @@ class WorldScreen( globalShortcuts.add(KeyboardBinding.ToggleWorkedTilesDisplay) { minimapWrapper.populationImageButton.toggle() } globalShortcuts.add(KeyboardBinding.ToggleMovementDisplay) { minimapWrapper.movementsImageButton.toggle() } - globalShortcuts.add(KeyboardBinding.DeveloperConsole) { - // No cheating unless you're by yourself - if (gameInfo.civilizations.count { it.isHuman() } > 1) return@add - val consolePopup = DevConsolePopup(this) - } + globalShortcuts.add(KeyboardBinding.DeveloperConsole, action = ::openDeveloperConsole) + } + + fun openDeveloperConsole() { + // No cheating unless you're by yourself + if (gameInfo.civilizations.count { it.isHuman() } > 1) return + val consolePopup = DevConsolePopup(this) } private fun toggleUI() { diff --git a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt index 206bf6010e..415e28c07e 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt @@ -1,43 +1,67 @@ package com.unciv.ui.screens.worldscreen.mainmenu -import com.unciv.UncivGame +import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.onLongPress import com.unciv.ui.popups.Popup -import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen import com.unciv.ui.screens.savescreens.LoadGameScreen import com.unciv.ui.screens.victoryscreen.VictoryScreen import com.unciv.ui.screens.worldscreen.WorldScreen -class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, scrollable = Scrollability.All) { +/** The in-game menu called from the "Hamburger" button top-left + * + * Popup automatically opens as soon as it's initialized + */ +class WorldScreenMenuPopup( + val worldScreen: WorldScreen, + expertMode: Boolean = false +) : Popup(worldScreen, scrollable = Scrollability.All) { + private val singleColumn: Boolean + private fun Cell.nextColumn() { + if (!singleColumn && column == 0) return + row() + } + init { worldScreen.autoPlay.stopAutoPlay() defaults().fillX() - addButton("Main menu") { + val showSave = !worldScreen.gameInfo.gameParameters.isOnlineMultiplayer + val showMusic = worldScreen.game.musicController.isMusicAvailable() + val showConsole = showSave && expertMode + val buttonCount = 8 + (if (showSave) 1 else 0) + (if (showMusic) 1 else 0) + (if (showConsole) 1 else 0) + + val emptyPrefHeight = this.prefHeight + val firstCell = addButton("Main menu") { worldScreen.game.goToMainMenu() - }.row() + } + singleColumn = worldScreen.isCrampedPortrait() || + 2 * prefWidth > maxPopupWidth || // Very coarse: Assume width of translated "Main menu" is representative + buttonCount * (prefHeight - emptyPrefHeight) + emptyPrefHeight < maxPopupHeight + firstCell.nextColumn() + addButton("Civilopedia", KeyboardBinding.Civilopedia) { close() worldScreen.openCivilopedia() - }.row() - if (!worldScreen.gameInfo.gameParameters.isOnlineMultiplayer) + }.nextColumn() + if (showSave) addButton("Save game", KeyboardBinding.SaveGame) { close() worldScreen.openSaveGameScreen() - }.row() + }.nextColumn() addButton("Load game", KeyboardBinding.LoadGame) { close() worldScreen.game.pushScreen(LoadGameScreen()) - }.row() + }.nextColumn() addButton("Start new game", KeyboardBinding.NewGame) { close() worldScreen.openNewGameScreen() - }.row() + }.nextColumn() addButton("Victory status", KeyboardBinding.VictoryScreen) { close() worldScreen.game.pushScreen(VictoryScreen(worldScreen)) - }.row() + }.nextColumn() val optionsCell = addButton("Options", KeyboardBinding.Options) { close() worldScreen.openOptionsPopup() @@ -46,17 +70,26 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, sc close() worldScreen.openOptionsPopup(withDebug = true) } - optionsCell.row() + optionsCell.nextColumn() addButton("Community") { close() WorldScreenCommunityPopup(worldScreen).open(force = true) - }.row() - addButton("Music", KeyboardBinding.MusicPlayer) { - close() - WorldScreenMusicPopup(worldScreen).open(force = true) - }.row() + }.nextColumn() + if (showMusic) + addButton("Music", KeyboardBinding.MusicPlayer) { + close() + WorldScreenMusicPopup(worldScreen).open(force = true) + }.nextColumn() - addCloseButton() + if (showConsole) + addButton("Developer Console", KeyboardBinding.DeveloperConsole) { + close() + worldScreen.openDeveloperConsole() + }.nextColumn() + + addCloseButton().run { colspan(if (singleColumn || column == 1) 1 else 2) } pack() + + open(force = true) } } diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt index 4615cb48aa..1a709a2169 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt @@ -16,10 +16,9 @@ import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick +import com.unciv.ui.components.input.onRightClick import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen -import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories -import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories import com.unciv.ui.screens.worldscreen.BackgroundActor import com.unciv.ui.screens.worldscreen.WorldScreen @@ -212,9 +211,8 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() { padTop((10f - descenderHeight).coerceAtLeast(0f)) menuButton.color = Color.WHITE - menuButton.onActivation(binding = KeyboardBinding.Menu) { - WorldScreenMenuPopup(worldScreen).open(force = true) - } + menuButton.onActivation(binding = KeyboardBinding.Menu) { WorldScreenMenuPopup(worldScreen) } + menuButton.onRightClick { WorldScreenMenuPopup(worldScreen, true) } val onNationClick = { worldScreen.openCivilopedia(worldScreen.selectedCiv.nation.makeLink())