mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-23 03:23:17 -04:00
Sortable unit overview (#11521)
* SortableGrid architecture changes: defaultSort instead of defaultDescending * SortableGrid architecture changes: Header cell actor management * SortableGrid architecture changes: General Services and reusable defaults * Fix EmpireOverviewScreen sometimes forgetting the last active tab * Reimplement Unit Overview using SortableGrid (but dropping unit supply) * Get UnitSupplyTable back into Unit Overview * Fix unit overview does not know PromotionPickerScreen changed the name * Simplify update after rename * Fix "Ooops" wrong value in ranged strength column
This commit is contained in:
parent
9d8484cf75
commit
4b2f5e468d
@ -1,15 +1,23 @@
|
||||
package com.unciv.ui.components
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.widgets.SortableGrid
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
|
||||
/**
|
||||
* This defines all behaviour of a sortable Grid per column through overridable parts:
|
||||
* - [isVisible] can hide a column
|
||||
* - [align], [fillX], [expandX], [equalizeHeight] control geometry
|
||||
* - [getComparator] or [getEntryValue] control sorting, [defaultDescending] the initial order
|
||||
* - [getHeaderIcon], [headerTip] and [headerTipHideIcons] define how the header row looks
|
||||
* - [getComparator] or [getEntryValue] control sorting, [defaultSort] the initial order
|
||||
* - [getHeaderActor], [headerTip] and [headerTipHideIcons] define how the header row looks
|
||||
* - [getEntryValue] or [getEntryActor] define what the cells display
|
||||
* - [getEntryValue] or [getTotalsActor] define what the totals row displays
|
||||
* @param IT The item type - what defines the row
|
||||
@ -34,9 +42,9 @@ interface ISortableGridContentProvider<IT, ACT> {
|
||||
/** When overridden `true`, the entry cells of this column will be equalized to their max height */
|
||||
val equalizeHeight: Boolean
|
||||
|
||||
/** When `true` the column will be sorted descending when the user switches sort to it. */
|
||||
/** Default sort direction when a column is first sorted - can be None to disable sorting entirely for this column. */
|
||||
// Relevant for visuals (simply inverting the comparator would leave the displayed arrow not matching)
|
||||
val defaultDescending: Boolean
|
||||
val defaultSort: SortableGrid.SortDirection
|
||||
|
||||
/** @return whether the column should be rendered */
|
||||
fun isVisible(gameInfo: GameInfo): Boolean = true
|
||||
@ -47,8 +55,10 @@ interface ISortableGridContentProvider<IT, ACT> {
|
||||
*/
|
||||
fun getComparator(): Comparator<IT> = compareBy { item: IT -> getEntryValue(item) }
|
||||
|
||||
/** Factory for the header cell [Actor] */
|
||||
fun getHeaderIcon(iconSize: Float): Actor?
|
||||
/** Factory for the header cell [Actor]
|
||||
* @param iconSize Suggestion for icon size passed down from [SortableGrid] constructor, intended to scale the grid header. If the actor is not an icon, treat as height.
|
||||
*/
|
||||
fun getHeaderActor(iconSize: Float): Actor?
|
||||
|
||||
/** A getter for the numeric value to display in a cell */
|
||||
fun getEntryValue(item: IT): Int
|
||||
@ -57,7 +67,8 @@ interface ISortableGridContentProvider<IT, ACT> {
|
||||
* - By default displays the (numeric) result of [getEntryValue].
|
||||
* - [actionContext] can be used to define `onClick` actions.
|
||||
*/
|
||||
fun getEntryActor(item: IT, iconSize: Float, actionContext: ACT): Actor?
|
||||
fun getEntryActor(item: IT, iconSize: Float, actionContext: ACT): Actor? =
|
||||
getEntryValue(item).toCenteredLabel()
|
||||
|
||||
/** Factory for totals cell [Actor]
|
||||
* - By default displays the sum over [getEntryValue].
|
||||
@ -66,6 +77,21 @@ interface ISortableGridContentProvider<IT, ACT> {
|
||||
* - On the other hand, a sum may not be meaningful even if the cells are numbers - to leave
|
||||
* the total empty override to return `null`.
|
||||
*/
|
||||
fun getTotalsActor(items: Iterable<IT>): Actor?
|
||||
fun getTotalsActor(items: Iterable<IT>): Actor? =
|
||||
items.sumOf { getEntryValue(it) }.toCenteredLabel()
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val collator = UncivGame.Current.settings.getCollatorFromLocale()
|
||||
|
||||
@JvmStatic
|
||||
fun getCircledIcon(path: String, iconSize: Float, circleColor: Color = Color.LIGHT_GRAY) =
|
||||
ImageGetter.getImage(path)
|
||||
.apply { color = Color.BLACK }
|
||||
.surroundWithCircle(iconSize, color = circleColor)
|
||||
|
||||
@JvmStatic
|
||||
fun Int.toCenteredLabel(): Label =
|
||||
this.toLabel().apply { setAlignment(Align.center) }
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,11 @@ import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.Layout
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.ui.components.ISortableGridContentProvider
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
@ -15,6 +18,7 @@ import com.unciv.ui.components.extensions.center
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.IconCircleGroup
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
|
||||
@ -30,7 +34,10 @@ import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
class SortableGrid<IT, ACT, CT: ISortableGridContentProvider<IT, ACT>> (
|
||||
/** Provides the columns to render as [ISortableGridContentProvider] instances */
|
||||
private val columns: Iterable<CT>,
|
||||
/** Provides the actual "data" as in one object per row that can then be passed to [ISortableGridContentProvider] methods to fetch cell content */
|
||||
/** Provides the actual "data" as in one object per row that can then be passed to [ISortableGridContentProvider] methods to fetch cell content.
|
||||
*
|
||||
* Note: If your initial [sortState] has [SortDirection.None], then enumeration order of this will determine initial presentation order.
|
||||
*/
|
||||
private val data: Iterable<IT>,
|
||||
/** Passed to [ISortableGridContentProvider.getEntryActor] where it can be used to define `onClick` actions. */
|
||||
private val actionContext: ACT,
|
||||
@ -81,10 +88,10 @@ class SortableGrid<IT, ACT, CT: ISortableGridContentProvider<IT, ACT>> (
|
||||
}
|
||||
|
||||
private val headerRow = Table(skin)
|
||||
private val headerIcons = hashMapOf<CT, HeaderGroup>()
|
||||
private val headerElements = hashMapOf<CT, IHeaderElement>()
|
||||
private val sortSymbols = hashMapOf<Boolean, Label>()
|
||||
|
||||
private val details = Table(skin)
|
||||
val details = Table(skin) // public otherwise can't be used in reified public method
|
||||
private val totalsRow = Table(skin)
|
||||
|
||||
init {
|
||||
@ -130,20 +137,33 @@ class SortableGrid<IT, ACT, CT: ISortableGridContentProvider<IT, ACT>> (
|
||||
sortSymbols[true] = "↓".toLabel() // U+FFEC
|
||||
|
||||
for (column in columns) {
|
||||
val group = HeaderGroup(column)
|
||||
headerIcons[column] = group
|
||||
headerRow.add(group).size(iconSize).align(column.align)
|
||||
.fill(column.fillX, false).expand(column.expandX, false)
|
||||
val element = getHeaderElement(column)
|
||||
headerElements[column] = element
|
||||
val cell = headerRow.add(element.outerActor)
|
||||
element.sizeCell(cell)
|
||||
cell.align(column.align).fill(column.fillX, false).expand(column.expandX, false)
|
||||
}
|
||||
}
|
||||
|
||||
/** Calls [updateHeader] and [updateDetails] but not [updateCallback].
|
||||
*
|
||||
* Clients can call this if some data change affects cell content and sorting.
|
||||
* Note there is optimization potential here - a mechanism that updates one cell, and resorts only if it is in the currently sorted column
|
||||
*/
|
||||
fun update() {
|
||||
updateHeader()
|
||||
updateDetails()
|
||||
}
|
||||
|
||||
/** Update the sort direction icons of the header */
|
||||
fun updateHeader() {
|
||||
for (column in columns) {
|
||||
val sortDirection = if (sortState.sortedBy == column) sortState.direction else SortDirection.None
|
||||
headerIcons[column]?.setSortState(sortDirection)
|
||||
headerElements[column]?.setSortState(sortDirection)
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild the grid cells to update and/or resort the data */
|
||||
fun updateDetails() {
|
||||
details.clear()
|
||||
if (data.none()) return
|
||||
@ -190,64 +210,154 @@ class SortableGrid<IT, ACT, CT: ISortableGridContentProvider<IT, ACT>> (
|
||||
fun SortDirection.inverted() = when {
|
||||
this == SortDirection.Ascending -> SortDirection.Descending
|
||||
this == SortDirection.Descending -> SortDirection.Ascending
|
||||
sortBy.defaultDescending -> SortDirection.Descending
|
||||
else -> SortDirection.Ascending
|
||||
else -> sortBy.defaultSort
|
||||
}
|
||||
val direction = if (sortState.sortedBy == sortBy) sortState.direction else SortDirection.None
|
||||
setSort(sortBy, direction.inverted())
|
||||
}
|
||||
|
||||
sortState.run {
|
||||
if (sortedBy == sortBy) {
|
||||
direction = direction.inverted()
|
||||
} else {
|
||||
sortedBy = sortBy
|
||||
direction = SortDirection.None.inverted()
|
||||
}
|
||||
}
|
||||
fun setSort(sortBy: CT, direction: SortDirection) {
|
||||
sortState.sortedBy = sortBy
|
||||
sortState.direction = direction
|
||||
|
||||
// Rebuild header content to show sort state
|
||||
updateHeader()
|
||||
// Sort the table: clear and fill with sorted data
|
||||
updateDetails()
|
||||
// And resort the table: clear and fill with sorted data
|
||||
update()
|
||||
fireCallback()
|
||||
}
|
||||
|
||||
// We must be careful - this is an inner class in order to have access to the SortableGrid
|
||||
// type parameters, but that also means we have access to this@SortableGrid - automatically.
|
||||
// Any unqualified method calls not implemented in this or a superclass will silently try a
|
||||
// method offered by Table! Thus all the explicit `this` - to be really safe.
|
||||
//
|
||||
// Using Group to overlay an optional sort symbol on top of the icon - we could also
|
||||
// do HorizontalGroup to have them side by side. Also, note this is not a WidgetGroup
|
||||
// so all layout details are left to the container - in this case, a Table.Cell
|
||||
// This will knowingly place the arrow partly outside the Group bounds.
|
||||
/** Wrap icon and sort symbol for a header cell */
|
||||
inner class HeaderGroup(column: CT) : Group() {
|
||||
private val icon = column.getHeaderIcon(iconSize)
|
||||
private var sortShown: SortDirection = SortDirection.None
|
||||
/** Find the first Cell that contains an Actor of type [T] that matches [predicate].
|
||||
* @return `null` if no such Actor found */
|
||||
inline fun <reified T : Actor> findCell(predicate: (T) -> Boolean): Cell<T>? =
|
||||
details.cells.asSequence()
|
||||
.filterIsInstance<Cell<T>>() // does not remove cells with null actors, still necessary so the return type is fulfilled
|
||||
.firstOrNull { it.actor is T && predicate(it.actor) }
|
||||
|
||||
init {
|
||||
this.isTransform = false
|
||||
this.setSize(iconSize, iconSize)
|
||||
if (icon != null) {
|
||||
this.onClick { toggleSort(column) }
|
||||
icon.setSize(iconSize, iconSize)
|
||||
icon.center(this)
|
||||
if (column.headerTip.isNotEmpty())
|
||||
icon.addTooltip(column.headerTip, 18f, tipAlign = Align.center, hideIcons = column.headerTipHideIcons)
|
||||
this.addActor(icon)
|
||||
}
|
||||
}
|
||||
/** Find the first Cell that contains an Actor of type [T] with the given [name][Actor.name].
|
||||
*
|
||||
* Not to be confused with [Group.findActor], which does a recursive search over children, does not filter by the type, and returns the actor.
|
||||
* @return `null` if no such Actor found */
|
||||
inline fun <reified T : Actor> findCell(name: String): Cell<T>? =
|
||||
findCell { it.name == name }
|
||||
|
||||
|
||||
// We must be careful - the implementations of IHeaderElement are inner classes in order to have access
|
||||
// to the SortableGrid type parameters, but that also means we have access to this@SortableGrid - automatically.
|
||||
// Any unqualified method calls not implemented in this or a superclass will silently try a
|
||||
// method offered by Table! Thus make sure any Actor methods run for the contained actors.
|
||||
|
||||
/** Wrap icon, label or other Actor and sort symbol for a header cell.
|
||||
*
|
||||
* Not an Actor but a wrapper that contains [outerActor] which goes into the actual header.
|
||||
*
|
||||
* Where/how the sort symbol is rendered depends on the type returned by [ISortableGridContentProvider.getHeaderActor].
|
||||
* Instantiate through [getHeaderElement] - can be [EmptyHeaderElement], [LayoutHeaderElement] or [IconHeaderElement].
|
||||
*/
|
||||
// Note - not an Actor because Actor is not an interface. Otherwise we *could* build a class that **is** an Actor which can be
|
||||
// implemented by any Actor subclass but also carry additional fields and methods - via delegation.
|
||||
interface IHeaderElement {
|
||||
val outerActor: Actor
|
||||
val headerActor: Actor?
|
||||
var sortShown: SortDirection
|
||||
|
||||
/** Show or remove the sort symbol.
|
||||
* @param showSort None removes the symbol, Ascending shows an up arrow, Descending a down arrow */
|
||||
fun setSortState(showSort: SortDirection) {
|
||||
if (showSort == sortShown) return
|
||||
for (symbol in sortSymbols.values)
|
||||
removeActor(symbol) // Important: Does nothing if the actor is not our child
|
||||
sortShown = showSort
|
||||
if (showSort == SortDirection.None) return
|
||||
val sortSymbol = sortSymbols[showSort == SortDirection.Descending]!!
|
||||
* @param newSort None removes the symbol, Ascending shows an up arrow, Descending a down arrow */
|
||||
fun setSortState(newSort: SortDirection)
|
||||
|
||||
/** Internal: Used by common implementation for [setSortState] */
|
||||
fun removeSortSymbol(sortSymbol: Label)
|
||||
/** Internal: Used by common implementation for [setSortState] */
|
||||
fun showSortSymbol(sortSymbol: Label)
|
||||
|
||||
/** Override in case the implementation has specific requirements for the Table.Cell its [outerActor] is hosted in. */
|
||||
fun sizeCell(cell: Cell<Actor>) {}
|
||||
}
|
||||
|
||||
private fun IHeaderElement.initActivationAndTooltip(column: CT) {
|
||||
if (column.defaultSort != SortDirection.None)
|
||||
outerActor.onClick { toggleSort(column) }
|
||||
if (column.headerTip.isNotEmpty())
|
||||
headerActor!!.addTooltip(column.headerTip, 18f, tipAlign = Align.center, hideIcons = column.headerTipHideIcons)
|
||||
}
|
||||
|
||||
private fun IHeaderElement.setSortStateImpl(newSort: SortDirection) {
|
||||
if (newSort == sortShown) return
|
||||
for (symbol in sortSymbols.values)
|
||||
removeSortSymbol(symbol) // Important: Does nothing if the actor is not our child
|
||||
sortShown = newSort
|
||||
if (newSort == SortDirection.None) return
|
||||
val sortSymbol = sortSymbols[newSort == SortDirection.Descending]!!
|
||||
showSortSymbol(sortSymbol)
|
||||
}
|
||||
|
||||
private inner class EmptyHeaderElement : IHeaderElement {
|
||||
override val outerActor = Actor()
|
||||
override val headerActor = null
|
||||
override var sortShown = SortDirection.None
|
||||
override fun setSortState(newSort: SortDirection) {}
|
||||
override fun removeSortSymbol(sortSymbol: Label) {}
|
||||
override fun showSortSymbol(sortSymbol: Label) {}
|
||||
}
|
||||
|
||||
/** Version of [IHeaderElement] that works fine for Image or IconCircleGroup and **overlays** the sort symbol on its lower right.
|
||||
* Also used for all non-Layout non-null returns from [ISortableGridContentProvider.getHeaderActor].
|
||||
*/
|
||||
// Note this is not a WidgetGroup and thus does not implement Layout, so all layout details are left to the container
|
||||
// - in this case, a Table.Cell. This will knowingly place the arrow partly outside the Group bounds.
|
||||
private inner class IconHeaderElement(column: CT, override val headerActor: Actor) : IHeaderElement {
|
||||
override val outerActor = Group()
|
||||
override var sortShown = SortDirection.None
|
||||
|
||||
init {
|
||||
outerActor.isTransform = false
|
||||
outerActor.setSize(iconSize, iconSize)
|
||||
outerActor.addActor(headerActor)
|
||||
headerActor.setSize(iconSize, iconSize)
|
||||
headerActor.center(outerActor)
|
||||
initActivationAndTooltip(column)
|
||||
}
|
||||
|
||||
override fun sizeCell(cell: Cell<Actor>) {
|
||||
cell.size(iconSize)
|
||||
}
|
||||
override fun setSortState(newSort: SortDirection) = setSortStateImpl(newSort)
|
||||
override fun removeSortSymbol(sortSymbol: Label) {
|
||||
outerActor.removeActor(sortSymbol)
|
||||
}
|
||||
override fun showSortSymbol(sortSymbol: Label) {
|
||||
sortSymbol.setPosition(iconSize - 2f, 0f)
|
||||
addActor(sortSymbol)
|
||||
outerActor.addActor(sortSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
/** Version of [IHeaderElement] for all Layout returns from [ISortableGridContentProvider.getHeaderActor].
|
||||
* Draws the sort symbol by rendering [headerActor] and the sort symbol side by side in a HorizontalGroup
|
||||
*/
|
||||
private inner class LayoutHeaderElement(column: CT, override val headerActor: Actor) : IHeaderElement {
|
||||
override val outerActor = HorizontalGroup()
|
||||
override var sortShown = SortDirection.None
|
||||
|
||||
init {
|
||||
outerActor.isTransform = false
|
||||
outerActor.align(column.align)
|
||||
outerActor.addActor(headerActor)
|
||||
initActivationAndTooltip(column)
|
||||
}
|
||||
override fun setSortState(newSort: SortDirection) = setSortStateImpl(newSort)
|
||||
override fun removeSortSymbol(sortSymbol: Label) {
|
||||
outerActor.removeActor(sortSymbol)
|
||||
}
|
||||
override fun showSortSymbol(sortSymbol: Label) {
|
||||
outerActor.addActor(sortSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHeaderElement(column: CT): IHeaderElement {
|
||||
return when (val headerActor = column.getHeaderActor(iconSize)) {
|
||||
null -> EmptyHeaderElement()
|
||||
is Image, is IconCircleGroup -> IconHeaderElement(column, headerActor) // They're also `is Layout`, but we want the overlaid header version
|
||||
is Layout -> LayoutHeaderElement(column, headerActor)
|
||||
else -> IconHeaderElement(column, headerActor) // We haven't got a better implementation for other non-Layout Actors.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,21 @@ package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.city.CityFlags
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.ISortableGridContentProvider
|
||||
import com.unciv.ui.components.ISortableGridContentProvider.Companion.collator
|
||||
import com.unciv.ui.components.ISortableGridContentProvider.Companion.getCircledIcon
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.widgets.SortableGrid
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.cityscreen.CityScreen
|
||||
import kotlin.math.roundToInt
|
||||
@ -34,9 +35,9 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
override val headerTip = "Name"
|
||||
override val align = Align.left
|
||||
override val fillX = true
|
||||
override val defaultDescending = false
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Ascending
|
||||
override fun getComparator() = compareBy<City, String>(collator) { it.name.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
override fun getHeaderActor(iconSize: Float) =
|
||||
ImageGetter.getUnitIcon("Settler")
|
||||
.surroundWithCircle(iconSize)
|
||||
override fun getEntryValue(item: City) = 0 // make sure that `stat!!` in the super isn't used
|
||||
@ -51,7 +52,7 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
|
||||
Status {
|
||||
override val headerTip = "Status\n(puppet, resistance or being razed)"
|
||||
override fun getHeaderIcon(iconSize: Float) = ImageGetter.getImage("OtherIcons/CityStatus")
|
||||
override fun getHeaderActor(iconSize: Float) = ImageGetter.getImage("OtherIcons/CityStatus")
|
||||
override fun getEntryValue(item: City) = when {
|
||||
item.isBeingRazed -> 3
|
||||
item.isInResistance() -> 2
|
||||
@ -72,7 +73,7 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
},
|
||||
|
||||
ConstructionIcon {
|
||||
override fun getHeaderIcon(iconSize: Float) = null
|
||||
override fun getHeaderActor(iconSize: Float) = null
|
||||
override fun getEntryValue(item: City) =
|
||||
item.cityConstructions.run { turnsToConstruction(currentConstructionFromQueue) }
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen): Actor? {
|
||||
@ -88,10 +89,10 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
override val expandX = false
|
||||
override val equalizeHeight = true
|
||||
override val headerTip = "Current construction"
|
||||
override val defaultDescending = false
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Ascending
|
||||
override fun getComparator() =
|
||||
compareBy<City, String>(collator) { it.cityConstructions.currentConstructionFromQueue.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
override fun getHeaderActor(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/Settings", iconSize)
|
||||
override fun getEntryValue(item: City) = 0
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen) =
|
||||
@ -124,10 +125,10 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
|
||||
WLTK {
|
||||
override val headerTip = "We Love The King Day"
|
||||
override val defaultDescending = false
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Ascending
|
||||
override fun getComparator() =
|
||||
super.getComparator().thenBy { it.demandedResource.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
override fun getHeaderActor(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/WLTK 2", iconSize, Color.TAN)
|
||||
override fun getEntryValue(item: City) =
|
||||
if (item.isWeLoveTheKingDayActive()) 1 else 0
|
||||
@ -153,10 +154,10 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
|
||||
Garrison {
|
||||
override val headerTip = "Garrisoned by unit"
|
||||
override val defaultDescending = false
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Ascending
|
||||
override fun getComparator() =
|
||||
compareBy<City, String>(collator) { it.getGarrison()?.name?.tr(hideIcons = true) ?: "" }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
override fun getHeaderActor(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/Shield", iconSize)
|
||||
override fun getEntryValue(item: City) =
|
||||
if (item.getGarrison() != null) 1 else 0
|
||||
@ -166,7 +167,7 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
val unitIcon = ImageGetter.getConstructionPortrait(unit.baseUnit.getIconName(), iconSize * 0.7f)
|
||||
unitIcon.addTooltip(unitName, 18f, tipAlign = Align.topLeft)
|
||||
unitIcon.onClick {
|
||||
actionContext.select(EmpireOverviewCategories.Units, UnitOverviewTab.getUnitIdentifier(unit) )
|
||||
actionContext.select(EmpireOverviewCategories.Units, UnitOverviewTabColumn.getUnitIdentifier(unit) )
|
||||
}
|
||||
return unitIcon
|
||||
}
|
||||
@ -185,8 +186,7 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
override val fillX = false
|
||||
override val expandX = false
|
||||
override val equalizeHeight = false
|
||||
override val defaultDescending = true
|
||||
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Descending
|
||||
//endregion
|
||||
//region Overridable methods
|
||||
|
||||
@ -194,7 +194,7 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
* - Must override unless a texture exists for "StatIcons/$name" - e.g. a [Stat] column or [Population].
|
||||
* - _Should_ be sized to [iconSize].
|
||||
*/
|
||||
override fun getHeaderIcon(iconSize: Float): Actor? =
|
||||
override fun getHeaderActor(iconSize: Float): Actor? =
|
||||
ImageGetter.getStatIcon(name)
|
||||
|
||||
/** A getter for the numeric value to display in a cell
|
||||
@ -206,34 +206,5 @@ enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOver
|
||||
override fun getEntryValue(item: City): Int =
|
||||
item.cityStats.currentCityStats[stat!!].roundToInt()
|
||||
|
||||
/** Factory for entry cell [Actor]
|
||||
* - By default displays the (numeric) result of [getEntryValue].
|
||||
* - [actionContext] will be the parent screen used to define `onClick` actions.
|
||||
*/
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen): Actor? =
|
||||
getEntryValue(item).toCenteredLabel()
|
||||
|
||||
//endregion
|
||||
|
||||
/** Factory for totals cell [Actor]
|
||||
* - By default displays the sum over [getEntryValue].
|
||||
* - Note a count may be meaningful even if entry cells display something other than a number,
|
||||
* In that case _not_ overriding this and supply a meaningful [getEntryValue] may be easier.
|
||||
* - On the other hand, a sum may not be meaningful even if the cells are numbers - to leave
|
||||
* the total empty override to return `null`.
|
||||
*/
|
||||
override fun getTotalsActor(items: Iterable<City>): Actor? =
|
||||
items.sumOf { getEntryValue(it) }.toCenteredLabel()
|
||||
|
||||
companion object {
|
||||
private val collator = UncivGame.Current.settings.getCollatorFromLocale()
|
||||
|
||||
private fun getCircledIcon(path: String, iconSize: Float, circleColor: Color = Color.LIGHT_GRAY) =
|
||||
ImageGetter.getImage(path)
|
||||
.apply { color = Color.BLACK }
|
||||
.surroundWithCircle(iconSize, color = circleColor)
|
||||
|
||||
private fun Int.toCenteredLabel(): Label =
|
||||
this.toLabel().apply { setAlignment(Align.center) }
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ abstract class EmpireOverviewTab (
|
||||
open val persistableData = persistedData ?: EmpireOverviewTabPersistableData()
|
||||
|
||||
override fun activated(index: Int, caption: String, pager: TabbedPager) {
|
||||
if (caption.isEmpty()) return
|
||||
overviewScreen.game.settings.lastOverviewPage = caption
|
||||
}
|
||||
|
||||
|
@ -1,44 +1,16 @@
|
||||
package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.badlogic.gdx.scenes.scene2d.Action
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.GUI
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.UpgradeUnitAction
|
||||
import com.unciv.models.ruleset.unit.BaseUnit
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.brighten
|
||||
import com.unciv.ui.components.extensions.center
|
||||
import com.unciv.ui.components.extensions.darken
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.extensions.toPrettyString
|
||||
import com.unciv.ui.components.fonts.Fonts
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.widgets.ExpanderTab
|
||||
import com.unciv.ui.components.widgets.SortableGrid
|
||||
import com.unciv.ui.components.widgets.TabbedPager
|
||||
import com.unciv.ui.components.widgets.UnitGroup
|
||||
import com.unciv.ui.images.IconTextButton
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popups.UnitUpgradeMenu
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
|
||||
import com.unciv.ui.screens.pickerscreens.UnitRenamePopup
|
||||
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
|
||||
import kotlin.math.abs
|
||||
|
||||
//TODO use SortableGrid
|
||||
/**
|
||||
* Supplies the Unit sub-table for the Empire Overview
|
||||
*/
|
||||
@ -47,26 +19,14 @@ class UnitOverviewTab(
|
||||
overviewScreen: EmpireOverviewScreen,
|
||||
persistedData: EmpireOverviewTabPersistableData? = null
|
||||
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
|
||||
class UnitTabPersistableData(
|
||||
class UnitTabPersistableData : EmpireOverviewTabPersistableData(), SortableGrid.ISortState<UnitOverviewTabColumn> {
|
||||
var scrollY: Float? = null
|
||||
) : EmpireOverviewTabPersistableData() {
|
||||
override fun isEmpty() = scrollY == null
|
||||
override var sortedBy: UnitOverviewTabColumn = UnitOverviewTabColumn.Name
|
||||
override var direction = SortableGrid.SortDirection.None
|
||||
override fun isEmpty() = scrollY == null && sortedBy == UnitOverviewTabColumn.Name && direction != SortableGrid.SortDirection.Descending
|
||||
}
|
||||
override val persistableData = (persistedData as? UnitTabPersistableData) ?: UnitTabPersistableData()
|
||||
|
||||
override fun activated(index: Int, caption: String, pager: TabbedPager) {
|
||||
if (persistableData.scrollY != null)
|
||||
pager.setPageScrollY(index, persistableData.scrollY!!)
|
||||
super.activated(index, caption, pager)
|
||||
}
|
||||
override fun deactivated(index: Int, caption: String, pager: TabbedPager) {
|
||||
persistableData.scrollY = pager.getPageScrollY(index)
|
||||
removeBlinkAction()
|
||||
}
|
||||
|
||||
private val supplyTableWidth = (overviewScreen.stage.width * 0.25f).coerceAtLeast(240f)
|
||||
private val unitListTable = Table() // could be `this` instead, extra nesting helps readability a little
|
||||
private val unitHeaderTable = Table()
|
||||
private val fixedContent = Table()
|
||||
|
||||
// used for select()
|
||||
@ -79,262 +39,74 @@ class UnitOverviewTab(
|
||||
blinkActor = null
|
||||
}
|
||||
|
||||
//todo The original did its own sort:
|
||||
/*
|
||||
val oldIterator = viewingPlayer.units.getCivUnits().sortedWith(
|
||||
compareBy(
|
||||
{ it.displayName() },
|
||||
{ !it.due },
|
||||
{ it.currentMovement <= Constants.minimumMovementEpsilon },
|
||||
{ abs(it.currentTile.position.x) + abs(it.currentTile.position.y) }
|
||||
)
|
||||
)
|
||||
*/
|
||||
// We're using getCivUnits enumeration order instead so far.
|
||||
// - Adding that sort to the data source using Sequence would be inefficient because it is re-enumerated on every resort, and user sorts would mean that initial sort would largely be overridden
|
||||
// - Materializing the sort result would only waste memory
|
||||
// - But - isn't getCivUnits() deterministic anyway - controls "Next Unit" order? actually, getCivUnitsStartingAtNextDue would give that, it slices by an internal pointer
|
||||
|
||||
//todo the comments and todo below are copied verbatim from CityOverviewTab - synergies?
|
||||
private val grid = SortableGrid(
|
||||
columns = UnitOverviewTabColumn.values().asIterable(),
|
||||
data = viewingPlayer.units.getCivUnits().asIterable(),
|
||||
actionContext = this,
|
||||
sortState = persistableData,
|
||||
iconSize = 20f,
|
||||
paddingVert = 5f,
|
||||
paddingHorz = 8f,
|
||||
separateHeader = true
|
||||
) { header, details, totals ->
|
||||
// Notes: header.parent is the LinkedScrollPane of TabbedPager. Its linked twin is details.parent.parent.parent however!
|
||||
// horizontal "slack" if available width > content width is taken up between SortableGrid and CityOverviewTab for the details,
|
||||
// but not so for the header. We must force the LinkedScrollPane somehow (no? how?) to do so - or the header Table itself.
|
||||
|
||||
equalizeColumns(details, header, totals)
|
||||
// todo Kludge! Positioning and alignment of the header Table within its parent has quirks when content width < stage width
|
||||
// This code should likely be included in SortableGrid anyway?
|
||||
if (header.width < this.width) header.width = this.width
|
||||
this.validate()
|
||||
}
|
||||
|
||||
override fun getFixedContent() = fixedContent
|
||||
|
||||
init {
|
||||
fixedContent.add(getUnitSupplyTable()).align(Align.top).padBottom(10f).row()
|
||||
fixedContent.add(unitHeaderTable.updateUnitHeaderTable())
|
||||
val supplyTableWidth = (overviewScreen.stage.width * 0.25f).coerceAtLeast(240f)
|
||||
val unitSupplyTable = UnitSupplyTable.create(overviewScreen, this, viewingPlayer, supplyTableWidth)
|
||||
fixedContent.add(unitSupplyTable).align(Align.top).padBottom(10f).row()
|
||||
fixedContent.add(grid.getHeader()).grow()
|
||||
top()
|
||||
add(unitListTable.updateUnitListTable())
|
||||
equalizeColumns(unitListTable, unitHeaderTable)
|
||||
add(grid)
|
||||
}
|
||||
|
||||
// Here overloads are simpler than a generic:
|
||||
private fun Table.addLabeledValue (label: String, value: Int) {
|
||||
add(label.toLabel()).left()
|
||||
add(value.toLabel()).right().row()
|
||||
override fun activated(index: Int, caption: String, pager: TabbedPager) {
|
||||
if (persistableData.scrollY != null)
|
||||
pager.setPageScrollY(index, persistableData.scrollY!!)
|
||||
super.activated(index, caption, pager)
|
||||
}
|
||||
private fun Table.addLabeledValue (label: String, value: String) {
|
||||
add(label.toLabel()).left()
|
||||
add(value.toLabel()).right().row()
|
||||
override fun deactivated(index: Int, caption: String, pager: TabbedPager) {
|
||||
persistableData.scrollY = pager.getPageScrollY(index)
|
||||
removeBlinkAction()
|
||||
}
|
||||
|
||||
private fun showWorldScreenAt(position: Vector2, unit: MapUnit?) {
|
||||
GUI.resetToWorldScreen()
|
||||
GUI.getMap().setCenterPosition(position, forceSelectUnit = unit)
|
||||
}
|
||||
private fun showWorldScreenAt(unit: MapUnit) = showWorldScreenAt(unit.currentTile.position, unit)
|
||||
private fun showWorldScreenAt(tile: Tile) = showWorldScreenAt(tile.position, null)
|
||||
|
||||
private fun getUnitSupplyTable(): ExpanderTab {
|
||||
val stats = viewingPlayer.stats
|
||||
val deficit = stats.getUnitSupplyDeficit()
|
||||
val icon = if (deficit <= 0) null else Group().apply {
|
||||
isTransform = false
|
||||
setSize(36f, 36f)
|
||||
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
||||
image.color = Color.FIREBRICK
|
||||
image.setSize(36f, 36f)
|
||||
image.center(this)
|
||||
image.setOrigin(Align.center)
|
||||
addActor(image)
|
||||
}
|
||||
return ExpanderTab(
|
||||
title = "Unit Supply",
|
||||
fontSize = Constants.defaultFontSize,
|
||||
icon = icon,
|
||||
startsOutOpened = deficit > 0,
|
||||
defaultPad = 0f,
|
||||
expanderWidth = supplyTableWidth,
|
||||
onChange = {
|
||||
overviewScreen.resizePage(this)
|
||||
}
|
||||
) {
|
||||
it.defaults().pad(5f).fill(false)
|
||||
it.background = BaseScreen.skinStrings.getUiBackground(
|
||||
"OverviewScreen/UnitOverviewTab/UnitSupplyTable",
|
||||
tintColor = BaseScreen.skinStrings.skinConfig.baseColor.darken(0.6f)
|
||||
)
|
||||
it.addLabeledValue("Base Supply", stats.getBaseUnitSupply())
|
||||
it.addLabeledValue("Cities", stats.getUnitSupplyFromCities())
|
||||
it.addLabeledValue("Population", stats.getUnitSupplyFromPop())
|
||||
it.addSeparator()
|
||||
it.addLabeledValue("Total Supply", stats.getUnitSupply())
|
||||
it.addLabeledValue("In Use", viewingPlayer.units.getCivUnitsSize())
|
||||
it.addSeparator()
|
||||
it.addLabeledValue("Supply Deficit", deficit)
|
||||
it.addLabeledValue("Production Penalty", "${stats.getUnitSupplyProductionPenalty().toInt()}%")
|
||||
if (deficit > 0) {
|
||||
val penaltyLabel = "Increase your supply or reduce the amount of units to remove the production penalty"
|
||||
.toLabel(Color.FIREBRICK)
|
||||
penaltyLabel.wrap = true
|
||||
it.add(penaltyLabel).colspan(2).left()
|
||||
.width(supplyTableWidth).row()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Table.updateUnitHeaderTable(): Table {
|
||||
defaults().pad(5f)
|
||||
add("Name".toLabel())
|
||||
add() // Column: edit-name
|
||||
add("Action".toLabel())
|
||||
add(Fonts.strength.toString().toLabel())
|
||||
add(Fonts.rangedStrength.toString().toLabel())
|
||||
add(Fonts.movement.toString().toLabel())
|
||||
add("Closest city".toLabel())
|
||||
add("Promotions".toLabel())
|
||||
add("Upgrade".toLabel())
|
||||
add("Health".toLabel())
|
||||
addSeparator().padBottom(0f)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun Table.updateUnitListTable(): Table {
|
||||
clear()
|
||||
val game = overviewScreen.game
|
||||
defaults().pad(5f)
|
||||
|
||||
for (unit in viewingPlayer.units.getCivUnits().sortedWith(
|
||||
compareBy({ it.displayName() },
|
||||
{ !it.due },
|
||||
{ it.currentMovement <= Constants.minimumMovementEpsilon },
|
||||
{ abs(it.currentTile.position.x) + abs(it.currentTile.position.y) })
|
||||
)) {
|
||||
val baseUnit = unit.baseUnit
|
||||
|
||||
// Unit button column - name, health, fortified, sleeping, embarked are visible here
|
||||
val button = IconTextButton(
|
||||
unit.displayName(),
|
||||
UnitGroup(unit, 20f).apply { if (!unit.isIdle()) color.a = 0.5f },
|
||||
fontColor = if (unit.isIdle()) Color.WHITE else Color.LIGHT_GRAY
|
||||
)
|
||||
button.name = getUnitIdentifier(unit) // Marker to find a unit in select()
|
||||
button.onClick {
|
||||
showWorldScreenAt(unit)
|
||||
}
|
||||
add(button).fillX()
|
||||
|
||||
// Column: edit-name
|
||||
val editIcon = ImageGetter.getImage("OtherIcons/Pencil").apply { this.color = Color.WHITE }.surroundWithCircle(30f, true, Color.valueOf("000c31"))
|
||||
editIcon.onClick {
|
||||
UnitRenamePopup(
|
||||
screen = overviewScreen,
|
||||
unit = unit,
|
||||
actionOnClose = {
|
||||
overviewScreen.game.replaceCurrentScreen(
|
||||
EmpireOverviewScreen(viewingPlayer, selection = getUnitIdentifier(unit))
|
||||
)
|
||||
})
|
||||
}
|
||||
add(editIcon)
|
||||
|
||||
// Column: action
|
||||
fun getWorkerActionText(unit: MapUnit): String? = when {
|
||||
// See UnitTurnManager.endTurn, if..workOnImprovement or UnitGroup.getActionImage: similar logic
|
||||
!unit.cache.hasUniqueToBuildImprovements -> null
|
||||
unit.currentMovement == 0f -> null
|
||||
unit.currentTile.improvementInProgress == null -> null
|
||||
!unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!) -> null
|
||||
else -> unit.currentTile.improvementInProgress
|
||||
}
|
||||
fun getActionText(unit: MapUnit): String? {
|
||||
val workerText by lazy { getWorkerActionText(unit) }
|
||||
return when {
|
||||
unit.action == null -> workerText
|
||||
unit.isFortified() -> UnitActionType.Fortify.value
|
||||
unit.isMoving() -> "Moving"
|
||||
unit.isAutomated() && workerText != null -> "[$workerText] ${Fonts.automate}"
|
||||
else -> unit.action
|
||||
}
|
||||
}
|
||||
add(getActionText(unit)?.toLabel())
|
||||
|
||||
// Columns: strength, ranged
|
||||
if (baseUnit.strength > 0) add(baseUnit.strength.toLabel()) else add()
|
||||
if (baseUnit.rangedStrength > 0) add(baseUnit.rangedStrength.toLabel()) else add()
|
||||
add(unit.getMovementString().toLabel())
|
||||
|
||||
// Closest city column
|
||||
val closestCity =
|
||||
unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() }
|
||||
val cityColor = if (unit.getTile() == closestCity) Color.FOREST.brighten(0.5f) else Color.WHITE
|
||||
if (closestCity != null)
|
||||
add(closestCity.getCity()!!.name.toLabel(cityColor).apply {
|
||||
onClick { showWorldScreenAt(closestCity) }
|
||||
})
|
||||
else add()
|
||||
|
||||
// Promotions column
|
||||
val promotionsTable = Table()
|
||||
updatePromotionsTable(promotionsTable, unit)
|
||||
promotionsTable.onClick {
|
||||
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
|
||||
game.pushScreen(PromotionPickerScreen(unit) {
|
||||
updatePromotionsTable(promotionsTable, unit)
|
||||
})
|
||||
}
|
||||
}
|
||||
add(promotionsTable)
|
||||
|
||||
// Upgrade column
|
||||
val upgradeTable = Table()
|
||||
updateUpgradeTable(upgradeTable, unit)
|
||||
add(upgradeTable)
|
||||
|
||||
// Numeric health column - there's already a health bar on the button, but...?
|
||||
if (unit.health < 100) add(unit.health.toLabel()) else add()
|
||||
row()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun updateUpgradeTable(table: Table, unit: MapUnit){
|
||||
table.clearChildren()
|
||||
|
||||
val unitActions = UnitActionsUpgrade.getUpgradeActionAnywhere(unit)
|
||||
if (unitActions.none()) table.add()
|
||||
for (unitAction in unitActions){
|
||||
val enable = unitAction.action != null && viewingPlayer.isCurrentPlayer() &&
|
||||
GUI.isAllowedChangeState()
|
||||
val unitToUpgradeTo = (unitAction as UpgradeUnitAction).unitToUpgradeTo
|
||||
val selectKey = getUnitIdentifier(unit, unitToUpgradeTo)
|
||||
val upgradeIcon = ImageGetter.getUnitIcon(unitToUpgradeTo.name,
|
||||
if (enable) Color.GREEN else Color.GREEN.darken(0.5f))
|
||||
upgradeIcon.onClick {
|
||||
UnitUpgradeMenu(overviewScreen.stage, upgradeIcon, unit, unitAction, enable) {
|
||||
unitListTable.updateUnitListTable()
|
||||
select(selectKey)
|
||||
}
|
||||
}
|
||||
table.add(upgradeIcon).size(28f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePromotionsTable(table: Table, unit: MapUnit) {
|
||||
table.clearChildren()
|
||||
|
||||
// getPromotions goes by json order on demand - so this is the same sorting as on UnitTable,
|
||||
// but not same as on PromotionPickerScreen (which e.g. tries to respect prerequisite proximity)
|
||||
val promotions = unit.promotions.getPromotions(true)
|
||||
val showPromoteStar = unit.promotions.canBePromoted()
|
||||
if (promotions.any()) {
|
||||
val iconCount = promotions.count() + (if (showPromoteStar) 1 else 0)
|
||||
val numberOfLines = (iconCount - 1) / 8 + 1 // Int math: -1,/,+1 means divide rounding *up*
|
||||
val promotionsPerLine = (iconCount - 1) / numberOfLines + 1
|
||||
for (linePromotions in promotions.chunked(promotionsPerLine)) {
|
||||
for (promotion in linePromotions) {
|
||||
table.add(ImageGetter.getPromotionPortrait(promotion.name))
|
||||
}
|
||||
if (linePromotions.size == promotionsPerLine) table.row()
|
||||
}
|
||||
}
|
||||
|
||||
if (!showPromoteStar) return
|
||||
table.add(
|
||||
ImageGetter.getImage("OtherIcons/Star").apply {
|
||||
color = if (GUI.isAllowedChangeState() && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
||||
Color.GOLDENROD
|
||||
else Color.GOLDENROD.darken(0.25f)
|
||||
}
|
||||
).size(24f).padLeft(8f)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getUnitIdentifier(unit: MapUnit, unitToUpgradeTo: BaseUnit? = null): String {
|
||||
val name = unitToUpgradeTo?.name ?: unit.name
|
||||
return "$name@${unit.getTile().position.toPrettyString()}"
|
||||
}
|
||||
}
|
||||
internal fun update() = grid.update()
|
||||
|
||||
override fun select(selection: String): Float? {
|
||||
val cell = unitListTable.cells.asSequence()
|
||||
.filter { it.actor is IconTextButton && it.actor.name == selection }
|
||||
.firstOrNull() ?: return null
|
||||
val button = cell.actor as IconTextButton
|
||||
val cell = grid.findCell<IconTextButton>(selection)
|
||||
?: return null
|
||||
val button = cell.actor
|
||||
val scrollY = (0 until cell.row)
|
||||
.map { unitListTable.getRowHeight(it) }.sum() -
|
||||
(parent.height - unitListTable.getRowHeight(cell.row)) / 2
|
||||
.map { grid.details.getRowHeight(it) }.sum() -
|
||||
(parent.height - grid.details.getRowHeight(cell.row)) / 2
|
||||
|
||||
removeBlinkAction()
|
||||
blinkAction = Actions.repeat(3, Actions.sequence(
|
||||
@ -345,5 +117,4 @@ class UnitOverviewTab(
|
||||
button.addAction(blinkAction)
|
||||
return scrollY
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,143 @@
|
||||
package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.ISortableGridContentProvider
|
||||
import com.unciv.ui.components.ISortableGridContentProvider.Companion.toCenteredLabel
|
||||
import com.unciv.ui.components.extensions.brighten
|
||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.fonts.Fonts
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.widgets.SortableGrid
|
||||
import com.unciv.ui.components.widgets.UnitGroup
|
||||
import com.unciv.ui.images.IconTextButton
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.pickerscreens.UnitRenamePopup
|
||||
|
||||
//todo Extending getEntryValue here to have a second String-based "channel" - could go into SortableGrid, possibly by defining a DataType per column???
|
||||
|
||||
enum class UnitOverviewTabColumn(
|
||||
private val headerLabel: String? = null,
|
||||
override val headerTip: String = "",
|
||||
private val isNumeric: Boolean = false
|
||||
) : ISortableGridContentProvider<MapUnit, UnitOverviewTab> {
|
||||
//region Enum Instances
|
||||
Name {
|
||||
override val fillX = true
|
||||
override fun getEntryString(item: MapUnit) = item.displayName().tr(hideIcons = true)
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab): Actor {
|
||||
// Unit button column - name, health, fortified, sleeping, embarked are visible here
|
||||
val button = IconTextButton(
|
||||
item.displayName(),
|
||||
UnitGroup(item, 20f).apply { if (!unit.isIdle()) color.a = 0.5f },
|
||||
fontColor = if (item.isIdle()) Color.WHITE else Color.LIGHT_GRAY
|
||||
)
|
||||
button.name = getUnitIdentifier(item) // Marker to find a unit in select()
|
||||
button.onClick {
|
||||
showWorldScreenAt(item)
|
||||
}
|
||||
return button
|
||||
}
|
||||
override fun getTotalsActor(items: Iterable<MapUnit>) = items.count().toCenteredLabel()
|
||||
},
|
||||
|
||||
EditName("") {
|
||||
override val defaultSort get() = SortableGrid.SortDirection.None
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab): Actor {
|
||||
val selectKey = getUnitIdentifier(item)
|
||||
val editIcon = ImageGetter.getImage("OtherIcons/Pencil")
|
||||
.apply { this.color = Color.WHITE }
|
||||
.surroundWithCircle(30f, true, Color(0x000c31))
|
||||
editIcon.onClick {
|
||||
UnitRenamePopup(actionContext.overviewScreen, item) {
|
||||
actionContext.update()
|
||||
actionContext.overviewScreen.select(EmpireOverviewCategories.Units, selectKey)
|
||||
}
|
||||
}
|
||||
return editIcon
|
||||
}
|
||||
},
|
||||
|
||||
Action {
|
||||
override fun getEntryString(item: MapUnit): String? = getActionText(item)
|
||||
},
|
||||
|
||||
Strength(Fonts.strength.toString(), "Strength", true) {
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Descending
|
||||
override fun getEntryValue(item: MapUnit) = item.baseUnit.strength
|
||||
},
|
||||
RangedStrength(Fonts.rangedStrength.toString(), "Ranged strength", true) {
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Descending
|
||||
override fun getEntryValue(item: MapUnit) = item.baseUnit.rangedStrength
|
||||
},
|
||||
Movement(Fonts.movement.toString(), "Movement", true) {
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Descending
|
||||
override fun getEntryString(item: MapUnit) = item.getMovementString()
|
||||
override fun getComparator() = compareBy<MapUnit> { it.getMaxMovement() }.thenBy { it.currentMovement }
|
||||
},
|
||||
|
||||
ClosestCity("Closest city") {
|
||||
//todo these overrides call a getTilesInDistance(3).firstOrNull loop independently and possibly repeatedly - caching?
|
||||
override fun getEntryString(item: MapUnit) = getClosestCityTile(item)?.getCity()?.name
|
||||
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab): Actor? {
|
||||
val closestCityTile = getClosestCityTile(item) ?: return null
|
||||
val cityColor = if (item.getTile() == closestCityTile) Color.FOREST.brighten(0.5f) else Color.WHITE
|
||||
val label = closestCityTile.getCity()!!.name.toLabel(fontColor = cityColor, alignment = Align.center)
|
||||
label.onClick { showWorldScreenAt(closestCityTile) }
|
||||
return label
|
||||
}
|
||||
private fun getClosestCityTile(item: MapUnit) = item.getTile()
|
||||
.getTilesInDistance(3).firstOrNull { it.isCityCenter() }
|
||||
},
|
||||
|
||||
Promotions(isNumeric = true) {
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Descending
|
||||
override fun getEntryValue(item: MapUnit) =
|
||||
(if (item.promotions.canBePromoted()) 10000 else 0) +
|
||||
item.promotions.promotions.size // Not numberOfPromotions - DO count free ones. Or sort by totalXpProduced?
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab) = getPromotionsTable(item, actionContext)
|
||||
},
|
||||
|
||||
Upgrade {
|
||||
//todo these overrides call UnitActionsUpgrade.getUpgradeActionAnywhere independently and possibly repeatedly - caching?
|
||||
override fun getEntryString(item: MapUnit) = getUpgradeSortString(item)
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab) = getUpgradeTable(item, actionContext)
|
||||
override fun getTotalsActor(items: Iterable<MapUnit>) = items.count { getUpgradeSortString(it) != null }.toCenteredLabel()
|
||||
},
|
||||
|
||||
Health(isNumeric = true) {
|
||||
override fun getEntryValue(item: MapUnit) = item.health
|
||||
override fun getEntryString(item: MapUnit) = if (item.health == 100) null else item.health.toString()
|
||||
override fun getTotalsActor(items: Iterable<MapUnit>) = items.count { it.health < 100 }.toCenteredLabel()
|
||||
},
|
||||
;
|
||||
//endregion
|
||||
|
||||
//region Overridden superclass fields
|
||||
override val align = Align.center
|
||||
override val fillX = false
|
||||
override val expandX = false
|
||||
override val equalizeHeight = false
|
||||
override val defaultSort get() = SortableGrid.SortDirection.Ascending
|
||||
//endregion
|
||||
|
||||
open fun getEntryString(item: MapUnit): String? = getEntryValue(item).takeIf { it > 0 }?.toString()
|
||||
|
||||
//region Overridden superclass methods
|
||||
override fun getHeaderActor(iconSize: Float) = (headerLabel ?: name).toLabel()
|
||||
override fun getEntryValue(item: MapUnit) = 0
|
||||
override fun getEntryActor(item: MapUnit, iconSize: Float, actionContext: UnitOverviewTab): Actor? =
|
||||
getEntryString(item)?.toLabel(alignment = Align.center)
|
||||
override fun getComparator() = if (isNumeric) super.getComparator()
|
||||
// Sort empty cells to the end by faking a `String.MAX_VALUE` - to do it properly would be a far more verbose Comparator subclass
|
||||
else compareBy(ISortableGridContentProvider.collator) { getEntryString(it)?.tr(hideIcons = true) ?: "\uD83D\uDE00zzz" }
|
||||
override fun getTotalsActor(items: Iterable<MapUnit>): Actor? = null
|
||||
//endregion
|
||||
|
||||
companion object : UnitOverviewTabHelpers()
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.GUI
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.UpgradeUnitAction
|
||||
import com.unciv.models.ruleset.unit.BaseUnit
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.components.extensions.darken
|
||||
import com.unciv.ui.components.extensions.toPrettyString
|
||||
import com.unciv.ui.components.fonts.Fonts
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popups.UnitUpgradeMenu
|
||||
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
|
||||
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
|
||||
|
||||
/**
|
||||
* Helper library for [UnitOverviewTabColumn]
|
||||
*
|
||||
* Note - this will be made into a companion object by simply inheriting it, so do treat it as singleton
|
||||
*/
|
||||
open class UnitOverviewTabHelpers {
|
||||
/** Create an identifier to support selecting a specific unit - or finding it again after a resort or after an upgrade.
|
||||
* This is for UI only, as there can be no 100% guarantee the find will succeed or be unambiguous.
|
||||
*/
|
||||
internal fun getUnitIdentifier(unit: MapUnit, unitToUpgradeTo: BaseUnit? = null): String {
|
||||
val name = unitToUpgradeTo?.name ?: unit.name
|
||||
return "$name@${unit.getTile().position.toPrettyString()}"
|
||||
}
|
||||
|
||||
private fun showWorldScreenAt(position: Vector2, unit: MapUnit?) {
|
||||
GUI.resetToWorldScreen()
|
||||
GUI.getMap().setCenterPosition(position, forceSelectUnit = unit)
|
||||
}
|
||||
|
||||
protected fun showWorldScreenAt(unit: MapUnit) = showWorldScreenAt(unit.currentTile.position, unit)
|
||||
protected fun showWorldScreenAt(tile: Tile) = showWorldScreenAt(tile.position, null)
|
||||
|
||||
private fun getWorkerActionText(unit: MapUnit): String? = when {
|
||||
// See UnitTurnManager.endTurn, if..workOnImprovement or UnitGroup.getActionImage: similar logic
|
||||
!unit.cache.hasUniqueToBuildImprovements -> null
|
||||
unit.currentMovement == 0f -> null
|
||||
unit.currentTile.improvementInProgress == null -> null
|
||||
!unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!) -> null
|
||||
else -> unit.currentTile.improvementInProgress
|
||||
}
|
||||
|
||||
protected fun getActionText(unit: MapUnit): String? {
|
||||
val workerText by lazy { getWorkerActionText(unit) }
|
||||
return when {
|
||||
unit.action == null -> workerText
|
||||
unit.isFortified() -> UnitActionType.Fortify.value
|
||||
unit.isMoving() -> "Moving"
|
||||
unit.isAutomated() && workerText != null -> "[$workerText] ${Fonts.automate}"
|
||||
else -> unit.action
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getUpgradeTable(unit: MapUnit, actionContext: UnitOverviewTab): Table? {
|
||||
val table = Table()
|
||||
val unitActions = UnitActionsUpgrade.getUpgradeActionAnywhere(unit)
|
||||
if (unitActions.none()) return null
|
||||
val canEnable = actionContext.viewingPlayer.isCurrentPlayer() && GUI.isAllowedChangeState()
|
||||
|
||||
for (unitAction in unitActions) {
|
||||
val enable = canEnable && unitAction.action != null
|
||||
val unitToUpgradeTo = (unitAction as UpgradeUnitAction).unitToUpgradeTo
|
||||
val selectKey = getUnitIdentifier(unit, unitToUpgradeTo)
|
||||
val upgradeIcon = ImageGetter.getUnitIcon(unitToUpgradeTo.name,
|
||||
if (enable) Color.GREEN else Color.GREEN.darken(0.5f))
|
||||
upgradeIcon.onClick {
|
||||
UnitUpgradeMenu(actionContext.overviewScreen.stage, upgradeIcon, unit, unitAction, enable) {
|
||||
actionContext.update()
|
||||
actionContext.overviewScreen.select(EmpireOverviewCategories.Units, selectKey) // actionContext.select skips setting scrollY
|
||||
}
|
||||
}
|
||||
upgradeIcon.addTooltip(unitToUpgradeTo.name, 24f, tipAlign = Align.bottomLeft)
|
||||
table.add(upgradeIcon).size(28f)
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
protected fun getUpgradeSortString(unit: MapUnit): String? {
|
||||
val upgrade = UnitActionsUpgrade.getUpgradeActionAnywhere(unit).firstOrNull()
|
||||
?: return null
|
||||
return (upgrade as UpgradeUnitAction).unitToUpgradeTo.name.tr(hideIcons = true)
|
||||
}
|
||||
|
||||
protected fun getPromotionsTable(unit: MapUnit, actionContext: UnitOverviewTab): Table {
|
||||
// This was once designed to be redrawn in place without rebuilding the grid.
|
||||
// That created problems with sorting - and determining when the state would allow minimal updating is complex.
|
||||
// But the old way also had the mini-bug that PromotionPicker allows unit rename which wasn't reflected on the grid...
|
||||
// Now it always does rebuild all rows (as simple as actionContext.update instead of updatePromotionsTable).
|
||||
val promotionsTable = Table()
|
||||
val canEnable = actionContext.viewingPlayer.isCurrentPlayer() && GUI.isAllowedChangeState()
|
||||
updatePromotionsTable(promotionsTable, unit, canEnable)
|
||||
val selectKey = getUnitIdentifier(unit)
|
||||
|
||||
fun onPromotionsTableClick() {
|
||||
val canPromote = canEnable && unit.promotions.canBePromoted()
|
||||
if (!canPromote && unit.promotions.promotions.isEmpty()) return
|
||||
// We can either add a promotion or at least view existing ones.
|
||||
// PromotionPickerScreen is reponsible for checking viewingPlayer.isCurrentPlayer and isAllowedChangeState **again**.
|
||||
actionContext.overviewScreen.game.pushScreen(
|
||||
PromotionPickerScreen(unit) {
|
||||
// Todo seems the picker does not call this if only the unit rename was used
|
||||
actionContext.update()
|
||||
actionContext.overviewScreen.select(EmpireOverviewCategories.Units, selectKey) // actionContext.select skips setting scrollY
|
||||
}
|
||||
)
|
||||
}
|
||||
promotionsTable.onClick(::onPromotionsTableClick)
|
||||
return promotionsTable
|
||||
}
|
||||
|
||||
private fun updatePromotionsTable(table: Table, unit: MapUnit, canEnable: Boolean) {
|
||||
table.clearChildren()
|
||||
|
||||
// getPromotions goes by json order on demand - so this is the same sorting as on UnitTable,
|
||||
// but not same as on PromotionPickerScreen (which e.g. tries to respect prerequisite proximity)
|
||||
val promotions = unit.promotions.getPromotions(true)
|
||||
val showPromoteStar = unit.promotions.canBePromoted()
|
||||
if (promotions.any()) {
|
||||
val iconCount = promotions.count() + (if (showPromoteStar) 1 else 0)
|
||||
val numberOfLines = (iconCount - 1) / 8 + 1 // Int math: -1,/,+1 means divide rounding *up*
|
||||
val promotionsPerLine = (iconCount - 1) / numberOfLines + 1
|
||||
for (linePromotions in promotions.chunked(promotionsPerLine)) {
|
||||
for (promotion in linePromotions) {
|
||||
table.add(ImageGetter.getPromotionPortrait(promotion.name))
|
||||
}
|
||||
if (linePromotions.size == promotionsPerLine) table.row()
|
||||
}
|
||||
}
|
||||
|
||||
if (!showPromoteStar) return
|
||||
table.add(
|
||||
ImageGetter.getImage("OtherIcons/Star").apply {
|
||||
color = if (canEnable && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
||||
Color.GOLDENROD
|
||||
else Color.GOLDENROD.darken(0.25f)
|
||||
}
|
||||
).size(24f).padLeft(8f)
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.center
|
||||
import com.unciv.ui.components.extensions.darken
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.widgets.ExpanderTab
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
// This is a static factory to avoid making ExpanderTab open. UnitSupplyTable object used purely as namespace.
|
||||
internal object UnitSupplyTable {
|
||||
fun create(
|
||||
overviewScreen: EmpireOverviewScreen,
|
||||
unitOverviewTab: UnitOverviewTab,
|
||||
viewingPlayer: Civilization,
|
||||
supplyTableWidth: Float
|
||||
): ExpanderTab {
|
||||
val stats = viewingPlayer.stats
|
||||
val deficit = stats.getUnitSupplyDeficit()
|
||||
val icon = if (deficit <= 0) null else Group().apply {
|
||||
isTransform = false
|
||||
setSize(36f, 36f)
|
||||
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
||||
image.color = Color.FIREBRICK
|
||||
image.setSize(36f, 36f)
|
||||
image.center(this)
|
||||
image.setOrigin(Align.center)
|
||||
addActor(image)
|
||||
}
|
||||
return ExpanderTab(
|
||||
title = "Unit Supply",
|
||||
fontSize = Constants.defaultFontSize,
|
||||
icon = icon,
|
||||
startsOutOpened = deficit > 0,
|
||||
defaultPad = 0f,
|
||||
expanderWidth = supplyTableWidth,
|
||||
onChange = {
|
||||
overviewScreen.resizePage(unitOverviewTab)
|
||||
}
|
||||
) {
|
||||
it.defaults().pad(5f).fill(false)
|
||||
it.background = BaseScreen.skinStrings.getUiBackground(
|
||||
"OverviewScreen/UnitOverviewTab/UnitSupplyTable",
|
||||
tintColor = BaseScreen.skinStrings.skinConfig.baseColor.darken(0.6f)
|
||||
)
|
||||
it.addLabeledValue("Base Supply", stats.getBaseUnitSupply())
|
||||
it.addLabeledValue("Cities", stats.getUnitSupplyFromCities())
|
||||
it.addLabeledValue("Population", stats.getUnitSupplyFromPop())
|
||||
it.addSeparator()
|
||||
it.addLabeledValue("Total Supply", stats.getUnitSupply())
|
||||
it.addLabeledValue("In Use", viewingPlayer.units.getCivUnitsSize())
|
||||
it.addSeparator()
|
||||
it.addLabeledValue("Supply Deficit", deficit)
|
||||
it.addLabeledValue("Production Penalty", "${stats.getUnitSupplyProductionPenalty().toInt()}%")
|
||||
if (deficit > 0) {
|
||||
val penaltyLabel = "Increase your supply or reduce the amount of units to remove the production penalty"
|
||||
.toLabel(Color.FIREBRICK)
|
||||
penaltyLabel.wrap = true
|
||||
it.add(penaltyLabel).colspan(2).left()
|
||||
.width(supplyTableWidth).row()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here overloads are simpler than a generic:
|
||||
private fun Table.addLabeledValue (label: String, value: Int) {
|
||||
add(label.toLabel()).left()
|
||||
add(value.toLabel()).right().row()
|
||||
}
|
||||
private fun Table.addLabeledValue (label: String, value: String) {
|
||||
add(label.toLabel()).left()
|
||||
add(value.toLabel()).right().row()
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.components.extensions.isEnabled
|
||||
import com.unciv.ui.components.extensions.toTextButton
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.input.KeyboardBinding
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
@ -26,11 +27,20 @@ import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.basescreen.RecreateOnResize
|
||||
import kotlin.math.abs
|
||||
|
||||
class PromotionPickerScreen(
|
||||
class PromotionPickerScreen private constructor(
|
||||
val unit: MapUnit,
|
||||
private val closeOnPick: Boolean = true,
|
||||
private val onChange: (() -> Unit)? = null
|
||||
private val closeOnPick: Boolean,
|
||||
private val originalName: String?,
|
||||
private val onChange: (() -> Unit)?
|
||||
) : PickerScreen(), RecreateOnResize {
|
||||
/** Show promotions organized by depencencies, allow picking new ones, allow unit rename
|
||||
* @param unit The MapUnit to work with
|
||||
* @param closeOnPick Should picking a new promotion close the screen?
|
||||
* @param onChange Optional callback called when a promotion is picked or during close if the name was changed
|
||||
*/
|
||||
constructor(unit: MapUnit, closeOnPick: Boolean = true, onChange: (() -> Unit)? = null)
|
||||
: this(unit, closeOnPick, unit.instanceName, onChange)
|
||||
|
||||
// Style stuff
|
||||
private val colors = skin[PromotionScreenColors::class.java]
|
||||
private val promotedLabelStyle = Label.LabelStyle(skin[Label.LabelStyle::class.java]).apply {
|
||||
@ -56,7 +66,12 @@ class PromotionPickerScreen(
|
||||
|
||||
|
||||
init {
|
||||
setDefaultCloseAction()
|
||||
closeButton.onActivation {
|
||||
if (unit.instanceName != originalName)
|
||||
onChange?.invoke()
|
||||
game.popScreen()
|
||||
}
|
||||
closeButton.keyShortcuts.add(KeyCharAndCode.BACK)
|
||||
|
||||
if (canPromoteNow) {
|
||||
rightSideButton.setText("Pick promotion".tr())
|
||||
@ -339,7 +354,7 @@ class PromotionPickerScreen(
|
||||
override fun recreate() = recreate(closeOnPick)
|
||||
|
||||
fun recreate(closeOnPick: Boolean): BaseScreen {
|
||||
val newScreen = PromotionPickerScreen(unit, closeOnPick, onChange)
|
||||
val newScreen = PromotionPickerScreen(unit, closeOnPick, originalName, onChange)
|
||||
newScreen.setScrollY(scrollPane.scrollY)
|
||||
return newScreen
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user