Rewritten Tooltip class (#4552)

This commit is contained in:
SomeTroglodyte
2021-07-17 21:44:09 +02:00
committed by GitHub
parent 3a7da738c4
commit 8c35e54b71
7 changed files with 216 additions and 88 deletions

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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<Actor>(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)))
}
}
}

View File

@ -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 <T: Actor>(
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<T> = 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)
}
*/
}
}

View File

@ -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)

View File

@ -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
}
}
}