Improved Widgets - Fixing Tabbed Pager Scrolling (#6413)

* Fix TabbedPager problems when fixedContent wider than widget - Sync ScrollPanes approach V2

* UncivTooltip no longer has a reason to limit itself to Group
This commit is contained in:
SomeTroglodyte 2022-03-25 15:28:36 +01:00 committed by GitHub
parent e8ab73df98
commit 2f035e55f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 307 additions and 129 deletions

View File

@ -32,22 +32,22 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
*/
open class AutoScrollPane(widget: Actor?, style: ScrollPaneStyle = ScrollPaneStyle()): ScrollPane(widget,style) {
constructor(widget: Actor, skin: Skin) : this(widget,skin.get(ScrollPaneStyle::class.java))
constructor(widget: Actor, skin: Skin, styleName: String) : this(widget,skin.get(styleName,ScrollPaneStyle::class.java))
constructor(widget: Actor?, skin: Skin) : this(widget,skin.get(ScrollPaneStyle::class.java))
constructor(widget: Actor?, skin: Skin, styleName: String) : this(widget,skin.get(styleName,ScrollPaneStyle::class.java))
private var savedFocus: Actor? = null
init {
this.addListener (object : ClickListener() {
override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) {
if (stage == null)
return
if (stage == null) return
if (fromActor?.isDescendantOf(this@AutoScrollPane) == true) return
if (savedFocus == null) savedFocus = stage.scrollFocus
stage.scrollFocus = this@AutoScrollPane
}
override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) {
if (stage == null)
return
if (stage == null) return
if (toActor?.isDescendantOf(this@AutoScrollPane) == true) return
if (stage.scrollFocus == this@AutoScrollPane) stage.scrollFocus = savedFocus
savedFocus = null
}

View File

@ -1,19 +1,18 @@
package com.unciv.ui.utils
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g3d.model.Animation
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.*
import com.badlogic.gdx.scenes.scene2d.ui.*
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import kotlin.math.min
/*
Unimplemented ideas:
Use fixedContent for OptionsPopup mod check tab
`scrollAlign: Align` property controls initial content scroll position (currently it's Align.top)
*/
/**
@ -23,24 +22,28 @@ import kotlin.math.min
* [replaced][replacePage] or dynamically added after the Widget is already shown.
* Pages are automatically scrollable, switching pages preserves scroll positions individually.
* The widget optionally supports "fixed content", an additional Actor that will be inserted above
* the regular content of any page. It will scroll only horizontally, and its scroll position will
* synchronize bidirectionally with the scrolling of the main content.
*
* Pages can be disabled or secret - any 'secret' pages added require a later call to [askForPassword]
* to activate them (or discard if the password is wrong).
*
* The size parameters are lower and upper bounds of the page content area. The widget will always report
* these bounds (plus header height) as layout properties min/max-Width/Height, and measure the content
* area of added pages and set the reported pref-W/H to their maximum within these bounds. But, if a
* maximum is not specified, that coordinate will grow with content unlimited, and layout max-W/H will
* always report the same as pref-W/H.
* maximum is not specified, that coordinate will grow with content up to screen size, and layout
* max-W/H will always report the same as pref-W/H.
*
* [keyPressDispatcher] is optional and works with the `shortcutKey` parameter of [addPage] to support key bindings with tooltips.
*/
//region Fields and initialization
//region Fields
@Suppress("MemberVisibilityCanBePrivate", "unused") // All member are part of our API
class TabbedPager(
private val minimumWidth: Float = 0f,
private var maximumWidth: Float = Float.MAX_VALUE,
private val minimumHeight: Float = 0f,
private var maximumHeight: Float = Float.MAX_VALUE,
minimumWidth: Float = 0f,
maximumWidth: Float = Float.MAX_VALUE,
minimumHeight: Float = 0f,
maximumHeight: Float = Float.MAX_VALUE,
private val headerFontSize: Int = Constants.defaultFontSize,
private val headerFontColor: Color = Color.WHITE,
private val highlightColor: Color = Color.BLUE,
@ -51,38 +54,8 @@ class TabbedPager(
capacity: Int = 4
) : Table() {
private class PageState(
caption: String,
var content: Actor,
var fixedContent: WidgetGroup? = null,
var disabled: Boolean = false,
val onActivation: ((Int, String) -> Unit)? = null,
val onDeactivation: ((Int, String, Float) -> Unit)? = null,
icon: Actor? = null,
iconSize: Float = 0f,
val shortcutKey: KeyCharAndCode = KeyCharAndCode.UNKNOWN,
pager: TabbedPager
) {
var scrollX = 0f
var scrollY = 0f
val button = IconTextButton(caption, icon, pager.headerFontSize, pager.headerFontColor).apply {
if (icon != null) {
if (iconSize != 0f)
iconCell!!.size(iconSize)
iconCell!!.padRight(pager.headerPadding * 0.5f)
}
}
var buttonX = 0f
var buttonW = 0f
}
private var preferredWidth = minimumWidth
private val growMaxWidth = maximumWidth == Float.MAX_VALUE
private val limitWidth = maximumWidth
private var preferredHeight = minimumHeight
private val growMaxHeight = maximumHeight == Float.MAX_VALUE
private val limitHeight = maximumHeight
private val dimW: DimensionMeasurement
private val dimH: DimensionMeasurement
private val pages = ArrayList<PageState>(capacity)
@ -93,26 +66,207 @@ class TabbedPager(
private set
private val header = Table(BaseScreen.skin)
private val headerScroll = AutoScrollPane(header)
private val headerScroll = LinkedScrollPane(horizontalOnly = true, header)
private var headerHeight = 0f
private val contentScroll = AutoScrollPane(null)
private val fixedContentWrapper = Container<WidgetGroup>()
private val fixedContentScroll = LinkedScrollPane(horizontalOnly = true)
private val fixedContentScrollCell: Cell<ScrollPane>
private val contentScroll = LinkedScrollPane(horizontalOnly = false, linkTo = fixedContentScroll)
private val deferredSecretPages = ArrayDeque<PageState>(0)
private var askPasswordLock = false
//endregion
//region Private Classes
private class PageState(
caption: String,
var content: Actor,
var fixedContent: Actor?,
var disabled: Boolean,
val onActivation: ((Int, String) -> Unit)?,
val onDeactivation: ((Int, String, Float) -> Unit)?,
icon: Actor?,
iconSize: Float,
val shortcutKey: KeyCharAndCode,
var scrollAlign: Int,
val syncScroll: Boolean,
pager: TabbedPager
) {
var fixedHeight = 0f
var scrollX = 0f
var scrollY = 0f
val button = IconTextButton(caption, icon, pager.headerFontSize, pager.headerFontColor).apply {
name = caption // enable finding pages by untranslated caption without needing our own field
if (icon != null) {
if (iconSize != 0f)
iconCell!!.size(iconSize)
iconCell!!.padRight(pager.headerPadding * 0.5f)
}
}
var buttonX = 0f
var buttonW = 0f
val caption: String
get() = button.name
override fun toString() = "PageState($caption, key=$shortcutKey, disabled=$disabled, content:${content.javaClass.simpleName}, fixedContent:${fixedContent?.javaClass?.simpleName})"
}
private data class DimensionMeasurement(
var min: Float,
var pref: Float,
var max: Float,
val limit: Float,
val growMax: Boolean
) {
constructor(limit: Float) : this(0f, 0f, 0f, limit, true)
companion object {
fun from(min: Float, max: Float, limit: Float): DimensionMeasurement {
if (max == Float.MAX_VALUE)
return DimensionMeasurement(min, min, 0f, limit, true)
val fixedMax = max.coerceAtMost(limit)
return DimensionMeasurement(min, min, fixedMax, fixedMax, false)
}
}
fun measure(newMin: Float, newPref: Float, newMax: Float): Boolean {
var needLayout = false
newMin.coerceAtMost(limit)
.let { if (it > min) { min = it; needLayout = true } }
newPref.coerceAtLeast(min).coerceAtMost(limit)
.let { if (it > pref) { pref = it; needLayout = true } }
if (!growMax) return needLayout
newMax.coerceAtLeast(pref).coerceAtMost(limit)
.let { if (it > max) { max = it; needLayout = true } }
return needLayout
}
fun measureWidth(group: WidgetGroup?): Boolean {
if (group == null) return false
group.packIfNeeded()
return measure(group.minWidth, group.prefWidth, group.maxWidth)
}
fun measureHeight(group: WidgetGroup?): Boolean {
if (group == null) return false
group.packIfNeeded()
return measure(group.minHeight, group.prefHeight, group.maxHeight)
}
fun combine(header: Float, top: DimensionMeasurement, bottom: DimensionMeasurement) {
min = (header + top.min + bottom.min).coerceAtLeast(min).coerceAtMost(limit)
pref = (header + top.pref + bottom.pref).coerceAtLeast(pref).coerceIn(min..limit)
if (growMax)
max = (header + top.max + bottom.max).coerceAtLeast(max).coerceIn(pref..limit)
}
}
private class LinkedScrollPane(
horizontalOnly: Boolean,
widget: Actor? = null,
linkTo: LinkedScrollPane? = null
) : AutoScrollPane(widget, BaseScreen.skin) {
val linkedScrolls = mutableSetOf<LinkedScrollPane>()
var enableSync = true
init {
if (horizontalOnly)
setScrollingDisabled(false, true)
setOverscroll(false, false)
setScrollbarsOnTop(true)
setupFadeScrollBars(0f, 0f)
if (linkTo != null) {
linkedScrolls += linkTo
linkTo.linkedScrolls += this
}
}
private fun sync(update: Boolean = true) {
if (!enableSync) return
for (linkedScroll in linkedScrolls) {
if (linkedScroll.scrollX == this.scrollX) continue
linkedScroll.scrollX = this.scrollX
if (update) linkedScroll.updateVisualScroll()
}
}
override fun addScrollListener() {
super.addScrollListener()
val oldListener = listeners.removeIndex(listeners.size-1) as InputListener
addListener(object : InputListener() {
override fun scrolled(event: InputEvent?, x: Float, y: Float, amountX: Float, amountY: Float): Boolean {
val toReturn = oldListener.scrolled(event, x, y, amountX, amountY)
sync(false)
return toReturn
}
})
}
override fun addCaptureListener() {
super.addCaptureListener()
val oldListener = captureListeners.removeIndex(0) as InputListener
addCaptureListener(object : InputListener() {
override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean {
val toReturn = oldListener.touchDown(event, x, y, pointer, button)
sync()
return toReturn
}
override fun touchDragged(event: InputEvent?, x: Float, y: Float, pointer: Int) {
oldListener.touchDragged(event, x, y, pointer)
sync()
}
override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
oldListener.touchUp(event, x, y, pointer, button)
sync()
}
override fun mouseMoved(event: InputEvent?, x: Float, y: Float): Boolean {
// syncing here leads to stutter
return oldListener.mouseMoved(event, x, y)
}
})
}
override fun getFlickScrollListener(): ActorGestureListener {
val stdFlickListener = super.getFlickScrollListener()
val newFlickListener = object: ActorGestureListener() {
override fun pan(event: InputEvent?, x: Float, y: Float, deltaX: Float, deltaY: Float) {
stdFlickListener.pan(event, x, y, deltaX, deltaY)
sync()
}
override fun fling(event: InputEvent?, velocityX: Float, velocityY: Float, button: Int) {
stdFlickListener.fling(event, velocityX, velocityY, button)
sync()
}
}
return newFlickListener
}
override fun act(delta: Float) {
val wasFlinging = isFlinging
super.act(delta)
if (wasFlinging) sync()
}
}
//endregion
//region Initialization
init {
val screen = (if (UncivGame.isCurrentInitialized()) UncivGame.Current.screen else null) as? BaseScreen
val (screenWidth, screenHeight) = (screen?.stage?.run { width to height }) ?: (Float.MAX_VALUE to Float.MAX_VALUE)
dimW = DimensionMeasurement.from(minimumWidth, maximumWidth, screenWidth)
dimH = DimensionMeasurement.from(minimumHeight, maximumHeight, screenHeight)
background = ImageGetter.getBackground(backgroundColor)
header.defaults().pad(headerPadding, headerPadding * 0.5f)
headerScroll.setOverscroll(false,false)
headerScroll.setScrollingDisabled(false, true)
// Measure header height, most likely its final value
removePage(addPage("Dummy"))
add(headerScroll).growX().minHeight(headerHeight).row()
if (separatorColor != Color.CLEAR)
addSeparator(separatorColor)
add(fixedContentWrapper).growX().row()
fixedContentScrollCell = add(fixedContentScroll)
fixedContentScrollCell.growX().row()
add(contentScroll).grow().row()
}
@ -120,22 +274,22 @@ class TabbedPager(
//region Widget interface
// The following are part of the Widget interface and serve dynamic sizing
override fun getPrefWidth() = preferredWidth
override fun getPrefWidth() = dimW.pref
fun setPrefWidth(width: Float) {
if (width !in minimumWidth..maximumWidth) throw IllegalArgumentException()
preferredWidth = width
if (width !in dimW.min..dimW.max) throw IllegalArgumentException()
dimW.pref = width
invalidateHierarchy()
}
override fun getPrefHeight() = preferredHeight + headerHeight
override fun getPrefHeight() = dimH.pref
fun setPrefHeight(height: Float) {
if (height - headerHeight !in minimumHeight..maximumHeight) throw IllegalArgumentException()
preferredHeight = height - headerHeight
if (height !in dimH.min..dimH.max) throw IllegalArgumentException()
dimH.pref = height
invalidateHierarchy()
}
override fun getMinWidth() = minimumWidth
override fun getMaxWidth() = maximumWidth
override fun getMinHeight() = headerHeight + minimumHeight
override fun getMaxHeight() = headerHeight + maximumHeight
override fun getMinWidth() = dimW.min
override fun getMaxWidth() = dimW.max
override fun getMinHeight() = dimH.min
override fun getMaxHeight() = dimH.max
//endregion
//region API
@ -144,53 +298,79 @@ class TabbedPager(
fun pageCount() = pages.size
/** @return index of a page by its (untranslated) caption, or -1 if no such page exists */
fun getPageIndex(caption: String) = pages.indexOfLast { it.button.name == caption }
fun getPageIndex(caption: String) = pages.indexOfLast { it.caption == caption }
/** Change the selected page by using its index.
* @param index Page number or -1 to deselect the current page.
* @param centerButton `true` centers the page's header button, `false` ensures it is visible.
* @return `true` if the page was successfully changed.
*/
fun selectPage(index: Int): Boolean {
fun selectPage(index: Int, centerButton: Boolean = true): Boolean {
if (index !in -1 until pages.size) return false
if (activePage == index) return false
if (index >= 0 && pages[index].disabled) return false
if (activePage != -1) {
pages[activePage].apply {
onDeactivation?.invoke(activePage, button.name, contentScroll.scrollY)
button.color = Color.WHITE
fixedContentWrapper.actor = null
scrollX = contentScroll.scrollX
scrollY = contentScroll.scrollY
contentScroll.removeActor(content)
}
val page = pages[activePage]
page.onDeactivation?.invoke(activePage, page.caption, contentScroll.scrollY)
page.button.color = Color.WHITE
fixedContentScroll.actor = null
page.scrollX = contentScroll.scrollX
page.scrollY = contentScroll.scrollY
contentScroll.actor = null
}
activePage = index
if (index != -1) {
pages[index].apply {
button.color = highlightColor
fixedContentWrapper.actor = fixedContent
contentScroll.actor = content
val page = pages[index]
page.button.color = highlightColor
if (page.scrollAlign != 0) {
if (Align.isCenterHorizontal(page.scrollAlign))
page.scrollX = (page.content.width - this.width) / 2
else if (Align.isRight(page.scrollAlign))
page.scrollX = Float.MAX_VALUE // ScrollPane _will_ clamp this
if (Align.isCenterVertical(page.scrollAlign))
page.scrollY = (page.content.height - this.height) / 2
else if (Align.isBottom(page.scrollAlign))
page.scrollY = Float.MAX_VALUE // ScrollPane _will_ clamp this
page.scrollAlign = 0 // once only
}
fixedContentScroll.actor = page.fixedContent
fixedContentScroll.height = page.fixedHeight
fixedContentScrollCell.minHeight(page.fixedHeight)
fixedContentScroll.layout()
fixedContentScroll.scrollX = page.scrollX
fixedContentScroll.updateVisualScroll()
fixedContentScroll.enableSync = page.syncScroll
contentScroll.actor = page.content
contentScroll.layout()
if (scrollX < 0f) // was marked to center on first show
scrollX = ((content.width - this@TabbedPager.width) / 2).coerceIn(0f, contentScroll.maxX)
contentScroll.scrollX = scrollX
contentScroll.scrollY = scrollY
contentScroll.scrollX = page.scrollX
contentScroll.scrollY = page.scrollY
contentScroll.updateVisualScroll()
headerScroll.let {
it.scrollX = (buttonX + (buttonW - it.width) / 2).coerceIn(0f, it.maxX)
}
onActivation?.invoke(index, button.name)
}
contentScroll.enableSync = page.syncScroll
if (centerButton)
// centering is nice when selectPage is called programmatically
headerScroll.scrollX = page.buttonX + (page.buttonW - headerScroll.width) / 2
else
// when coming from a tap/click, can we at least ensure no part of it is outside the visible area
headerScroll.run { scrollX = scrollX.coerceIn((page.buttonX + page.buttonW - scrollWidth)..page.buttonX) }
page.onActivation?.invoke(index, page.caption)
}
return true
}
/** Change the selected page by using its caption.
* @param caption Caption of the page to select. A nonexistent name will deselect the current page.
* @param centerButton `true` centers the page's header button, `false` ensures it is visible.
* @return `true` if the page was successfully changed.
*/
fun selectPage(caption: String) = selectPage(getPageIndex(caption))
private fun selectPage(page: PageState) = selectPage(getPageIndex(page))
fun selectPage(caption: String, centerButton: Boolean = true) = selectPage(getPageIndex(caption), centerButton)
private fun selectPage(page: PageState) = selectPage(getPageIndex(page), centerButton = false)
/** Change the disabled property of a page by its index.
* @return previous value or `false` if index invalid.
@ -243,34 +423,41 @@ class TabbedPager(
if (index !in 0 until pages.size) return
val isActive = index == activePage
if (isActive) selectPage(-1)
pages[index].content = content
pages[index].let {
it.content = content
measureContent(it)
}
if (isActive) selectPage(index)
}
/** Replace a page's [content] and [fixedContent] by its [index]. */
fun replacePage(index: Int, content: Actor, fixedContent: WidgetGroup?) {
fun replacePage(index: Int, content: Actor, fixedContent: Actor?) {
if (index !in 0 until pages.size) return
val isActive = index == activePage
if (isActive) selectPage(-1)
pages[index].content = content
pages[index].fixedContent = fixedContent
pages[index].let {
it.content = content
it.fixedContent = fixedContent
measureContent(it)
}
if (isActive) selectPage(index)
}
/** Replace a page's [content] by its [caption]. */
fun replacePage(caption: String, content: Actor) = replacePage(getPageIndex(caption), content)
/** Replace a page's [content] and [fixedContent] by its [caption]. */
fun replacePage(caption: String, content: Actor, fixedContent: WidgetGroup?) = replacePage(getPageIndex(caption), content, fixedContent)
fun replacePage(caption: String, content: Actor, fixedContent: Actor?) = replacePage(getPageIndex(caption), content, fixedContent)
/** Add a page!
* @param caption Text to be shown on the header button (automatically translated), can later be used to reference the page in other calls.
* @param content Actor to show when this page is selected.
* @param icon Actor, typically an [Image], to show before the caption.
* @param content [Actor] to show in the lower area when this page is selected.
* @param icon Actor, typically an [Image], to show before the caption on the header button.
* @param iconSize Size for [icon] - if not zero, the icon is wrapped to allow a [setSize] even on [Image] which ignores size.
* @param insertBefore -1 to add at the end or index of existing page to insert this before
* @param insertBefore -1 to add at the end, or index of existing page to insert this before it.
* @param secret Marks page as 'secret'. A password is asked once per [TabbedPager] and if it does not match the has passed in the constructor the page and all subsequent secret pages are dropped.
* @param disabled Initial disabled state. Disabled pages cannot be selected even with [selectPage], their button is dimmed.
* @param shortcutKey Optional keyboard key to associate - goes to the [KeyPressDispatcher] passed in the constructor.
* @param fixedContent Optional second content [WidgetGroup], will be placed outside the tab's [ScrollPane] between header and [content].
* @param syncScroll If on, the ScrollPanes for [content] and [fixedContent] will synchronize horizontally.
* @param fixedContent Optional second content [Actor], will be placed outside the tab's main [ScrollPane] between header and [content]. Scrolls horizontally only.
* @param onDeactivation _Optional_ callback called when this page is hidden. Lambda arguments are page index and caption, and scrollY of the tab's [ScrollPane].
* @param onActivation _Optional_ callback called when this page is shown (per actual change to this page, not per header click). Lambda arguments are page index and caption.
* @return The new page's index or -1 if it could not be immediately added (secret).
@ -284,7 +471,9 @@ class TabbedPager(
secret: Boolean = false,
disabled: Boolean = false,
shortcutKey: KeyCharAndCode = KeyCharAndCode.UNKNOWN,
fixedContent: WidgetGroup? = null,
scrollAlign: Int = Align.top,
syncScroll: Boolean = true,
fixedContent: Actor? = null,
onDeactivation: ((Int, String, Float) -> Unit)? = null,
onActivation: ((Int, String) -> Unit)? = null
): Int {
@ -299,10 +488,11 @@ class TabbedPager(
icon = icon,
iconSize = iconSize,
shortcutKey = shortcutKey,
scrollAlign = scrollAlign,
syncScroll = syncScroll,
pager = this
)
page.button.apply {
name = caption // enable finding pages by untranslated caption without needing our own field
isEnabled = !disabled
onClick {
selectPage(page)
@ -358,20 +548,15 @@ class TabbedPager(
private fun getPageIndex(page: PageState) = pages.indexOf(page)
private fun measureContent(group: WidgetGroup) {
group.packIfNeeded()
var needLayout = false
val contentWidth = min(group.width, limitWidth)
if (contentWidth > preferredWidth) {
preferredWidth = contentWidth
needLayout = true
}
val contentHeight = min(group.height, limitHeight)
if (contentHeight > preferredHeight) {
preferredHeight = contentHeight
needLayout = true
}
if (needLayout && activePage >= 0) invalidateHierarchy()
private fun measureContent(page: PageState) {
val dimFixedH = DimensionMeasurement(dimH.limit)
val dimContentH = DimensionMeasurement(dimH.limit)
dimW.measureWidth(page.fixedContent as? WidgetGroup)
dimFixedH.measureHeight(page.fixedContent as? WidgetGroup)
page.fixedHeight = dimFixedH.min
dimW.measureWidth(page.content as? WidgetGroup)
dimContentH.measureHeight(page.content as? WidgetGroup)
dimH.combine(headerHeight, dimFixedH, dimContentH)
}
private fun addAndShowPage(page: PageState, insertBefore: Int): Int {
@ -393,14 +578,7 @@ class TabbedPager(
for (i in newIndex + 1 until pages.size)
pages[i].buttonX += page.buttonW
// Content Sizing
if (page.fixedContent != null) measureContent(page.fixedContent!!)
(page.content as? WidgetGroup)?.let {
measureContent(it)
page.scrollX = -1f // mark to center later when all pages are measured
}
if (growMaxWidth) maximumWidth = minimumWidth
if (growMaxHeight) maximumHeight = minimumHeight
measureContent(page)
keyPressDispatcher?.set(page.shortcutKey) { selectPage(newIndex) }

View File

@ -24,7 +24,7 @@ import com.unciv.models.translations.tr
* - because Gdx auto layout reports wrong dimensions on scaled actors.
*/
class UncivTooltip <T: Actor>(
val target: Group,
val target: Actor,
val content: T,
val targetAlign: Int = Align.topRight,
val tipAlign: Int = Align.topRight,
@ -160,7 +160,7 @@ class UncivTooltip <T: Actor>(
* @param always override requirement: presence of physical keyboard
* @param tipAlign Point on the Tooltip to align with the top right of the [target]
*/
fun Group.addTooltip(text: String, size: Float = 26f, always: Boolean = false, tipAlign: Int = Align.top) {
fun Actor.addTooltip(text: String, size: Float = 26f, always: Boolean = false, tipAlign: Int = Align.top) {
if (!(always || KeyPressDispatcher.keyboardAvailable) || text.isEmpty()) return
val label = text.toLabel(ImageGetter.getBlue(), 38)
@ -199,7 +199,7 @@ class UncivTooltip <T: Actor>(
* @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) {
fun Actor.addTooltip(char: Char, size: Float = 26f, always: Boolean = false) {
addTooltip((if (char in "Ii") 'i' else char.uppercaseChar()).toString(), size, always)
}
@ -211,7 +211,7 @@ class UncivTooltip <T: Actor>(
* @param size _Vertical_ size of the entire Tooltip including background
* @param always override requirement: presence of physical keyboard
*/
fun Group.addTooltip(key: KeyCharAndCode, size: Float = 26f, always: Boolean = false) {
fun Actor.addTooltip(key: KeyCharAndCode, size: Float = 26f, always: Boolean = false) {
if (key != KeyCharAndCode.UNKNOWN)
addTooltip(key.toString().tr(), size, always)
}