mirror of
https://github.com/yairm210/Unciv.git
synced 2025-08-01 15:49:46 +07:00
Rewritten Tooltip class (#4552)
This commit is contained in:
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
205
core/src/com/unciv/ui/utils/UncivTooltip.kt
Normal file
205
core/src/com/unciv/ui/utils/UncivTooltip.kt
Normal 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)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user