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) { 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) : 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, styleName: String) : this(widget,skin.get(styleName,ScrollPaneStyle::class.java))
private var savedFocus: Actor? = null private var savedFocus: Actor? = null
init { init {
this.addListener (object : ClickListener() { this.addListener (object : ClickListener() {
override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) { override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Actor?) {
if (stage == null) if (stage == null) return
return if (fromActor?.isDescendantOf(this@AutoScrollPane) == true) return
if (savedFocus == null) savedFocus = stage.scrollFocus if (savedFocus == null) savedFocus = stage.scrollFocus
stage.scrollFocus = this@AutoScrollPane stage.scrollFocus = this@AutoScrollPane
} }
override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) { override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Actor?) {
if (stage == null) if (stage == null) return
return if (toActor?.isDescendantOf(this@AutoScrollPane) == true) return
if (stage.scrollFocus == this@AutoScrollPane) stage.scrollFocus = savedFocus if (stage.scrollFocus == this@AutoScrollPane) stage.scrollFocus = savedFocus
savedFocus = null savedFocus = null
} }

View File

@ -1,19 +1,18 @@
package com.unciv.ui.utils package com.unciv.ui.utils
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g3d.model.Animation import com.badlogic.gdx.scenes.scene2d.*
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.ui.* 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.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import kotlin.math.min
/* /*
Unimplemented ideas: Unimplemented ideas:
Use fixedContent for OptionsPopup mod check tab 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. * [replaced][replacePage] or dynamically added after the Widget is already shown.
* Pages are automatically scrollable, switching pages preserves scroll positions individually. * 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] * 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). * 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 * 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 * 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 * 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 * maximum is not specified, that coordinate will grow with content up to screen size, and layout
* always report the same as pref-W/H. * 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. * [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 @Suppress("MemberVisibilityCanBePrivate", "unused") // All member are part of our API
class TabbedPager( class TabbedPager(
private val minimumWidth: Float = 0f, minimumWidth: Float = 0f,
private var maximumWidth: Float = Float.MAX_VALUE, maximumWidth: Float = Float.MAX_VALUE,
private val minimumHeight: Float = 0f, minimumHeight: Float = 0f,
private var maximumHeight: Float = Float.MAX_VALUE, maximumHeight: Float = Float.MAX_VALUE,
private val headerFontSize: Int = Constants.defaultFontSize, private val headerFontSize: Int = Constants.defaultFontSize,
private val headerFontColor: Color = Color.WHITE, private val headerFontColor: Color = Color.WHITE,
private val highlightColor: Color = Color.BLUE, private val highlightColor: Color = Color.BLUE,
@ -51,38 +54,8 @@ class TabbedPager(
capacity: Int = 4 capacity: Int = 4
) : Table() { ) : Table() {
private class PageState( private val dimW: DimensionMeasurement
caption: String, private val dimH: DimensionMeasurement
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 pages = ArrayList<PageState>(capacity) private val pages = ArrayList<PageState>(capacity)
@ -93,26 +66,207 @@ class TabbedPager(
private set private set
private val header = Table(BaseScreen.skin) private val header = Table(BaseScreen.skin)
private val headerScroll = AutoScrollPane(header) private val headerScroll = LinkedScrollPane(horizontalOnly = true, header)
private var headerHeight = 0f private var headerHeight = 0f
private val contentScroll = AutoScrollPane(null) private val fixedContentScroll = LinkedScrollPane(horizontalOnly = true)
private val fixedContentWrapper = Container<WidgetGroup>() private val fixedContentScrollCell: Cell<ScrollPane>
private val contentScroll = LinkedScrollPane(horizontalOnly = false, linkTo = fixedContentScroll)
private val deferredSecretPages = ArrayDeque<PageState>(0) private val deferredSecretPages = ArrayDeque<PageState>(0)
private var askPasswordLock = false 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 { 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) background = ImageGetter.getBackground(backgroundColor)
header.defaults().pad(headerPadding, headerPadding * 0.5f) header.defaults().pad(headerPadding, headerPadding * 0.5f)
headerScroll.setOverscroll(false,false)
headerScroll.setScrollingDisabled(false, true)
// Measure header height, most likely its final value // Measure header height, most likely its final value
removePage(addPage("Dummy")) removePage(addPage("Dummy"))
add(headerScroll).growX().minHeight(headerHeight).row() add(headerScroll).growX().minHeight(headerHeight).row()
if (separatorColor != Color.CLEAR) if (separatorColor != Color.CLEAR)
addSeparator(separatorColor) addSeparator(separatorColor)
add(fixedContentWrapper).growX().row()
fixedContentScrollCell = add(fixedContentScroll)
fixedContentScrollCell.growX().row()
add(contentScroll).grow().row() add(contentScroll).grow().row()
} }
@ -120,22 +274,22 @@ class TabbedPager(
//region Widget interface //region Widget interface
// The following are part of the Widget interface and serve dynamic sizing // 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) { fun setPrefWidth(width: Float) {
if (width !in minimumWidth..maximumWidth) throw IllegalArgumentException() if (width !in dimW.min..dimW.max) throw IllegalArgumentException()
preferredWidth = width dimW.pref = width
invalidateHierarchy() invalidateHierarchy()
} }
override fun getPrefHeight() = preferredHeight + headerHeight override fun getPrefHeight() = dimH.pref
fun setPrefHeight(height: Float) { fun setPrefHeight(height: Float) {
if (height - headerHeight !in minimumHeight..maximumHeight) throw IllegalArgumentException() if (height !in dimH.min..dimH.max) throw IllegalArgumentException()
preferredHeight = height - headerHeight dimH.pref = height
invalidateHierarchy() invalidateHierarchy()
} }
override fun getMinWidth() = minimumWidth override fun getMinWidth() = dimW.min
override fun getMaxWidth() = maximumWidth override fun getMaxWidth() = dimW.max
override fun getMinHeight() = headerHeight + minimumHeight override fun getMinHeight() = dimH.min
override fun getMaxHeight() = headerHeight + maximumHeight override fun getMaxHeight() = dimH.max
//endregion //endregion
//region API //region API
@ -144,53 +298,79 @@ class TabbedPager(
fun pageCount() = pages.size fun pageCount() = pages.size
/** @return index of a page by its (untranslated) caption, or -1 if no such page exists */ /** @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. /** Change the selected page by using its index.
* @param index Page number or -1 to deselect the current page. * @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. * @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 (index !in -1 until pages.size) return false
if (activePage == index) return false if (activePage == index) return false
if (index >= 0 && pages[index].disabled) return false if (index >= 0 && pages[index].disabled) return false
if (activePage != -1) { if (activePage != -1) {
pages[activePage].apply { val page = pages[activePage]
onDeactivation?.invoke(activePage, button.name, contentScroll.scrollY) page.onDeactivation?.invoke(activePage, page.caption, contentScroll.scrollY)
button.color = Color.WHITE page.button.color = Color.WHITE
fixedContentWrapper.actor = null fixedContentScroll.actor = null
scrollX = contentScroll.scrollX page.scrollX = contentScroll.scrollX
scrollY = contentScroll.scrollY page.scrollY = contentScroll.scrollY
contentScroll.removeActor(content) contentScroll.actor = null
}
} }
activePage = index activePage = index
if (index != -1) { if (index != -1) {
pages[index].apply { val page = pages[index]
button.color = highlightColor page.button.color = highlightColor
fixedContentWrapper.actor = fixedContent
contentScroll.actor = content if (page.scrollAlign != 0) {
contentScroll.layout() if (Align.isCenterHorizontal(page.scrollAlign))
if (scrollX < 0f) // was marked to center on first show page.scrollX = (page.content.width - this.width) / 2
scrollX = ((content.width - this@TabbedPager.width) / 2).coerceIn(0f, contentScroll.maxX) else if (Align.isRight(page.scrollAlign))
contentScroll.scrollX = scrollX page.scrollX = Float.MAX_VALUE // ScrollPane _will_ clamp this
contentScroll.scrollY = scrollY if (Align.isCenterVertical(page.scrollAlign))
contentScroll.updateVisualScroll() page.scrollY = (page.content.height - this.height) / 2
headerScroll.let { else if (Align.isBottom(page.scrollAlign))
it.scrollX = (buttonX + (buttonW - it.width) / 2).coerceIn(0f, it.maxX) page.scrollY = Float.MAX_VALUE // ScrollPane _will_ clamp this
} page.scrollAlign = 0 // once only
onActivation?.invoke(index, button.name)
} }
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()
contentScroll.scrollX = page.scrollX
contentScroll.scrollY = page.scrollY
contentScroll.updateVisualScroll()
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 return true
} }
/** Change the selected page by using its caption. /** 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 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. * @return `true` if the page was successfully changed.
*/ */
fun selectPage(caption: String) = selectPage(getPageIndex(caption)) fun selectPage(caption: String, centerButton: Boolean = true) = selectPage(getPageIndex(caption), centerButton)
private fun selectPage(page: PageState) = selectPage(getPageIndex(page)) private fun selectPage(page: PageState) = selectPage(getPageIndex(page), centerButton = false)
/** Change the disabled property of a page by its index. /** Change the disabled property of a page by its index.
* @return previous value or `false` if index invalid. * @return previous value or `false` if index invalid.
@ -243,34 +423,41 @@ class TabbedPager(
if (index !in 0 until pages.size) return if (index !in 0 until pages.size) return
val isActive = index == activePage val isActive = index == activePage
if (isActive) selectPage(-1) if (isActive) selectPage(-1)
pages[index].content = content pages[index].let {
it.content = content
measureContent(it)
}
if (isActive) selectPage(index) if (isActive) selectPage(index)
} }
/** Replace a page's [content] and [fixedContent] by its [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 if (index !in 0 until pages.size) return
val isActive = index == activePage val isActive = index == activePage
if (isActive) selectPage(-1) if (isActive) selectPage(-1)
pages[index].content = content pages[index].let {
pages[index].fixedContent = fixedContent it.content = content
it.fixedContent = fixedContent
measureContent(it)
}
if (isActive) selectPage(index) if (isActive) selectPage(index)
} }
/** Replace a page's [content] by its [caption]. */ /** Replace a page's [content] by its [caption]. */
fun replacePage(caption: String, content: Actor) = replacePage(getPageIndex(caption), content) fun replacePage(caption: String, content: Actor) = replacePage(getPageIndex(caption), content)
/** Replace a page's [content] and [fixedContent] by its [caption]. */ /** 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! /** 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 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 content [Actor] to show in the lower area when this page is selected.
* @param icon Actor, typically an [Image], to show before the caption. * @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 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 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 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 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 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. * @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). * @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, secret: Boolean = false,
disabled: Boolean = false, disabled: Boolean = false,
shortcutKey: KeyCharAndCode = KeyCharAndCode.UNKNOWN, shortcutKey: KeyCharAndCode = KeyCharAndCode.UNKNOWN,
fixedContent: WidgetGroup? = null, scrollAlign: Int = Align.top,
syncScroll: Boolean = true,
fixedContent: Actor? = null,
onDeactivation: ((Int, String, Float) -> Unit)? = null, onDeactivation: ((Int, String, Float) -> Unit)? = null,
onActivation: ((Int, String) -> Unit)? = null onActivation: ((Int, String) -> Unit)? = null
): Int { ): Int {
@ -299,10 +488,11 @@ class TabbedPager(
icon = icon, icon = icon,
iconSize = iconSize, iconSize = iconSize,
shortcutKey = shortcutKey, shortcutKey = shortcutKey,
scrollAlign = scrollAlign,
syncScroll = syncScroll,
pager = this pager = this
) )
page.button.apply { page.button.apply {
name = caption // enable finding pages by untranslated caption without needing our own field
isEnabled = !disabled isEnabled = !disabled
onClick { onClick {
selectPage(page) selectPage(page)
@ -358,20 +548,15 @@ class TabbedPager(
private fun getPageIndex(page: PageState) = pages.indexOf(page) private fun getPageIndex(page: PageState) = pages.indexOf(page)
private fun measureContent(group: WidgetGroup) { private fun measureContent(page: PageState) {
group.packIfNeeded() val dimFixedH = DimensionMeasurement(dimH.limit)
var needLayout = false val dimContentH = DimensionMeasurement(dimH.limit)
val contentWidth = min(group.width, limitWidth) dimW.measureWidth(page.fixedContent as? WidgetGroup)
if (contentWidth > preferredWidth) { dimFixedH.measureHeight(page.fixedContent as? WidgetGroup)
preferredWidth = contentWidth page.fixedHeight = dimFixedH.min
needLayout = true dimW.measureWidth(page.content as? WidgetGroup)
} dimContentH.measureHeight(page.content as? WidgetGroup)
val contentHeight = min(group.height, limitHeight) dimH.combine(headerHeight, dimFixedH, dimContentH)
if (contentHeight > preferredHeight) {
preferredHeight = contentHeight
needLayout = true
}
if (needLayout && activePage >= 0) invalidateHierarchy()
} }
private fun addAndShowPage(page: PageState, insertBefore: Int): Int { private fun addAndShowPage(page: PageState, insertBefore: Int): Int {
@ -393,14 +578,7 @@ class TabbedPager(
for (i in newIndex + 1 until pages.size) for (i in newIndex + 1 until pages.size)
pages[i].buttonX += page.buttonW pages[i].buttonX += page.buttonW
// Content Sizing measureContent(page)
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
keyPressDispatcher?.set(page.shortcutKey) { selectPage(newIndex) } 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. * - because Gdx auto layout reports wrong dimensions on scaled actors.
*/ */
class UncivTooltip <T: Actor>( class UncivTooltip <T: Actor>(
val target: Group, val target: Actor,
val content: T, val content: T,
val targetAlign: Int = Align.topRight, val targetAlign: Int = Align.topRight,
val tipAlign: 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 always override requirement: presence of physical keyboard
* @param tipAlign Point on the Tooltip to align with the top right of the [target] * @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 if (!(always || KeyPressDispatcher.keyboardAvailable) || text.isEmpty()) return
val label = text.toLabel(ImageGetter.getBlue(), 38) 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 size _Vertical_ size of the entire Tooltip including background
* @param always override requirement: presence of physical keyboard * @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) 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 size _Vertical_ size of the entire Tooltip including background
* @param always override requirement: presence of physical keyboard * @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) if (key != KeyCharAndCode.UNKNOWN)
addTooltip(key.toString().tr(), size, always) addTooltip(key.toString().tr(), size, always)
} }