mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 13:55:54 -04:00
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:
parent
e8ab73df98
commit
2f035e55f7
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user