diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 8ca7e5ab69..31887deebb 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -21,7 +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 com.unciv.ui.utils.UncivTooltip.Companion.addTooltip import kotlin.concurrent.thread class MainMenuScreen: CameraStageBaseScreen() { @@ -53,7 +53,7 @@ class MainMenuScreen: CameraStageBaseScreen() { if (key != null) { if (!keyVisualOnly) keyPressDispatcher[key] = function - table.addStaticTip(key, 32f) + table.addTooltip(key, 32f) } table.pack() diff --git a/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt b/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt index 4bfdf17d3f..2af0b34664 100644 --- a/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt +++ b/core/src/com/unciv/ui/overviewscreen/EmpireOverviewScreen.kt @@ -10,7 +10,7 @@ import com.unciv.logic.civilization.CivilizationInfo import com.unciv.models.translations.tr 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.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.utils.AutoScrollPane as ScrollPane class EmpireOverviewScreen(private var viewingPlayer:CivilizationInfo, defaultPage: String = "") : CameraStageBaseScreen(){ @@ -59,7 +59,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.addStaticTip(iconAndKey.key) + button.addTooltip(iconAndKey.key) keyPressDispatcher[iconAndKey.key] = setCategoryAction } setCategoryActions[name] = setCategoryAction diff --git a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt index 315891d401..d30f029045 100644 --- a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt @@ -15,7 +15,7 @@ import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.stats.Stats import com.unciv.models.translations.tr import com.unciv.ui.utils.* -import com.unciv.ui.utils.StaticTooltip.Companion.addStaticTip +import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip import kotlin.math.round class ImprovementPickerScreen(val tileInfo: TileInfo, unit: MapUnit, val onAccept: ()->Unit) : PickerScreen() { @@ -130,7 +130,7 @@ class ImprovementPickerScreen(val tileInfo: TileInfo, unit: MapUnit, val onAccep if (shortcutKey != null) { keyPressDispatcher[shortcutKey] = { accept(improvement) } - improvementButton.addStaticTip(shortcutKey) + improvementButton.addTooltip(shortcutKey) } regularImprovements.add(pickNow).padLeft(10f).fillY() diff --git a/core/src/com/unciv/ui/utils/StaticTooltip.kt b/core/src/com/unciv/ui/utils/StaticTooltip.kt deleted file mode 100644 index 39eca5b9dc..0000000000 --- a/core/src/com/unciv/ui/utils/StaticTooltip.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.unciv.ui.utils - -import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.scenes.scene2d.Actor -import com.badlogic.gdx.scenes.scene2d.InputEvent -import com.badlogic.gdx.scenes.scene2d.ui.Image -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.badlogic.gdx.scenes.scene2d.ui.Tooltip -import com.badlogic.gdx.scenes.scene2d.ui.TooltipManager -import com.unciv.UncivGame -import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable - -/** - * Modify Gdx [Tooltip] to place the tip over the top right corner of its target - * - * Usage: [table][Table].addStaticTip([key][Char]) - * - * Note: This is currently limited to displaying a single character in a circle of hardcoded size, - * displayed half-overlapping, partially out of the parent's bounding box, over the top right part - * of a Table-based Button. Adapting to new usecases shouldn't be too hard, though. - * - * @param contents The actor to display as Tooltip - * @param manager The [TooltipManager] to use - suggested: [tooltipManager] - */ -class StaticTooltip(contents: Actor, manager: TooltipManager) : Tooltip(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/utils/UncivTooltip.kt b/core/src/com/unciv/ui/utils/UncivTooltip.kt new file mode 100644 index 0000000000..eb479cecc1 --- /dev/null +++ b/core/src/com/unciv/ui/utils/UncivTooltip.kt @@ -0,0 +1,205 @@ +package com.unciv.ui.utils + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.math.Interpolation +import com.badlogic.gdx.math.Vector2 +import com.badlogic.gdx.scenes.scene2d.* +import com.badlogic.gdx.scenes.scene2d.actions.Actions +import com.badlogic.gdx.scenes.scene2d.ui.* +import com.badlogic.gdx.utils.Align + +/** + * A **Replacement** for Gdx [Tooltip], placement does not follow the mouse. + * + * Usage: [group][Group].addStaticTip([text][String], size) builds a [Label] as tip actor and attaches it to your [Group]. + * + * @param target The widget the tooltip will be added to - take care this is the same for which addListener is called + * @param content The actor to display as Tooltip + * @param targetAlign Point on the [target] widget to align the Tooltip to + * @param tipAlign Point on the Tooltip to align with the given point on the [target] + * @param offset Additional offset for Tooltip position after alignment + * @param animate Use show/hide animations + * @param forceContentSize Force virtual [content] width/height for alignment calculation + * - because Gdx auto layout reports wrong dimensions on scaled actors. + */ +@Suppress("unused") // reported incorrectly even when a use is right here in the Companion +class UncivTooltip ( + val target: Group, + val content: T, + val targetAlign: Int = Align.topRight, + val tipAlign: Int = Align.topRight, + val offset: Vector2 = Vector2.Zero, + val animate: Boolean = true, + forceContentSize: Vector2? = null, +) : InputListener() { + + // region fields + private val container: Container = Container(content) + enum class TipState { Hidden, Showing, Shown, Hiding } + /** current visibility state of the Tooltip */ + var state: TipState = TipState.Hidden + private set + private val contentWidth: Float + private val contentHeight: Float + + init { + content.touchable = Touchable.disabled + container.pack() + contentWidth = forceContentSize?.x ?: content.width + contentHeight = forceContentSize?.y ?: content.height + } + + //region show, hide and positioning + /** Show the Tooltip ([immediate]ly or begin the animation). _Can_ be called programmatically. */ + fun show(immediate: Boolean = false) { + val useAnimation = animate && !immediate + if (state == TipState.Shown || state == TipState.Showing && useAnimation || !target.hasParent()) return + if (state == TipState.Showing || state == TipState.Hiding) { + container.clearActions() + state = TipState.Hidden + container.remove() + } + val pos = target.localToParentCoordinates(target.getEdgePoint(targetAlign)).add(offset) + container.run { + val originX = getOriginX(contentWidth,tipAlign) + val originY = getOriginY(contentHeight,tipAlign) + setOrigin(originX, originY) + setPosition(pos.x - originX, pos.y - originY) + if (useAnimation) { + isTransform = true + color.a = 0.2f + setScale(0.05f) + } else { + isTransform = false + color.a = 1f + setScale(1f) + } + } + target.parent.addActor(container) + if (useAnimation) { + state = TipState.Showing + container.addAction(Actions.sequence( + Actions.parallel( + Actions.fadeIn(UncivSlider.tipAnimationDuration, Interpolation.fade), + Actions.scaleTo(1f, 1f, 0.2f, Interpolation.fade) + ), + Actions.run { if (state == TipState.Showing) state = TipState.Shown } + )) + } else + state = TipState.Shown + } + + /** Hide the Tooltip ([immediate]ly or begin the animation). _Can_ be called programmatically. */ + fun hide(immediate: Boolean = false) { + val useAnimation = animate && !immediate + if (state == TipState.Hidden || state == TipState.Hiding && useAnimation) return + if (state == TipState.Showing || state == TipState.Hiding) { + container.clearActions() + state = TipState.Shown // edge case. may actually only be partially 'shown' - animate hide anyway + } + if (useAnimation) { + state = TipState.Hiding + container.addAction(Actions.sequence( + Actions.parallel( + Actions.alpha(0.2f, 0.2f, Interpolation.fade), + Actions.scaleTo(0.05f, 0.05f, 0.2f, Interpolation.fade) + ), + Actions.removeActor(), + Actions.run { if (state == TipState.Hiding) state = TipState.Hidden } + )) + } else { + container.remove() + state = TipState.Hidden + } + } + + private fun getOriginX(width: Float, align: Int) = when { + (align and Align.left) != 0 -> 0f + (align and Align.right) != 0 -> width + else -> width / 2 + } + private fun getOriginY(height: Float, align: Int) = when { + (align and Align.bottom) != 0 -> 0f + (align and Align.top) != 0 -> height + else -> height / 2 + } + private fun Actor.getEdgePoint(align: Int) = + Vector2(getOriginX(width,align),getOriginY(height,align)) + //endregion + + //region events + override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) { + // assert(event?.listenerActor == target) - tested - holds true + if (fromActor != null && fromActor.isDescendantOf(target)) return + show() + } + + override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) { + if (toActor != null && toActor.isDescendantOf(target)) return + hide() + } + + override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int ): Boolean { + container.toFront() // this is a no-op if it has no parent + return super.touchDown(event, x, y, pointer, button) + } + //endregion + + companion object { + /** + * Add a [Label]-based Tooltip with a rounded-corner background to a [Table] or other [Group]. + * + * Tip is positioned over top right corner, slightly overshooting the receiver widget, longer tip [text]s will extend to the left. + * + * @param size _Vertical_ size of the entire Tooltip including background + * @param always override requirement: presence of physical keyboard + */ + fun Group.addTooltip(text: String, size: Float = 26f, always: Boolean = false) { + if (!(always || KeyPressDispatcher.keyboardAvailable) || text.isEmpty()) return + + val label = text.toLabel(ImageGetter.getBlue(), 38) + label.setAlignment(Align.center) + + val background = ImageGetter.getRoundedEdgeRectangle(Color.LIGHT_GRAY) + // This controls text positioning relative to the background. + // The minute fiddling makes both single caps and longer text look centered. + @Suppress("SpellCheckingInspection") + val skewPadDescenders = if (",;gjpqy".any { it in text }) 0f else 2.5f + val horizontalPad = if (text.length > 1) 10f else 6f + background.setPadding(4f+skewPadDescenders, horizontalPad, 8f-skewPadDescenders, horizontalPad) + + val widthHeightRatio: Float + val labelWithBackground = Container(label).apply { + setBackground(background) + pack() + widthHeightRatio = width / height + isTransform = true // otherwise setScale is ignored + setScale(size / height) + } + + addListener(UncivTooltip(this, + labelWithBackground, + forceContentSize = Vector2(size * widthHeightRatio, size), + offset = Vector2(size/4, 0f) + )) + } + + /** + * Add a single Char [Label]-based Tooltip with a rounded-corner background to a [Table] or other [Group]. + * + * Tip is positioned over top right corner, slightly overshooting the receiver widget. + * + * @param size _Vertical_ size of the entire Tooltip including background + * @param always override requirement: presence of physical keyboard + */ + fun Group.addTooltip(char: Char, size: Float = 26f, always: Boolean = false) { + addTooltip((if (char in "Ii") 'i' else char.toUpperCase()).toString(), size, always) + } + +/* unused - template in case we need it - problem: how exactly to handle translation? + fun Group.addTooltip(key: KeyCharAndCode, size: Float = 26f, always: Boolean = false) { + addTooltip(key.toString(), size, always) + } +*/ + } +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt b/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt index 91867b6b2f..725697b5b7 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreenTopBar.kt @@ -13,7 +13,7 @@ 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.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.victoryscreen.VictoryScreen import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup import kotlin.math.abs @@ -152,7 +152,7 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() { private fun getOverviewButton(): Button { val overviewButton = Button(CameraStageBaseScreen.skin) overviewButton.add("Overview".toLabel()).pad(10f) - overviewButton.addStaticTip('e') + overviewButton.addTooltip('e') overviewButton.pack() overviewButton.onClick { worldScreen.game.setScreen(EmpireOverviewScreen(worldScreen.selectedCiv)) } overviewButton.centerY(this) diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt index 684720da6c..0497d80385 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt @@ -12,7 +12,7 @@ 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.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.worldscreen.WorldScreen import kotlin.concurrent.thread @@ -86,7 +86,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { 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) - actionButton.addStaticTip(iconAndKey.key) + actionButton.addTooltip(iconAndKey.key) actionButton.pack() val action = { unitAction.action?.invoke() @@ -104,4 +104,4 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { return actionButton } -} \ No newline at end of file +}