mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
Generic Widget/Provider framework for sortable grids (#8962)
* City Overview reorg - adding a Column should get easier * City Overview reorg - patch * City Overview reorg - SortableGrid Widget * SortableGrid Widget - cleanup * Generalize equalizeColumns * SortableGrid framework - cleaner v2 * Revert file rename to facilitate merge * Post-merge missed change * SortableGrid merge fix * Resolve wildcard import * Post-merge fix: showOneTimeNotification * Post-merge fixes * Post-merge cleanup * More Post-merge cleanup * Fix sort (bug symptom: dependence on column click order) * Tooltip update to "fix" icons if hideIcons=false * Allow hideIcons control for grid header Tooltip * Lint String.tr() Kdoc * Move getComparator() default implementation to interface * Nicer getComparator() implementations, better sorting for WLTK column * Fix "Tooltip update to "fix" icons" reverting tooltip color * Suppress detekt false positives * Fix merge error
This commit is contained in:
3
.github/workflows/detektAnalysis.yml
vendored
3
.github/workflows/detektAnalysis.yml
vendored
@ -29,11 +29,10 @@ jobs:
|
||||
id: setup_detekt
|
||||
uses: peter-murray/setup-detekt@v2
|
||||
with:
|
||||
detekt_version: '1.23.0-RC3'
|
||||
detekt_version: '1.23.1'
|
||||
|
||||
- name: Detekt errors
|
||||
run: detekt-cli --parallel --config detekt/config/detekt-errors.yml
|
||||
|
||||
- name: Detekt warnings
|
||||
run: detekt-cli --parallel --config detekt/config/detekt-warnings.yml
|
||||
|
||||
|
@ -283,7 +283,7 @@ object TranslationActiveModsCache {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* This function does the actual translation work,
|
||||
* using an instance of [Translations] stored in UncivGame.Current
|
||||
*
|
||||
@ -293,12 +293,13 @@ object TranslationActiveModsCache {
|
||||
* sentences - contains at least one '{'
|
||||
* - phrases between curly braces are translated individually
|
||||
* Additionally, they may contain conditionals between '<' and '>'
|
||||
* @param hideIcons disables auto-inserting icons for ruleset objects (but not Stats)
|
||||
* @return The translated string
|
||||
* defaults to the input string if no translation is available,
|
||||
* but with placeholder or sentence brackets removed.
|
||||
*/
|
||||
fun String.tr(hideIcons:Boolean = false): String {
|
||||
val language:String = UncivGame.Current.settings.language
|
||||
fun String.tr(hideIcons: Boolean = false): String {
|
||||
val language: String = UncivGame.Current.settings.language
|
||||
|
||||
// '<' and '>' checks for quick 'no' answer, regex to ensure that no one accidentally put '><' and ruined things
|
||||
if (contains('<') && contains('>') && pointyBraceRegex.containsMatchIn(this)) { // Conditionals!
|
||||
|
@ -117,7 +117,7 @@ class ColorMarkupLabel private constructor(
|
||||
return translated.replace('«', '[').replace('»', ']')
|
||||
}
|
||||
|
||||
private fun prepareText(text: String, textColor: Color, symbolColor: Color): String {
|
||||
fun prepareText(text: String, textColor: Color, symbolColor: Color): String {
|
||||
val translated = text.tr()
|
||||
if ((textColor == Color.WHITE && symbolColor == Color.WHITE) || translated.isBlank())
|
||||
return translated
|
||||
|
@ -0,0 +1,71 @@
|
||||
package com.unciv.ui.components
|
||||
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.unciv.logic.GameInfo
|
||||
|
||||
/**
|
||||
* 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
|
||||
* - [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
|
||||
* @param ACT Action context type - The Type of any object you need passed to [getEntryActor] for potential OnClick calls
|
||||
*/
|
||||
interface ISortableGridContentProvider<IT, ACT> {
|
||||
/** tooltip for the column header, typically overridden to default to enum name, will be auto-translated */
|
||||
val headerTip: String
|
||||
|
||||
/** Passed to addTooltip(hideIcons) - override to true to prevent autotranslation from inserting icons */
|
||||
val headerTipHideIcons get() = false
|
||||
|
||||
/** [Cell.align] - used on header, entry and total cells */
|
||||
val align: Int
|
||||
|
||||
/** [Cell.fillX] - used on header, entry and total cells */
|
||||
val fillX: Boolean
|
||||
|
||||
/** [Cell.expandX] - used on header, entry and total cells */
|
||||
val expandX: Boolean
|
||||
|
||||
/** 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. */
|
||||
// Relevant for visuals (simply inverting the comparator would leave the displayed arrow not matching)
|
||||
val defaultDescending: Boolean
|
||||
|
||||
/** @return whether the column should be rendered */
|
||||
fun isVisible(gameInfo: GameInfo): Boolean = true
|
||||
|
||||
/** [Comparator] Factory used for sorting.
|
||||
* - The default will sort by [getEntryValue] ascending.
|
||||
* @return positive to sort second lambda argument before first lambda argument
|
||||
*/
|
||||
fun getComparator(): Comparator<IT> = compareBy { item: IT -> getEntryValue(item) }
|
||||
|
||||
/** Factory for the header cell [Actor] */
|
||||
fun getHeaderIcon(iconSize: Float): Actor?
|
||||
|
||||
/** A getter for the numeric value to display in a cell */
|
||||
fun getEntryValue(item: IT): Int
|
||||
|
||||
/** Factory for entry cell [Actor]
|
||||
* - 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?
|
||||
|
||||
/** 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`.
|
||||
*/
|
||||
fun getTotalsActor(items: Iterable<IT>): Actor?
|
||||
|
||||
}
|
242
core/src/com/unciv/ui/components/SortableGrid.kt
Normal file
242
core/src/com/unciv/ui/components/SortableGrid.kt
Normal file
@ -0,0 +1,242 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate") // A generic Widget has public members not necessarily used elsewhere
|
||||
package com.unciv.ui.components
|
||||
|
||||
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.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
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.screens.basescreen.BaseScreen
|
||||
|
||||
|
||||
/**
|
||||
* A generic sortable grid Widget
|
||||
*
|
||||
* Note this only remembers one sort criterion. Sorts like compareBy(type).thenBy(name) aren't supported.
|
||||
*
|
||||
* @param IT Type of the data objects that provide info per row
|
||||
* @param ACT Type for [actionContext], Anything allowed, specific meaning defined only by ISortableGridContentProvider subclass
|
||||
* @param CT Type of the columns
|
||||
*/
|
||||
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 */
|
||||
private val data: Iterable<IT>,
|
||||
/** Passed to [ISortableGridContentProvider.getEntryActor] where it can be used to define `onClick` actions. */
|
||||
private val actionContext: ACT,
|
||||
/** Sorting state will be kept here - provide your own e.g. if you want to persist it */
|
||||
private val sortState: ISortState<CT> = SortState(columns.first()),
|
||||
/** Size for header icons - if you set this too low, there is a chance that the tables will be misaligned */
|
||||
private val iconSize: Float = 50f,
|
||||
/** vertical padding for all Cells */
|
||||
paddingVert: Float = 5f,
|
||||
/** horizontal padding for all Cells */
|
||||
paddingHorz: Float = 8f,
|
||||
/** When `true`, the header row isn't part of the widget but delivered through [getHeader] */
|
||||
private val separateHeader: Boolean = false,
|
||||
/** Called after every update - during init and re-sort */
|
||||
private val updateCallback: ((header: Table, details: Table, totals: Table) -> Unit)? = null
|
||||
) : Table(BaseScreen.skin) {
|
||||
/** The direction a column may be sorted in */
|
||||
// None is the Natural order of underlying data - only available before using any sort-click
|
||||
enum class SortDirection { None, Ascending, Descending }
|
||||
|
||||
/** Defines what is needed to remember the sorting state of the grid. */
|
||||
// Abstract to allow easier implementation outside this class
|
||||
// Note this does not automatically enforce this CT to be te same as SortableGrid's CT - not here.
|
||||
// The _client_ will get the compilation errors when passing a custom SortState with a mismatch in CT.
|
||||
interface ISortState<CT> {
|
||||
/** Stores the column this grid is currently sorted by */
|
||||
var sortedBy: CT
|
||||
/** Stores the direction column [sortedBy] is sorted in */
|
||||
var direction: SortDirection
|
||||
}
|
||||
|
||||
/** Default implementation used as default for the [sortState] parameter
|
||||
* - unused if the client provides that parameter */
|
||||
private class SortState<CT>(default: CT) : ISortState<CT> {
|
||||
override var sortedBy: CT = default
|
||||
override var direction: SortDirection = SortDirection.None
|
||||
}
|
||||
|
||||
/** Provides the header row separately if and only if [separateHeader] is true,
|
||||
* e.g. to allow scrolling the content but leave the header fixed
|
||||
* (which will need some column width equalization method).
|
||||
* @see com.unciv.ui.components.extensions.equalizeColumns
|
||||
*/
|
||||
fun getHeader(): Table {
|
||||
if (!separateHeader)
|
||||
throw IllegalStateException("You can't call SortableGrid.getHeader unless you override separateHeader to true")
|
||||
return headerRow
|
||||
}
|
||||
|
||||
private val headerRow = Table(skin)
|
||||
private val headerIcons = hashMapOf<CT, HeaderGroup>()
|
||||
private val sortSymbols = hashMapOf<Boolean, Label>()
|
||||
|
||||
private val details = Table(skin)
|
||||
private val totalsRow = Table(skin)
|
||||
|
||||
init {
|
||||
headerRow.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
details.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
totalsRow.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
|
||||
initHeader()
|
||||
updateHeader()
|
||||
updateDetails()
|
||||
initTotals()
|
||||
fireCallback()
|
||||
|
||||
top()
|
||||
if (!separateHeader) {
|
||||
add(headerRow).row()
|
||||
addSeparator(Color.GRAY).pad(paddingVert, 0f)
|
||||
}
|
||||
add(details).row()
|
||||
addSeparator(Color.GRAY).pad(paddingVert, 0f)
|
||||
add(totalsRow)
|
||||
}
|
||||
|
||||
private fun fireCallback() {
|
||||
if (updateCallback == null) return
|
||||
headerRow.pack()
|
||||
details.pack()
|
||||
totalsRow.pack()
|
||||
updateCallback.invoke(headerRow, details, totalsRow)
|
||||
}
|
||||
|
||||
private fun initHeader() {
|
||||
sortSymbols[false] = "↑".toLabel()
|
||||
sortSymbols[true] = "↓".toLabel()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHeader() {
|
||||
for (column in columns) {
|
||||
val sortDirection = if (sortState.sortedBy == column) sortState.direction else SortDirection.None
|
||||
headerIcons[column]?.setSortState(sortDirection)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDetails() {
|
||||
details.clear()
|
||||
if (data.none()) return
|
||||
|
||||
val comparator = sortState.sortedBy.getComparator()
|
||||
val sortedData = when(sortState.direction) {
|
||||
SortDirection.None -> data.asSequence()
|
||||
SortDirection.Ascending -> data.asSequence().sortedWith(comparator)
|
||||
SortDirection.Descending -> data.asSequence().sortedWith(comparator.reversed())
|
||||
}
|
||||
|
||||
val cellsToEqualize = mutableListOf<Cell<Actor>>()
|
||||
for (item in sortedData) {
|
||||
for (column in columns) {
|
||||
val actor = column.getEntryActor(item, iconSize, actionContext)
|
||||
if (actor == null) {
|
||||
details.add()
|
||||
continue
|
||||
}
|
||||
val cell = details.add(actor).align(column.align)
|
||||
.fill(column.fillX, false).expand(column.expandX, false)
|
||||
if (column.equalizeHeight) cellsToEqualize.add(cell)
|
||||
}
|
||||
details.row()
|
||||
}
|
||||
|
||||
// row heights may diverge - fix it by setting minHeight to
|
||||
// largest actual height (of the cells marked `equalizeHeight`)
|
||||
if (cellsToEqualize.isNotEmpty()) {
|
||||
val largestLabelHeight = cellsToEqualize.maxByOrNull { it.prefHeight }!!.prefHeight
|
||||
for (cell in cellsToEqualize) cell.minHeight(largestLabelHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTotals() {
|
||||
for (column in columns) {
|
||||
totalsRow.add(column.getTotalsActor(data)).align(column.align)
|
||||
.fill(column.fillX, false).expand(column.expandX, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSort(sortBy: CT) {
|
||||
// Could be a SortDirection method, but it's single use here
|
||||
fun SortDirection.inverted() = when {
|
||||
this == SortDirection.Ascending -> SortDirection.Descending
|
||||
this == SortDirection.Descending -> SortDirection.Ascending
|
||||
sortBy.defaultDescending -> SortDirection.Descending
|
||||
else -> SortDirection.Ascending
|
||||
}
|
||||
|
||||
sortState.run {
|
||||
if (sortedBy == sortBy) {
|
||||
direction = direction.inverted()
|
||||
} else {
|
||||
sortedBy = sortBy
|
||||
direction = SortDirection.None.inverted()
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild header content to show sort state
|
||||
updateHeader()
|
||||
// Sort the table: clear and fill with sorted data
|
||||
updateDetails()
|
||||
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
|
||||
/** 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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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]!!
|
||||
sortSymbol.setPosition(iconSize - 2f, 0f)
|
||||
addActor(sortSymbol)
|
||||
}
|
||||
}
|
||||
}
|
@ -111,41 +111,7 @@ open class TabbedPager(
|
||||
/** @return Optional second content [Actor], will be placed outside the tab's main [ScrollPane] between header and `content`. Scrolls horizontally only. */
|
||||
fun getFixedContent(): Actor? = null
|
||||
|
||||
/** Sets first row cell's minWidth to the max of the widths of that column over all given tables
|
||||
*
|
||||
* Notes:
|
||||
* - This aligns columns only if the tables are arranged vertically with equal X coordinates.
|
||||
* - first table determines columns processed, all others must have at least the same column count.
|
||||
* - Tables are left as needsLayout==true, so while equal width is ensured, you may have to pack if you want to see the value before this is rendered.
|
||||
*/
|
||||
fun equalizeColumns(vararg tables: Table) {
|
||||
for (table in tables)
|
||||
table.packIfNeeded()
|
||||
val columns = tables.first().columns
|
||||
check(tables.all { it.columns >= columns }) {
|
||||
"IPageExtensions.equalizeColumns needs all tables to have at least the same number of columns as the first one"
|
||||
}
|
||||
|
||||
val widths = (0 until columns)
|
||||
.mapTo(ArrayList(columns)) { column ->
|
||||
tables.maxOf { it.getColumnWidth(column) }
|
||||
}
|
||||
for (table in tables) {
|
||||
for (column in 0 until columns)
|
||||
table.cells[column].run {
|
||||
if (actor == null)
|
||||
// Empty cells ignore minWidth, so just doing Table.add() for an empty cell in the top row will break this. Fix!
|
||||
setActor<Label>("".toLabel())
|
||||
else if (Align.isCenterHorizontal(align)) (actor as? Label)?.run {
|
||||
// minWidth acts like fillX, so Labels will fill and then left-align by default. Fix!
|
||||
if (!Align.isCenterHorizontal(labelAlign))
|
||||
setAlignment(Align.center)
|
||||
}
|
||||
minWidth(widths[column] - padLeft - padRight)
|
||||
}
|
||||
table.invalidate()
|
||||
}
|
||||
}
|
||||
// fun equalizeColumns(vararg tables: Table) - moved to com.unciv.ui.components.extensions (Scene2dExtensions)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
@ -224,6 +224,7 @@ class UncivTooltip <T: Actor>(
|
||||
* @param targetAlign Point on the [target] widget to align the Tooltip to
|
||||
* @param tipAlign Point on the Tooltip to align with the given point on the [target]
|
||||
* @param hideIcons Do not automatically add ruleset object icons during translation
|
||||
* @param dynamicTextProvider If specified, the tooltip calls this every time it is about to be shown to get refreshed text - will be translated. Used e.g. by addTooltip(KeyboardBinding).
|
||||
*/
|
||||
fun Actor.addTooltip(
|
||||
text: String,
|
||||
@ -241,7 +242,9 @@ class UncivTooltip <T: Actor>(
|
||||
|
||||
if (!(always || GUI.keyboardAvailable) || text.isEmpty()) return
|
||||
|
||||
val label = text.toLabel(BaseScreen.skinStrings.skinConfig.baseColor, 38, hideIcons = hideIcons)
|
||||
val labelColor = BaseScreen.skinStrings.skinConfig.baseColor
|
||||
val label = if (hideIcons) text.toLabel(labelColor, fontSize = 38, hideIcons = true)
|
||||
else ColorMarkupLabel(text, labelColor, fontSize = 38)
|
||||
label.setAlignment(Align.center)
|
||||
|
||||
val background = BaseScreen.skinStrings.getUiBackground("General/Tooltip", BaseScreen.skinStrings.roundedEdgeRectangleShape, Color.LIGHT_GRAY)
|
||||
@ -272,7 +275,14 @@ class UncivTooltip <T: Actor>(
|
||||
|
||||
val contentRefresher: (() -> Vector2)? = if (dynamicTextProvider == null) null else { {
|
||||
val newText = dynamicTextProvider()
|
||||
label.setText(newText)
|
||||
if (hideIcons)
|
||||
label.setText(newText.tr())
|
||||
else
|
||||
// Note: This is a kludge. `setText` alone would revert the text color since
|
||||
// ColorMarkupLabel doesn't use Actor.color but markup only. The proper way -
|
||||
// let ColorMarkupLabel override setText and manage - is much more effort.
|
||||
// Note this also translates, so for consistency the normal branch above does the same.
|
||||
label.setText(ColorMarkupLabel.prepareText(newText, labelColor, Color.WHITE))
|
||||
scaleContainerAndGetSize(newText)
|
||||
} }
|
||||
|
||||
|
@ -364,3 +364,41 @@ object GdxKeyCodeFixes {
|
||||
|
||||
fun Input.areSecretKeysPressed() = isKeyPressed(Input.Keys.SHIFT_RIGHT) &&
|
||||
(isKeyPressed(Input.Keys.CONTROL_RIGHT) || isKeyPressed(Input.Keys.ALT_RIGHT))
|
||||
|
||||
/** Sets first row cell's minWidth to the max of the widths of that column over all given tables
|
||||
*
|
||||
* Notes:
|
||||
* - This aligns columns only if the tables are arranged vertically with equal X coordinates.
|
||||
* - first table determines columns processed, all others must have at least the same column count.
|
||||
* - Tables are left as needsLayout==true, so while equal width is ensured, you may have to pack if you want to see the value before this is rendered.
|
||||
* - Note: The receiver <Group> isn't actually needed except to make sure the arguments are descendants.
|
||||
*/
|
||||
fun equalizeColumns(vararg tables: Table) {
|
||||
for (table in tables) {
|
||||
table.packIfNeeded()
|
||||
}
|
||||
val columns = tables.first().columns
|
||||
check(tables.all { it.columns >= columns }) {
|
||||
"IPageExtensions.equalizeColumns needs all tables to have at least the same number of columns as the first one"
|
||||
}
|
||||
|
||||
val widths = (0 until columns)
|
||||
.mapTo(ArrayList(columns)) { column ->
|
||||
tables.maxOf { it.getColumnWidth(column) }
|
||||
}
|
||||
for (table in tables) {
|
||||
for (column in 0 until columns)
|
||||
table.cells[column].run {
|
||||
if (actor == null)
|
||||
// Empty cells ignore minWidth, so just doing Table.add() for an empty cell in the top row will break this. Fix!
|
||||
setActor<Label>("".toLabel())
|
||||
else if (Align.isCenterHorizontal(align)) (actor as? Label)?.run {
|
||||
// minWidth acts like fillX, so Labels will fill and then left-align by default. Fix!
|
||||
if (!Align.isCenterHorizontal(labelAlign))
|
||||
setAlignment(Align.center)
|
||||
}
|
||||
minWidth(widths[column] - padLeft - padRight)
|
||||
}
|
||||
table.invalidate()
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,9 @@
|
||||
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.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.city.CityFlags
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.center
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
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.images.ImageGetter
|
||||
import com.unciv.ui.screens.cityscreen.CityScreen
|
||||
import kotlin.math.roundToInt
|
||||
import com.unciv.ui.components.SortableGrid
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
|
||||
|
||||
class CityOverviewTab(
|
||||
viewingPlayer: Civilization,
|
||||
@ -32,249 +11,32 @@ class CityOverviewTab(
|
||||
persistedData: EmpireOverviewTabPersistableData? = null
|
||||
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
|
||||
class CityTabPersistableData(
|
||||
var sortedBy: String = CITY,
|
||||
var descending: Boolean = false
|
||||
) : EmpireOverviewTabPersistableData() {
|
||||
override fun isEmpty() = sortedBy == CITY
|
||||
override var sortedBy: CityOverviewTabColumn = CityOverviewTabColumn.CityColumn,
|
||||
|
||||
) : EmpireOverviewTabPersistableData(), SortableGrid.ISortState<CityOverviewTabColumn> {
|
||||
override fun isEmpty() = sortedBy == CityOverviewTabColumn.CityColumn
|
||||
override var direction = SortableGrid.SortDirection.None
|
||||
}
|
||||
|
||||
override val persistableData = (persistedData as? CityTabPersistableData) ?: CityTabPersistableData()
|
||||
|
||||
companion object {
|
||||
const val iconSize = 50f //if you set this too low, there is a chance that the tables will be misaligned
|
||||
const val paddingVert = 5f // vertical padding
|
||||
const val paddingHorz = 8f // horizontal padding
|
||||
|
||||
private const val CITY = "City"
|
||||
private const val WLTK = "WLTK"
|
||||
private const val CONSTRUCTION = "Construction"
|
||||
private const val GARRISON = "Garrison"
|
||||
private val alphabeticColumns = listOf(CITY, CONSTRUCTION, WLTK, GARRISON)
|
||||
|
||||
private val citySortIcon = ImageGetter.getUnitIcon("Settler")
|
||||
.surroundWithCircle(iconSize)
|
||||
.apply { addTooltip("Name", 18f, tipAlign = Align.center) }
|
||||
private val wltkSortIcon = ImageGetter.getImage("OtherIcons/WLTK 2")
|
||||
.apply { color = Color.BLACK }
|
||||
.surroundWithCircle(iconSize, color = Color.TAN)
|
||||
.apply { addTooltip("We Love The King Day", 18f, tipAlign = Align.center) }
|
||||
private val constructionSortIcon = ImageGetter.getImage("OtherIcons/Settings")
|
||||
.apply { color = Color.BLACK }
|
||||
.surroundWithCircle(iconSize, color = Color.LIGHT_GRAY)
|
||||
.apply { addTooltip("Current construction", 18f, tipAlign = Align.center) }
|
||||
private val garrisonSortIcon = ImageGetter.getImage("OtherIcons/Shield")
|
||||
.apply { color = Color.BLACK }
|
||||
.surroundWithCircle(iconSize, color = Color.LIGHT_GRAY)
|
||||
.apply { addTooltip("Garrisoned by unit", 18f, tipAlign = Align.center) }
|
||||
|
||||
// Readability helpers
|
||||
private fun String.isStat() = Stat.isStat(this)
|
||||
|
||||
private fun City.getStat(stat: Stat) =
|
||||
if (stat == Stat.Happiness)
|
||||
cityStats.happinessList.values.sum().roundToInt()
|
||||
else cityStats.currentCityStats[stat].roundToInt()
|
||||
|
||||
private fun Int.toCenteredLabel(): Label =
|
||||
this.toLabel().apply { setAlignment(Align.center) }
|
||||
}
|
||||
|
||||
private val columnsNames = arrayListOf("Population", "Food", "Gold", "Science", "Production", "Culture", "Happiness")
|
||||
.apply { if (gameInfo.isReligionEnabled()) add("Faith") }
|
||||
|
||||
private val headerTable = Table(skin)
|
||||
private val detailsTable = Table(skin)
|
||||
private val totalTable = Table(skin)
|
||||
|
||||
private val collator = UncivGame.Current.settings.getCollatorFromLocale()
|
||||
|
||||
override fun getFixedContent() = Table().apply {
|
||||
add("Cities".toLabel(fontSize = Constants.headingFontSize)).padTop(10f).row()
|
||||
add(headerTable).padBottom(paddingVert).row()
|
||||
addSeparator(Color.GRAY)
|
||||
private val grid = SortableGrid(
|
||||
columns = CityOverviewTabColumn.values().asIterable(),
|
||||
data = viewingPlayer.cities,
|
||||
actionContext = overviewScreen,
|
||||
sortState = persistableData,
|
||||
iconSize = 50f, //if you set this too low, there is a chance that the tables will be misaligned
|
||||
paddingVert = 5f,
|
||||
paddingHorz = 8f,
|
||||
separateHeader = false
|
||||
) {
|
||||
header, details, totals ->
|
||||
this.name
|
||||
equalizeColumns(details, header, totals)
|
||||
this.layout()
|
||||
}
|
||||
|
||||
init {
|
||||
headerTable.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
detailsTable.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
totalTable.defaults().pad(paddingVert, paddingHorz).minWidth(iconSize)
|
||||
|
||||
updateTotal()
|
||||
update()
|
||||
|
||||
top()
|
||||
add(detailsTable).row()
|
||||
addSeparator(Color.GRAY).pad(paddingVert, 0f)
|
||||
add(totalTable)
|
||||
}
|
||||
|
||||
private fun toggleSort(sortBy: String) {
|
||||
if (sortBy == persistableData.sortedBy) {
|
||||
persistableData.descending = !persistableData.descending
|
||||
} else {
|
||||
persistableData.sortedBy = sortBy
|
||||
persistableData.descending = sortBy !in alphabeticColumns // Start numeric columns descending
|
||||
}
|
||||
}
|
||||
|
||||
private fun getComparator() = Comparator { city2: City, city1: City ->
|
||||
when(persistableData.sortedBy) {
|
||||
CITY -> collator.compare(city2.name.tr(hideIcons = true), city1.name.tr(hideIcons = true))
|
||||
CONSTRUCTION -> collator.compare(
|
||||
city2.cityConstructions.currentConstructionFromQueue.tr(hideIcons = true),
|
||||
city1.cityConstructions.currentConstructionFromQueue.tr(hideIcons = true))
|
||||
"Population" -> city2.population.population - city1.population.population
|
||||
WLTK -> city2.isWeLoveTheKingDayActive().compareTo(city1.isWeLoveTheKingDayActive())
|
||||
GARRISON -> collator.compare(
|
||||
city2.getGarrison()?.name?.tr(hideIcons = true) ?: "",
|
||||
city1.getGarrison()?.name?.tr(hideIcons = true) ?: "",
|
||||
)
|
||||
else -> {
|
||||
val stat = Stat.safeValueOf(persistableData.sortedBy)!!
|
||||
city2.getStat(stat) - city1.getStat(stat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSortSymbol() = if(persistableData.descending) "↓" else "↑"
|
||||
|
||||
private fun update() {
|
||||
updateHeader()
|
||||
updateCities()
|
||||
equalizeColumns(detailsTable, headerTable, totalTable)
|
||||
layout()
|
||||
}
|
||||
|
||||
private fun updateHeader() {
|
||||
fun sortOnClick(sortBy: String) {
|
||||
toggleSort(sortBy)
|
||||
// sort the table: clear and fill with sorted data
|
||||
update()
|
||||
}
|
||||
|
||||
fun addSortIcon(iconName: String, iconParam: Actor? = null): Cell<Group> {
|
||||
val image = iconParam ?: ImageGetter.getStatIcon(iconName)
|
||||
|
||||
val icon = Group().apply {
|
||||
isTransform = false
|
||||
setSize(iconSize, iconSize)
|
||||
image.setSize(iconSize, iconSize)
|
||||
image.center(this)
|
||||
image.setOrigin(Align.center)
|
||||
onClick { sortOnClick(iconName) }
|
||||
}
|
||||
if (iconName == persistableData.sortedBy) {
|
||||
val label = getSortSymbol().toLabel()
|
||||
label.setOrigin(Align.bottomRight)
|
||||
label.setPosition(iconSize - 2f, 0f)
|
||||
icon.addActor(label)
|
||||
}
|
||||
icon.addActor(image)
|
||||
return headerTable.add(icon).size(iconSize)
|
||||
}
|
||||
|
||||
headerTable.clear()
|
||||
addSortIcon(CITY, citySortIcon).left()
|
||||
headerTable.add() // construction _icon_ column
|
||||
addSortIcon(CONSTRUCTION, constructionSortIcon).left()
|
||||
for (name in columnsNames) {
|
||||
addSortIcon(name)
|
||||
}
|
||||
addSortIcon(WLTK, wltkSortIcon)
|
||||
addSortIcon(GARRISON, garrisonSortIcon)
|
||||
headerTable.pack()
|
||||
}
|
||||
|
||||
private fun updateCities() {
|
||||
detailsTable.clear()
|
||||
if (viewingPlayer.cities.isEmpty()) return
|
||||
|
||||
val sorter = getComparator()
|
||||
var cityList = viewingPlayer.cities.sortedWith(sorter)
|
||||
if (persistableData.descending)
|
||||
cityList = cityList.reversed()
|
||||
|
||||
val constructionCells: MutableList<Cell<Label>> = mutableListOf()
|
||||
for (city in cityList) {
|
||||
val button = city.name.toTextButton(hideIcons = true)
|
||||
button.onClick {
|
||||
overviewScreen.game.pushScreen(CityScreen(city))
|
||||
}
|
||||
detailsTable.add(button).left().fillX()
|
||||
|
||||
val construction = city.cityConstructions.currentConstructionFromQueue
|
||||
if (construction.isNotEmpty()) {
|
||||
detailsTable.add(ImageGetter.getConstructionPortrait(construction, iconSize *0.8f)).padRight(
|
||||
paddingHorz
|
||||
)
|
||||
} else {
|
||||
detailsTable.add()
|
||||
}
|
||||
|
||||
val cell = detailsTable.add(city.cityConstructions.getCityProductionTextForCityButton().toLabel()).left().expandX()
|
||||
constructionCells.add(cell)
|
||||
|
||||
detailsTable.add(city.population.population.toCenteredLabel())
|
||||
|
||||
for (column in columnsNames) {
|
||||
val stat = Stat.safeValueOf(column) ?: continue
|
||||
detailsTable.add(city.getStat(stat).toCenteredLabel())
|
||||
}
|
||||
|
||||
when {
|
||||
city.isWeLoveTheKingDayActive() -> {
|
||||
val image = ImageGetter.getImage("OtherIcons/WLTK 1").surroundWithCircle(
|
||||
iconSize, color = Color.CLEAR)
|
||||
image.addTooltip("[${city.getFlag(CityFlags.WeLoveTheKing)}] turns", 18f, tipAlign = Align.topLeft)
|
||||
detailsTable.add(image)
|
||||
}
|
||||
city.demandedResource.isNotEmpty() -> {
|
||||
val image = ImageGetter.getResourcePortrait(city.demandedResource, iconSize *0.7f).apply {
|
||||
addTooltip("Demanding [${city.demandedResource}]", 18f, tipAlign = Align.topLeft)
|
||||
onClick { showOneTimeNotification(
|
||||
gameInfo.getExploredResourcesNotification(viewingPlayer, city.demandedResource)
|
||||
) }
|
||||
}
|
||||
detailsTable.add(image)
|
||||
}
|
||||
else -> detailsTable.add()
|
||||
}
|
||||
|
||||
val garrisonUnit = city.getGarrison()
|
||||
if (garrisonUnit == null) {
|
||||
detailsTable.add()
|
||||
} else {
|
||||
val garrisonUnitName = garrisonUnit.displayName()
|
||||
val garrisonUnitIcon = ImageGetter.getConstructionPortrait(garrisonUnit.baseUnit.getIconName(), iconSize * 0.7f)
|
||||
garrisonUnitIcon.addTooltip(garrisonUnitName, 18f, tipAlign = Align.topLeft)
|
||||
garrisonUnitIcon.onClick {
|
||||
overviewScreen.select(EmpireOverviewCategories.Units, UnitOverviewTab.getUnitIdentifier(garrisonUnit) )
|
||||
}
|
||||
detailsTable.add(garrisonUnitIcon)
|
||||
}
|
||||
detailsTable.row()
|
||||
}
|
||||
|
||||
// row heights may diverge - fix it by setting minHeight to
|
||||
// largest actual height (of the construction cell) - !! guarded by isEmpty test above
|
||||
val largestLabelHeight = constructionCells.maxByOrNull{ it.prefHeight }!!.prefHeight
|
||||
for (cell in constructionCells) cell.minHeight(largestLabelHeight)
|
||||
|
||||
detailsTable.pack()
|
||||
}
|
||||
|
||||
private fun updateTotal() {
|
||||
totalTable.add("Total".toLabel()).left()
|
||||
totalTable.add() // construction icon column
|
||||
totalTable.add().expandX() // construction label column
|
||||
totalTable.add(viewingPlayer.cities.sumOf { it.population.population }.toCenteredLabel())
|
||||
for (column in columnsNames.filter { it.isStat() }) {
|
||||
val stat = Stat.valueOf(column)
|
||||
if (stat == Stat.Food || stat == Stat.Production) totalTable.add() // an intended empty space
|
||||
else totalTable.add(viewingPlayer.cities.sumOf { it.getStat(stat) }.toCenteredLabel())
|
||||
}
|
||||
totalTable.add(viewingPlayer.cities.count { it.isWeLoveTheKingDayActive() }.toCenteredLabel())
|
||||
totalTable.add(viewingPlayer.cities.count { it.isGarrisoned() }.toCenteredLabel())
|
||||
totalTable.pack()
|
||||
add(grid)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,218 @@
|
||||
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.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.images.ImageGetter
|
||||
import com.unciv.ui.screens.cityscreen.CityScreen
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/**
|
||||
* This defines all behaviour of the [CityOverviewTab] columns through overridable parts
|
||||
*/
|
||||
|
||||
// This false positive of detekt is possibly fixed in https://github.com/detekt/detekt/pull/6367
|
||||
// (The getComparator overrides need the explicit City type on their lambda parameter)
|
||||
@Suppress("ExplicitItLambdaParameter") // detekt is wrong
|
||||
|
||||
enum class CityOverviewTabColumn : ISortableGridContentProvider<City, EmpireOverviewScreen> {
|
||||
//region Enum Instances
|
||||
CityColumn {
|
||||
override val headerTip = "Name"
|
||||
override val align = Align.left
|
||||
override val fillX = true
|
||||
override val defaultDescending = false
|
||||
override fun getComparator() = compareBy(collator) { it: City -> it.name.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
ImageGetter.getUnitIcon("Settler")
|
||||
.surroundWithCircle(iconSize)
|
||||
override fun getEntryValue(item: City) = 0 // make sure that `stat!!` in the super isn't used
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen) =
|
||||
item.name.toTextButton(hideIcons = true)
|
||||
.onClick {
|
||||
actionContext.game.pushScreen(CityScreen(item))
|
||||
}
|
||||
override fun getTotalsActor(items: Iterable<City>) =
|
||||
"Total".toLabel()
|
||||
},
|
||||
|
||||
ConstructionIcon {
|
||||
override fun getHeaderIcon(iconSize: Float) = null
|
||||
override fun getEntryValue(item: City) =
|
||||
item.cityConstructions.run { turnsToConstruction(currentConstructionFromQueue) }
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen): Actor? {
|
||||
val construction = item.cityConstructions.currentConstructionFromQueue
|
||||
if (construction.isEmpty()) return null
|
||||
return ImageGetter.getConstructionPortrait(construction, iconSize * 0.8f)
|
||||
}
|
||||
override fun getTotalsActor(items: Iterable<City>) = null
|
||||
},
|
||||
|
||||
Construction {
|
||||
override val align = Align.left
|
||||
override val expandX = true
|
||||
override val equalizeHeight = true
|
||||
override val headerTip = "Current construction"
|
||||
override val defaultDescending = false
|
||||
override fun getComparator() =
|
||||
compareBy(collator) { it: City -> it.cityConstructions.currentConstructionFromQueue.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/Settings", iconSize)
|
||||
override fun getEntryValue(item: City) = 0
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen) =
|
||||
item.cityConstructions.getCityProductionTextForCityButton().toLabel()
|
||||
override fun getTotalsActor(items: Iterable<City>) = null
|
||||
},
|
||||
|
||||
Population {
|
||||
override fun getEntryValue(item: City) =
|
||||
item.population.population
|
||||
},
|
||||
|
||||
Food {
|
||||
override fun getTotalsActor(items: Iterable<City>) = null // an intended empty space
|
||||
},
|
||||
Gold,
|
||||
Science,
|
||||
Production{
|
||||
override fun getTotalsActor(items: Iterable<City>) = null // an intended empty space
|
||||
},
|
||||
Culture,
|
||||
Happiness {
|
||||
override fun getEntryValue(item: City) =
|
||||
item.cityStats.happinessList.values.sum().roundToInt()
|
||||
},
|
||||
Faith {
|
||||
override fun isVisible(gameInfo: GameInfo) =
|
||||
gameInfo.isReligionEnabled()
|
||||
},
|
||||
|
||||
WLTK {
|
||||
override val headerTip = "We Love The King Day"
|
||||
override val defaultDescending = false
|
||||
override fun getComparator() =
|
||||
super.getComparator().thenBy { it.demandedResource.tr(hideIcons = true) }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/WLTK 2", iconSize, Color.TAN)
|
||||
override fun getEntryValue(item: City) =
|
||||
if (item.isWeLoveTheKingDayActive()) 1 else 0
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen) = when {
|
||||
item.isWeLoveTheKingDayActive() -> {
|
||||
ImageGetter.getImage("OtherIcons/WLTK 1")
|
||||
.surroundWithCircle(iconSize, color = Color.CLEAR)
|
||||
.apply {
|
||||
addTooltip("[${item.getFlag(CityFlags.WeLoveTheKing)}] turns", 18f, tipAlign = Align.topLeft)
|
||||
}
|
||||
}
|
||||
item.demandedResource.isNotEmpty() -> {
|
||||
ImageGetter.getResourcePortrait(item.demandedResource, iconSize * 0.7f).apply {
|
||||
addTooltip("Demanding [${item.demandedResource}]", 18f, tipAlign = Align.topLeft)
|
||||
onClick { actionContext.showOneTimeNotification(
|
||||
item.civ.gameInfo.getExploredResourcesNotification(item.civ, item.demandedResource)
|
||||
) }
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
|
||||
Garrison {
|
||||
override val headerTip = "Garrisoned by unit"
|
||||
override val defaultDescending = false
|
||||
override fun getComparator() =
|
||||
compareBy(collator) { it: City -> it.getCenterTile().militaryUnit?.name?.tr(hideIcons = true) ?: "" }
|
||||
override fun getHeaderIcon(iconSize: Float) =
|
||||
getCircledIcon("OtherIcons/Shield", iconSize)
|
||||
override fun getEntryValue(item: City) =
|
||||
if (item.getCenterTile().militaryUnit != null) 1 else 0
|
||||
override fun getEntryActor(item: City, iconSize: Float, actionContext: EmpireOverviewScreen): Actor? {
|
||||
val unit = item.getCenterTile().militaryUnit ?: return null
|
||||
val unitName = unit.displayName()
|
||||
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) )
|
||||
}
|
||||
return unitIcon
|
||||
}
|
||||
},
|
||||
;
|
||||
|
||||
//endregion
|
||||
|
||||
/** The Stat constant if this is a Stat column - helps the default getter methods */
|
||||
private val stat = Stat.safeValueOf(name)
|
||||
|
||||
//region Overridable fields
|
||||
|
||||
override val headerTip get() = name
|
||||
override val align = Align.center
|
||||
override val fillX = false
|
||||
override val expandX = false
|
||||
override val equalizeHeight = false
|
||||
override val defaultDescending = true
|
||||
|
||||
//endregion
|
||||
//region Overridable methods
|
||||
|
||||
/** Factory for the header cell [Actor]
|
||||
* - 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? =
|
||||
ImageGetter.getStatIcon(name)
|
||||
|
||||
/** A getter for the numeric value to display in a cell
|
||||
* - The default implementation works only on [Stat] columns, so an override is mandatory unless
|
||||
* it's a [Stat] _or_ all three methods mentioned below have overrides.
|
||||
* - By default this feeds [getComparator], [getEntryActor] _and_ [getTotalsActor],
|
||||
* so an override may be useful for sorting and/or a total even if you do override [getEntryActor].
|
||||
*/
|
||||
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) }
|
||||
}
|
||||
}
|
@ -2,7 +2,9 @@ package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.unciv.Constants
|
||||
import com.unciv.GUI
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.Notification
|
||||
import com.unciv.ui.components.TabbedPager
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
@ -98,4 +100,15 @@ class EmpireOverviewScreen(
|
||||
val scrollY = tab.select(selection) ?: return
|
||||
tabbedPager.setPageScrollY(tabbedPager.activePage, scrollY)
|
||||
}
|
||||
|
||||
/** Helper to show the world screen with a temporary "one-time" notification */
|
||||
// Here because it's common to notification history, resource finder, and city WLTK demanded resource
|
||||
internal fun showOneTimeNotification(notification: Notification?) {
|
||||
if (notification == null) return // Convenience - easier than a return@lambda for a caller
|
||||
val worldScreen = GUI.getWorldScreen()
|
||||
worldScreen.notificationsScroll.oneTimeNotification = notification
|
||||
GUI.resetToWorldScreen()
|
||||
notification.resetExecuteRoundRobin()
|
||||
notification.execute(worldScreen)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
package com.unciv.ui.screens.overviewscreen
|
||||
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.GUI
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.Notification
|
||||
import com.unciv.ui.components.TabbedPager
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
||||
@ -28,15 +25,4 @@ abstract class EmpireOverviewTab (
|
||||
open fun select(selection: String): Float? = null
|
||||
|
||||
val gameInfo = viewingPlayer.gameInfo
|
||||
|
||||
/** Helper to show the world screen with a temporary "one-time" notification */
|
||||
// Here because it's common to notification history and resource finder
|
||||
internal fun showOneTimeNotification(notification: Notification?) {
|
||||
if (notification == null) return // Convenience - easier than a return@lambda for a caller
|
||||
val worldScreen = GUI.getWorldScreen()
|
||||
worldScreen.notificationsScroll.oneTimeNotification = notification
|
||||
UncivGame.Current.resetToWorldScreen()
|
||||
notification.resetExecuteRoundRobin()
|
||||
notification.execute(worldScreen)
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ class NotificationsOverviewTable(
|
||||
notificationTable.background = BaseScreen.skinStrings.getUiBackground("OverviewScreen/NotificationOverviewTable/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape)
|
||||
notificationTable.touchable = Touchable.enabled
|
||||
if (notification.actions.isNotEmpty())
|
||||
notificationTable.onClick { showOneTimeNotification(notification) }
|
||||
notificationTable.onClick { overviewScreen.showOneTimeNotification(notification) }
|
||||
|
||||
notification.addNotificationIconsTo(notificationTable, gameInfo.ruleset, iconSize)
|
||||
|
||||
|
@ -15,6 +15,7 @@ import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.addSeparatorVertical
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.extensions.pad
|
||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
@ -79,7 +80,7 @@ class ResourcesOverviewTab(
|
||||
val label = if (resource.isStockpiled() && amount > 0) "+$amount".toLabel()
|
||||
else amount.toLabel()
|
||||
if (origin == ExtraInfoOrigin.Unimproved.name)
|
||||
label.onClick { showOneTimeNotification(
|
||||
label.onClick { overviewScreen.showOneTimeNotification(
|
||||
gameInfo.getExploredResourcesNotification(viewingPlayer, resource.name) {
|
||||
it.getOwner() == viewingPlayer && it.countAsUnimproved()
|
||||
}
|
||||
@ -95,7 +96,7 @@ class ResourcesOverviewTab(
|
||||
|
||||
private fun getResourceImage(name: String) =
|
||||
ImageGetter.getResourcePortrait(name, iconSize).apply {
|
||||
onClick { showOneTimeNotification(
|
||||
onClick { overviewScreen.showOneTimeNotification(
|
||||
gameInfo.getExploredResourcesNotification(viewingPlayer, name)
|
||||
) }
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ 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
|
||||
@ -37,6 +38,7 @@ 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
|
||||
*/
|
||||
|
@ -12,8 +12,9 @@ import com.unciv.models.ruleset.QuestName
|
||||
import com.unciv.models.ruleset.tech.Era
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.input.onClick
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
|
||||
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
|
||||
|
@ -6,6 +6,7 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.ui.components.TabbedPager
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
|
@ -7,6 +7,7 @@ import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.models.ruleset.Victory
|
||||
import com.unciv.ui.components.TabbedPager
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
|
@ -10,6 +10,7 @@ import com.unciv.models.ruleset.MilestoneType
|
||||
import com.unciv.models.ruleset.Victory
|
||||
import com.unciv.ui.components.TabbedPager
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.equalizeColumns
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
|
Reference in New Issue
Block a user