diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 2418098941..8ca7e5ab69 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -21,6 +21,7 @@ import com.unciv.ui.newgamescreen.NewGameScreen import com.unciv.ui.pickerscreens.ModManagementScreen import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.utils.* +import com.unciv.ui.utils.StaticTooltip.Companion.addStaticTip import kotlin.concurrent.thread class MainMenuScreen: CameraStageBaseScreen() { @@ -38,6 +39,7 @@ class MainMenuScreen: CameraStageBaseScreen() { text: String, icon: String, key: Char? = null, + keyVisualOnly: Boolean = false, function: () -> Unit ): Table { val table = Table().pad(15f, 30f, 15f, 30f) @@ -49,7 +51,9 @@ class MainMenuScreen: CameraStageBaseScreen() { table.onClick(function) if (key != null) { - keyPressDispatcher[key] = function + if (!keyVisualOnly) + keyPressDispatcher[key] = function + table.addStaticTip(key, 32f) } table.pack() @@ -152,7 +156,8 @@ class MainMenuScreen: CameraStageBaseScreen() { init{ // Using MainMenuScreen.getMenuButton - normally that would place key bindings into the // screen's key dispatcher, but we need them in this Popup's dispatcher instead. - // So we bind the keys separately. + // Thus the crutch with keyVisualOnly, we assign the key binding here but want + // The button to install the tooltip handler anyway. defaults().pad(10f) @@ -164,7 +169,7 @@ class MainMenuScreen: CameraStageBaseScreen() { screen.game.setScreen(newMapScreen) screen.dispose() } - val newMapButton = screen.getMenuButton("New map", "OtherIcons/New", function = newMapAction) + val newMapButton = screen.getMenuButton("New map", "OtherIcons/New", 'n', true, newMapAction) newMapButton.background = tableBackground add(newMapButton).row() keyPressDispatcher['n'] = newMapAction @@ -175,7 +180,7 @@ class MainMenuScreen: CameraStageBaseScreen() { screen.game.setScreen(loadMapScreen) screen.dispose() } - val loadMapButton = screen.getMenuButton("Load map", "OtherIcons/Load", function = loadMapAction) + val loadMapButton = screen.getMenuButton("Load map", "OtherIcons/Load", 'l', true, loadMapAction) loadMapButton.background = tableBackground add(loadMapButton).row() keyPressDispatcher['l'] = loadMapAction diff --git a/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt b/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt index ad35b5647e..f2a4ab9348 100644 --- a/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt +++ b/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt @@ -1,7 +1,5 @@ package com.unciv.ui.overviewscreen -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Group @@ -20,6 +18,8 @@ import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PromotionPickerScreen import com.unciv.ui.trade.DiplomacyScreen import com.unciv.ui.utils.* +import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable +import com.unciv.ui.utils.StaticTooltip.Companion.addStaticTip import java.text.DecimalFormat import kotlin.math.abs import kotlin.math.roundToInt @@ -48,7 +48,6 @@ class EmpireOverviewScreen(private var viewingPlayer:CivilizationInfo, defaultPa // Buttons now hold their old label plus optionally an indicator for the shortcut key. // Implement this templated on UnitActionsTable.getUnitActionButton() val iconAndKey = ButtonDecorations.keyIconMap[name] ?: return // category without decoration entry disappears - val keyboardAvailable = Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard) val setCategoryAction = { centerTable.clear() centerTable.add(ScrollPane(table).apply { setOverscroll(false, false) }) @@ -65,7 +64,7 @@ class EmpireOverviewScreen(private var viewingPlayer:CivilizationInfo, defaultPa } button.add(name.toLabel(Color.WHITE)).pad(5f) if (!disabled && keyboardAvailable && iconAndKey.key != Char.MIN_VALUE) { - button.add("(${iconAndKey.key})".toLabel(Color.WHITE)) + button.addStaticTip(iconAndKey.key) keyPressDispatcher[iconAndKey.key] = setCategoryAction } setCategoryActions[name] = setCategoryAction diff --git a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt index 93c9b1d16f..0cfa32d068 100644 --- a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt +++ b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt @@ -1,5 +1,6 @@ package com.unciv.ui.utils +import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.scenes.scene2d.EventListener import com.badlogic.gdx.scenes.scene2d.InputEvent @@ -192,4 +193,8 @@ class KeyPressDispatcher(val name: String? = null) : HashMap(contents,manager) { + init { + // Neither this nor tooltipManager.animations = false actually make the tip appear + // instantly. However, they hide the bug that the very first appearance is misplaced. + setInstant(true) + } + + // mark event as handled while Tooltip is shown, ignore otherwise + override fun mouseMoved(event: InputEvent?, x: Float, y: Float): Boolean { + if (container.hasParent()) return false + return super.mouseMoved(event, x, y) + } + + // put the tip in a fixed place relative to the target actor + // event.listenerActor is our button, and x/y are relative to its bottom left edge + override fun enter(event: InputEvent, x: Float, y: Float, pointer: Int, fromActor: Actor?) { + super.enter(event, event.listenerActor.width, event.listenerActor.height, pointer, fromActor) + } + + companion object { + /** Sizes the character height relative to the surrounding circle size */ + const val charHeightToCircleSize = 28f / 32f + + /** A factory for the default [TooltipManager] with a few altered properties */ + fun tooltipManager(size: Float): TooltipManager = + TooltipManager.getInstance().apply { + initialTime = 0f + offsetX = -0.75f * size // less than the tip actor width so it overshoots a little which looks nice + offsetY = 0f + animations = false + } + + /** Extension adds a circled single character as Tooltip over the top right part of a receiver Table */ + fun Table.addStaticTip (key: Char, size: Float = 26f) { + if (!keyboardAvailable || key == Char.MIN_VALUE) return + val displayKey = if (key in "iI") 'i' else key.toUpperCase() + + // Todo: Inefficient. + // The pixels have likely already been fetched from the font implementation + // and cached in a TextureRegion - but I'm lacking the skills to get them from there. + val keyPixmap = UncivGame.Current.fontImplementation!!.getCharPixmap(displayKey) + val height = size * charHeightToCircleSize + val width = height * keyPixmap.width / keyPixmap.height + val keyImage = Image(Texture(keyPixmap)).apply { + setSize(width, height) + color = ImageGetter.getBlue() + }.surroundWithCircle(size, resizeActor = false, color = Color.LIGHT_GRAY) + + addListener(StaticTooltip(keyImage, tooltipManager(size))) + } + } +} diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt b/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt index 782a45ad33..bcbd33c942 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt @@ -1,7 +1,5 @@ package com.unciv.ui.worldscreen -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Group @@ -15,16 +13,21 @@ import com.unciv.ui.overviewscreen.EmpireOverviewScreen import com.unciv.ui.pickerscreens.PolicyPickerScreen import com.unciv.ui.pickerscreens.TechPickerScreen import com.unciv.ui.utils.* +import com.unciv.ui.utils.StaticTooltip.Companion.addStaticTip import com.unciv.ui.victoryscreen.VictoryScreen import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup import kotlin.math.abs import kotlin.math.ceil import kotlin.math.roundToInt + +/** + * Table consisting of the menu button, current civ, some stats and the overview button for the top of [WorldScreen] + */ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { - var selectedCivLabel = worldScreen.selectedCiv.civName.toLabel() - private var selectedCivIconHolder = Container() + private var selectedCivLabel = worldScreen.selectedCiv.civName.toLabel() + private var selectedCivIconHolder = Container() private val turnsLabel = "Turns: 0/400".toLabel() private val goldLabel = "0".toLabel(colorFromRGB(225, 217, 71)) @@ -37,8 +40,8 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { private val happinessImage = Group() // These are all to improve performance IE reduce update time (was 150 ms on my phone, which is a lot!) - private val malcontentColor = Color.valueOf("ef5350") - private val happinessColor = colorFromRGB(92, 194, 77) + private val malcontentColor = colorFromRGB(239,83,80) // Color.valueOf("ef5350") + private val happinessColor = colorFromRGB(92, 194, 77) // Color.valueOf("8cc24d") private val malcontentGroup = ImageGetter.getStatIcon("Malcontent") private val happinessGroup = ImageGetter.getStatIcon("Happiness") @@ -149,9 +152,7 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { private fun getOverviewButton(): Button { val overviewButton = Button(CameraStageBaseScreen.skin) overviewButton.add("Overview".toLabel()).pad(10f) - if (Gdx.app.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard)) { - overviewButton.add("(E)".toLabel(Color.WHITE)) - } + overviewButton.addStaticTip('e') overviewButton.pack() overviewButton.onClick { worldScreen.game.setScreen(EmpireOverviewScreen(worldScreen.selectedCiv)) } overviewButton.centerY(this) @@ -217,10 +218,10 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { cultureLabel.setText(getCultureText(civInfo, nextTurnStats)) faithLabel.setText(civInfo.religionManager.storedFaith.toString() + "(+" + nextTurnStats.faith.roundToInt() + ")") - updateSelectedCivTabel() + updateSelectedCivTable() } - private fun updateSelectedCivTabel() { + private fun updateSelectedCivTable() { if (selectedCivLabel.text.toString() == worldScreen.selectedCiv.civName.tr()) return selectedCivLabel.setText(worldScreen.selectedCiv.civName.tr()) @@ -235,18 +236,19 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { if (nextTurnStats.culture == 0f) return cultureString // when you start the game, you're not producing any culture val turnsToNextPolicy = (civInfo.policies.getCultureNeededForNextPolicy() - civInfo.policies.storedCulture) / nextTurnStats.culture - if (turnsToNextPolicy > 0) cultureString += " (" + ceil(turnsToNextPolicy).toInt() + ")" - else cultureString += " (!)" + cultureString += if (turnsToNextPolicy <= 0f) " (!)" + else " (" + ceil(turnsToNextPolicy).toInt() + ")" return cultureString } private fun getHappinessText(civInfo: CivilizationInfo): String { var happinessText = civInfo.getHappiness().toString() - if (civInfo.goldenAges.isGoldenAge()) - happinessText += " " + "GOLDEN AGE".tr() + "(${civInfo.goldenAges.turnsLeftForCurrentGoldenAge})" - else - happinessText += (" (" + civInfo.goldenAges.storedHappiness + "/" - + civInfo.goldenAges.happinessRequiredForNextGoldenAge() + ")") + val goldenAges = civInfo.goldenAges + happinessText += + if (goldenAges.isGoldenAge()) + " {GOLDEN AGE}(${goldenAges.turnsLeftForCurrentGoldenAge})".tr() + else + " (${goldenAges.storedHappiness}/${goldenAges.happinessRequiredForNextGoldenAge()})" return happinessText } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt index f65451f7fe..0c938f153f 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt @@ -1,7 +1,5 @@ package com.unciv.ui.worldscreen.unit -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.Button @@ -13,10 +11,12 @@ import com.unciv.models.UnitAction import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.getPlaceholderParameters import com.unciv.ui.utils.* +import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable +import com.unciv.ui.utils.StaticTooltip.Companion.addStaticTip import com.unciv.ui.worldscreen.WorldScreen import kotlin.concurrent.thread -private data class UnitIconAndKey(val Icon: Actor, var key: Char = 0.toChar()) +private data class UnitIconAndKey(val Icon: Actor, var key: Char = Char.MIN_VALUE) class UnitActionsTable(val worldScreen: WorldScreen) : Table() { @@ -76,17 +76,13 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { val iconAndKey = getIconAndKeyForUnitAction(unitAction.title) // If peripheral keyboard not detected, hotkeys will not be displayed - val keyboardAvailable = Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard) - if (!keyboardAvailable){iconAndKey.key = 0.toChar()} + if (!keyboardAvailable) { iconAndKey.key = Char.MIN_VALUE } val actionButton = Button(CameraStageBaseScreen.skin) actionButton.add(iconAndKey.Icon).size(20f).pad(5f) val fontColor = if (unitAction.isCurrentAction) Color.YELLOW else Color.WHITE actionButton.add(unitAction.title.toLabel(fontColor)).pad(5f) - if (iconAndKey.key != 0.toChar()) { - val keyLabel = "(${iconAndKey.key.toUpperCase()})".toLabel(Color.WHITE) - actionButton.add(keyLabel) - } + actionButton.addStaticTip(iconAndKey.key) actionButton.pack() val action = { unitAction.action?.invoke() @@ -95,7 +91,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { if (unitAction.action == null) actionButton.disable() else { actionButton.onClick(unitAction.uncivSound, action) - if (iconAndKey.key != 0.toChar()) + if (iconAndKey.key != Char.MIN_VALUE) worldScreen.keyPressDispatcher[iconAndKey.key] = { thread(name = "Sound") { Sounds.play(unitAction.uncivSound) } action()