From af92fdc1d248a59c9ab202a5368fa622fd47f8ed Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 19 Aug 2021 09:06:52 +0200 Subject: [PATCH] Expander tab persist (#4905) * ExpanderTabs remember state * ExpanderTabs remember state - city constructions dynamic * ExpanderTabs remember state - city screen portrait --- .../ui/cityscreen/CityConstructionsTable.kt | 166 ++++++++++++------ .../com/unciv/ui/cityscreen/CityInfoTable.kt | 4 +- .../src/com/unciv/ui/cityscreen/CityScreen.kt | 106 +++++++---- .../ui/newgamescreen/ModCheckboxTable.kt | 4 +- .../com/unciv/ui/trade/OfferColumnsTable.kt | 8 +- .../com/unciv/ui/trade/OffersListScroll.kt | 16 +- core/src/com/unciv/ui/utils/ExpanderTab.kt | 13 +- .../com/unciv/ui/utils/ExtensionFunctions.kt | 8 + 8 files changed, 217 insertions(+), 108 deletions(-) diff --git a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt index 176a1cc31f..86d178f646 100644 --- a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Touchable +import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.utils.Align @@ -16,26 +17,44 @@ import com.unciv.models.translations.tr import com.unciv.ui.utils.* import kotlin.concurrent.thread import kotlin.math.max +import kotlin.math.min import com.unciv.ui.utils.AutoScrollPane as ScrollPane -class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBaseScreen.skin) { +/** + * Manager to hold and coordinate two widgets for the city screen left side: + * - Construction queue with switch to [ConstructionInfoTable] button and the enqueue / buy buttons. + * The queue is scrollable, limited to one third of the stage height. + * - Available constructions display, scrolling, grouped with expanders and therefore of dynamic height. + */ +class CityConstructionsTable(private val cityScreen: CityScreen) { /* -1 = Nothing, >= 0 queue entry (0 = current construction) */ private var selectedQueueEntry = -1 // None - - private val showCityInfoTableButton: TextButton - private val constructionsQueueScrollPane: ScrollPane - private val availableConstructionsScrollPane: ScrollPane - - private val constructionsQueueTable = Table() - private val availableConstructionsTable = Table() - private val buttons = Table() - private val pad = 10f - var improvementBuildingToConstruct: Building? = null + private val upperTable = Table(CameraStageBaseScreen.skin) + private val showCityInfoTableButton = "Show stats drilldown".toTextButton() + private val constructionsQueueScrollPane: ScrollPane + private val constructionsQueueTable = Table() + private val buyButtonsTable = Table() + + private val lowerTable = Table() + private val availableConstructionsScrollPane: ScrollPane + private val availableConstructionsTable = Table() + private val lowerTableScrollCell: Cell + + private val pad = 10f + private val posFromEdge = CityScreen.posFromEdge + private val stageHeight = cityScreen.stage.height + + /** Gets or sets visibility of [both widgets][CityConstructionsTable] */ + var isVisible: Boolean + get() = upperTable.isVisible + set(value) { + upperTable.isVisible = value + lowerTable.isVisible = value + } init { - showCityInfoTableButton = "Show stats drilldown".toTextButton() showCityInfoTableButton.onClick { cityScreen.showConstructionsTable = false cityScreen.update() @@ -43,33 +62,52 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase constructionsQueueScrollPane = ScrollPane(constructionsQueueTable.addBorder(2f, Color.WHITE)) constructionsQueueScrollPane.setOverscroll(false, false) + constructionsQueueTable.background = ImageGetter.getBackground(Color.BLACK) + + upperTable.defaults().left().top() + upperTable.add(showCityInfoTableButton).padLeft(pad).padBottom(pad).row() + upperTable.add(constructionsQueueScrollPane) + .maxHeight(stageHeight / 3 - 10f) + .padBottom(pad).row() + upperTable.add(buyButtonsTable).padBottom(pad).row() + availableConstructionsScrollPane = ScrollPane(availableConstructionsTable.addBorder(2f, Color.WHITE)) availableConstructionsScrollPane.setOverscroll(false, false) - - constructionsQueueTable.background = ImageGetter.getBackground(Color.BLACK) availableConstructionsTable.background = ImageGetter.getBackground(Color.BLACK) + lowerTableScrollCell = lowerTable.add(availableConstructionsScrollPane).bottom() + lowerTable.row() + } - add(showCityInfoTableButton).left().padLeft(pad).padBottom(pad).row() - add(constructionsQueueScrollPane).left().padBottom(pad).row() - add().expandY().row() // allow the bottom() below to open up the unneeded space - add(buttons).left().bottom().padBottom(pad).row() - add(availableConstructionsScrollPane).left().bottom().row() + /** Forces layout calculation and returns the upper Table's (construction queue) width */ + fun getUpperWidth() = upperTable.packIfNeeded().width + /** Forces layout calculation and returns the lower Table's (available constructions) width + * - or - the upper Table's width, whichever is greater (in case the former only contains "Loading...") + */ + fun getLowerWidth() = max(lowerTable.packIfNeeded().width, getUpperWidth()) // + + fun addActorsToStage() { + cityScreen.stage.addActor(upperTable) + cityScreen.stage.addActor(lowerTable) + lowerTable.setPosition(posFromEdge, posFromEdge, Align.bottomLeft) } fun update(selectedConstruction: IConstruction?) { updateButtons(selectedConstruction) updateConstructionQueue() - pack() // Need to pack before computing space left for bottom panel + upperTable.pack() + // This should work when set once only in addActorsToStage, but it doesn't (table invisible - why?) + upperTable.setPosition(posFromEdge, stageHeight - posFromEdge, Align.topLeft) + updateAvailableConstructions() - pack() + lowerTableScrollCell.maxHeight(stageHeight - upperTable.height - 2 * posFromEdge) } private fun updateButtons(construction: IConstruction?) { - buttons.clear() - buttons.add(getQueueButton(construction)).padRight(5f) + buyButtonsTable.clear() + buyButtonsTable.add(getQueueButton(construction)).padRight(5f) if (construction != null && construction !is PerpetualConstruction) for (button in getBuyButtons(construction as INonPerpetualConstruction)) - buttons.add(button).padRight(5f) + buyButtonsTable.add(button).padRight(5f) } private fun updateConstructionQueue() { @@ -111,7 +149,6 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase constructionsQueueScrollPane.layout() constructionsQueueScrollPane.scrollY = queueScrollY constructionsQueueScrollPane.updateVisualScroll() - getCell(constructionsQueueScrollPane).maxHeight(stage.height / 3 - 10f) } private fun getConstructionButtonDTOs(): ArrayList { @@ -127,7 +164,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase var buttonText = entry.name.tr() + cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction) for ((resource, amount) in entry.getResourceRequirements()) { buttonText += "\n" + (if (amount == 1) "Consumes 1 [$resource]" - else "Consumes [$amount] [$resource]").tr() + else "Consumes [$amount] [$resource]").tr() } constructionButtonDTOList.add(ConstructionButtonDTO(entry, buttonText, @@ -145,23 +182,23 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase } private fun updateAvailableConstructions() { - val constrScrollY = availableConstructionsScrollPane.scrollY + val constructionsScrollY = availableConstructionsScrollPane.scrollY if (!availableConstructionsTable.hasChildren()) { // availableConstructionsTable.add("Loading...".toLabel()).pad(10f) } - val units = ArrayList() - val buildableWonders = ArrayList
() - val buildableNationalWonders = ArrayList
() - val buildableBuildings = ArrayList
() - val specialConstructions = ArrayList
() thread { // Since this can be a heavy operation and leads to many ANRs on older phones we put the metadata-gathering in another thread. val constructionButtonDTOList = getConstructionButtonDTOs() Gdx.app.postRunnable { - availableConstructionsTable.clear() - var maxWidth = constructionsQueueTable.width + val units = ArrayList
() + val buildableWonders = ArrayList
() + val buildableNationalWonders = ArrayList
() + val buildableBuildings = ArrayList
() + val specialConstructions = ArrayList
() + + var maxButtonWidth = constructionsQueueTable.width for (dto in constructionButtonDTOList) { val constructionButton = getConstructionButton(dto) when (dto.construction) { @@ -175,24 +212,27 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase } is PerpetualConstruction -> specialConstructions.add(constructionButton) } - if (constructionButton.needsLayout()) constructionButton.pack() - maxWidth = max(maxWidth, constructionButton.width) + maxButtonWidth = max(maxButtonWidth, constructionButton.packIfNeeded().width) } - availableConstructionsTable.addCategory("Units", units, maxWidth) - availableConstructionsTable.addCategory("Wonders", buildableWonders, maxWidth) - availableConstructionsTable.addCategory("National Wonders", buildableNationalWonders, maxWidth) - availableConstructionsTable.addCategory("Buildings", buildableBuildings, maxWidth) - availableConstructionsTable.addCategory("Other", specialConstructions, maxWidth) + availableConstructionsTable.apply { + clear() + defaults().left().bottom() + addCategory("Units", units, maxButtonWidth) + addCategory("Wonders", buildableWonders, maxButtonWidth) + addCategory("National Wonders", buildableNationalWonders, maxButtonWidth) + addCategory("Buildings", buildableBuildings, maxButtonWidth) + addCategory("Other", specialConstructions, maxButtonWidth) + pack() + } - availableConstructionsScrollPane.layout() - availableConstructionsScrollPane.scrollY = constrScrollY - availableConstructionsScrollPane.updateVisualScroll() - val usedHeight = showCityInfoTableButton.height + constructionsQueueScrollPane.height + buttons.height + 3f * pad + 10f - getCell(availableConstructionsScrollPane).maxHeight(stage.height - usedHeight) - pack() - - setPosition(5f, stage.height - 5f, Align.topLeft) + availableConstructionsScrollPane.apply { + setSize(maxButtonWidth, min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight)) + layout() + scrollY = constructionsScrollY + updateVisualScroll() + } + lowerTable.pack() } } } @@ -216,9 +256,8 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase val constructionResource = cityConstructions.getConstruction(constructionName).getResourceRequirements() for ((resource, amount) in constructionResource) - if (amount == 1) text += "\n" + "Consumes 1 [$resource]".tr() - else text += "\n" + "Consumes [$amount] [$resource]".tr() - + text += if (amount == 1) "\n" + "Consumes 1 [$resource]".tr() + else "\n" + "Consumes [$amount] [$resource]".tr() table.defaults().pad(2f).minWidth(40f) if (isFirstConstructionOfItsKind) table.add(getProgressBar(constructionName)).minWidth(5f) @@ -244,7 +283,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase return table } - fun getProgressBar(constructionName: String): Group { + private fun getProgressBar(constructionName: String): Group { val cityConstructions = cityScreen.city.cityConstructions val construction = cityConstructions.getConstruction(constructionName) if (construction is PerpetualConstruction) return Table() @@ -256,7 +295,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase Color.BROWN.cpy().lerp(Color.WHITE, 0.5f), Color.WHITE) } - class ConstructionButtonDTO(val construction: IConstruction, val buttonText: String, val rejectionReason: String = "") + private class ConstructionButtonDTO(val construction: IConstruction, val buttonText: String, val rejectionReason: String = "") private fun getConstructionButton(constructionButtonDTO: ConstructionButtonDTO): Table { val construction = constructionButtonDTO.construction @@ -301,7 +340,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase private fun isSelectedQueueEntry(): Boolean = selectedQueueEntry >= 0 - fun cannotAddConstructionToQueue(construction: IConstruction, city: CityInfo, cityConstructions: CityConstructions): Boolean { + private fun cannotAddConstructionToQueue(construction: IConstruction, city: CityInfo, cityConstructions: CityConstructions): Boolean { return cityConstructions.isQueueFull() || !cityConstructions.getConstruction(construction.name).isBuildable(cityConstructions) || !cityScreen.canChangeState @@ -342,7 +381,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase return button } - fun addConstructionToQueue(construction: IConstruction, cityConstructions: CityConstructions) { + private fun addConstructionToQueue(construction: IConstruction, cityConstructions: CityConstructions) { if (construction is Building && construction.uniqueObjects.any { it.placeholderText == "Creates a [] improvement on a specific tile" }) { cityScreen.selectedTile improvementBuildingToConstruct = construction @@ -356,7 +395,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase cityScreen.game.settings.addCompletedTutorialTask("Pick construction") } - fun getConstructionSound(construction: IConstruction): UncivSound { + private fun getConstructionSound(construction: IConstruction): UncivSound { return when(construction) { is Building -> UncivSound.Construction is BaseUnit -> UncivSound.Promote @@ -493,11 +532,22 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase .pad(4f) } + private fun resizeAvailableConstructionsScrollPane() { + availableConstructionsScrollPane.height = min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight) + lowerTable.pack() + } + private fun Table.addCategory(title: String, list: ArrayList
, prefWidth: Float) { if (list.isEmpty()) return if (rows > 0) addSeparator() - val expander = ExpanderTab(title, defaultPad = 0f, expanderWidth = prefWidth) { + val expander = ExpanderTab( + title, + defaultPad = 0f, + expanderWidth = prefWidth, + persistenceID = "CityConstruction.$title", + onChange = { resizeAvailableConstructionsScrollPane() } + ) { for (table in list) { it.addSeparator(colSpan = 1) it.add(table).left().row() diff --git a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt index 05cab808b0..927524fc19 100644 --- a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt @@ -52,9 +52,9 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseS pack() } - private fun Table.addCategory(str: String, showHideTable: Table) { + private fun Table.addCategory(category: String, showHideTable: Table) { val categoryWidth = cityScreen.stage.width / 4 - val expander = ExpanderTab(str) { + val expander = ExpanderTab(category, persistenceID = "CityInfo") { it.add(showHideTable).minWidth(categoryWidth) } addSeparator() diff --git a/core/src/com/unciv/ui/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/cityscreen/CityScreen.kt index 0e6cbe4f23..8b64e0b071 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreen.kt @@ -12,9 +12,13 @@ import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.* import java.util.* -import com.unciv.ui.utils.AutoScrollPane as ScrollPane class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { + companion object { + /** Distance from stage edges to floating widgets */ + const val posFromEdge = 5f + } + var selectedTile: TileInfo? = null var selectedConstruction: IConstruction? = null @@ -26,7 +30,10 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { // Clockwise from the top-left - /** Displays current production, production queue and available productions list - sits on LEFT */ + /** Displays current production, production queue and available productions list + * Not a widget, but manages two: construction queue, info toggle button, buy buttons + * in a Table holder on upper LEFT, and available constructions in a ScrollPane lower LEFT. + */ private var constructionsTable = CityConstructionsTable(this) /** Displays stats, buildings, specialists and stats drilldown - sits on TOP LEFT, can be toggled to */ @@ -56,6 +63,9 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { /** Holds City tiles group*/ private var tileGroups = ArrayList() + /** The ScrollPane for the background map view of the city surroundings */ + private val mapScrollPane = ZoomableScrollPane() + init { onBackButtonClicked { game.setWorldScreen() } UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen") @@ -64,12 +74,12 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { //stage.setDebugTableUnderMouse(true) stage.addActor(cityStatsTable) - stage.addActor(constructionsTable) - stage.addActor(tileTable) - stage.addActor(selectedConstructionTable) - stage.addActor(cityPickerTable) - stage.addActor(exitCityButton) + constructionsTable.addActorsToStage() stage.addActor(cityInfoTable) + stage.addActor(selectedConstructionTable) + stage.addActor(tileTable) + stage.addActor(cityPickerTable) // add late so it's top in Z-order and doesn't get covered in cramped portrait + stage.addActor(exitCityButton) update() keyPressDispatcher[Input.Keys.LEFT] = { page(-1) } @@ -77,39 +87,61 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { } internal fun update() { + // Recalculate Stats + city.cityStats.update() + + // Left side, top and bottom: Construction queue / details if (showConstructionsTable) { constructionsTable.isVisible = true cityInfoTable.isVisible = false + constructionsTable.update(selectedConstruction) } else { constructionsTable.isVisible = false cityInfoTable.isVisible = true + cityInfoTable.update() + cityInfoTable.setPosition(posFromEdge, stage.height - posFromEdge, Align.topLeft) } - city.cityStats.update() - - constructionsTable.update(selectedConstruction) - constructionsTable.setPosition(5f, stage.height - 5f, Align.topLeft) - - cityInfoTable.update() - cityInfoTable.setPosition(5f, stage.height - 5f, Align.topLeft) - - exitCityButton.centerX(stage) - exitCityButton.y = 10f - cityPickerTable.update() - cityPickerTable.centerX(stage) - cityPickerTable.setY(exitCityButton.top + 10f, Align.bottom) - + // Bottom right: Tile or selected construction info tileTable.update(selectedTile) - tileTable.setPosition(stage.width - 5f, 5f, Align.bottomRight) - + tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) selectedConstructionTable.update(selectedConstruction) - selectedConstructionTable.setPosition(stage.width - 5f, 5f, Align.bottomRight) + selectedConstructionTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) + // In portrait mode only: calculate already occupied horizontal space + val rightMargin = when { + !isPortrait() -> 0f + selectedTile != null -> tileTable.packIfNeeded().width + selectedConstruction != null -> selectedConstructionTable.packIfNeeded().width + else -> posFromEdge + } + val leftMargin = when { + !isPortrait() -> 0f + showConstructionsTable -> constructionsTable.getLowerWidth() + else -> cityInfoTable.packIfNeeded().width + } + + // Bottom center: Name, paging, exit city button + val centeredX = (stage.width - leftMargin - rightMargin) / 2 + leftMargin + exitCityButton.setPosition(centeredX, 10f, Align.bottom) + cityPickerTable.update() + cityPickerTable.setPosition(centeredX, exitCityButton.top + 10f, Align.bottom) + + // Top right of screen: Stats / Specialists cityStatsTable.update() - cityStatsTable.setPosition(stage.width - 5f, stage.height - 5f, Align.topRight) + cityStatsTable.setPosition(stage.width - posFromEdge, stage.height - posFromEdge, Align.topRight) + // Top center: Annex/Raze button updateAnnexAndRazeCityButton() + + // Rest of screen: Map of surroundings updateTileGroups() + if (isPortrait()) mapScrollPane.apply { + // center scrolling so city center sits more to the bottom right + scrollX = (maxX - constructionsTable.getLowerWidth() - posFromEdge) / 2 + scrollY = (maxY - cityStatsTable.packIfNeeded().height - posFromEdge + cityPickerTable.top) / 2 + updateVisualScroll() + } } private fun updateTileGroups() { @@ -159,9 +191,9 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { razeCityButtonHolder.add(stopRazingCityButton).colspan(cityPickerTable.columns) } razeCityButtonHolder.pack() - //goToWorldButton.setSize(goToWorldButton.prefWidth, goToWorldButton.prefHeight) - razeCityButtonHolder.centerX(stage) - razeCityButtonHolder.y = stage.height - razeCityButtonHolder.height - 20 + val centerX = if (!isPortrait()) stage.width / 2 + else constructionsTable.getUpperWidth().let { it + (stage.width - cityStatsTable.width - it) / 2 } + razeCityButtonHolder.setPosition(centerX, stage.height - 20f, Align.top) stage.addActor(razeCityButtonHolder) } @@ -222,16 +254,16 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() { } val tileMapGroup = TileGroupMap(tileGroups, stage.width / 2, stage.height / 2, tileGroupsToUnwrap = tilesToUnwrap) - val scrollPane = ScrollPane(tileMapGroup) - scrollPane.setSize(stage.width, stage.height) - scrollPane.setOrigin(stage.width / 2, stage.height / 2) - scrollPane.center(stage) - stage.addActor(scrollPane) + mapScrollPane.actor = tileMapGroup + mapScrollPane.setSize(stage.width, stage.height) + mapScrollPane.setOrigin(stage.width / 2, stage.height / 2) + mapScrollPane.center(stage) + stage.addActor(mapScrollPane) - scrollPane.layout() // center scrolling - scrollPane.scrollPercentX = 0.5f - scrollPane.scrollPercentY = 0.5f - scrollPane.updateVisualScroll() + mapScrollPane.layout() // center scrolling + mapScrollPane.scrollPercentX = 0.5f + mapScrollPane.scrollPercentY = 0.5f + mapScrollPane.updateVisualScroll() } fun exit() { diff --git a/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt b/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt index 8c9dc337f2..d0b89cef4b 100644 --- a/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt @@ -35,7 +35,7 @@ class ModCheckboxTable( val padTop = if (isPortrait) 0f else 16f if (baseRulesetCheckboxes.any()) { - add(ExpanderTab("Base ruleset mods:") { + add(ExpanderTab("Base ruleset mods:", persistenceID = "NewGameBaseMods") { it.defaults().pad(5f,0f) for (checkbox in baseRulesetCheckboxes) it.add(checkbox).row() }).padTop(padTop).growX().row() @@ -45,7 +45,7 @@ class ModCheckboxTable( addSeparator(Color.DARK_GRAY, height = 1f) if (extensionRulesetModButtons.any()) { - add(ExpanderTab("Extension mods:") { + add(ExpanderTab("Extension mods:", persistenceID = "NewGameExpansionMods") { it.defaults().pad(5f,0f) for (checkbox in extensionRulesetModButtons) it.add(checkbox).row() }).padTop(padTop).growX().row() diff --git a/core/src/com/unciv/ui/trade/OfferColumnsTable.kt b/core/src/com/unciv/ui/trade/OfferColumnsTable.kt index ce4efa6341..08b9d1dd0c 100644 --- a/core/src/com/unciv/ui/trade/OfferColumnsTable.kt +++ b/core/src/com/unciv/ui/trade/OfferColumnsTable.kt @@ -18,19 +18,19 @@ class OfferColumnsTable(private val tradeLogic: TradeLogic, val screen: Diplomac onChange() } - private val ourAvailableOffersTable = OffersListScroll { + private val ourAvailableOffersTable = OffersListScroll("OurAvail") { if (it.type == TradeType.Gold) openGoldSelectionPopup(it, tradeLogic.currentTrade.ourOffers, tradeLogic.ourCivilization) else addOffer(it, tradeLogic.currentTrade.ourOffers, tradeLogic.currentTrade.theirOffers) } - private val ourOffersTable = OffersListScroll { + private val ourOffersTable = OffersListScroll("OurTrade") { if (it.type == TradeType.Gold) openGoldSelectionPopup(it, tradeLogic.currentTrade.ourOffers, tradeLogic.ourCivilization) else addOffer(it.copy(amount = -it.amount), tradeLogic.currentTrade.ourOffers, tradeLogic.currentTrade.theirOffers) } - private val theirOffersTable = OffersListScroll { + private val theirOffersTable = OffersListScroll("TheirTrade") { if (it.type == TradeType.Gold) openGoldSelectionPopup(it, tradeLogic.currentTrade.theirOffers, tradeLogic.otherCivilization) else addOffer(it.copy(amount = -it.amount), tradeLogic.currentTrade.theirOffers, tradeLogic.currentTrade.ourOffers) } - private val theirAvailableOffersTable = OffersListScroll { + private val theirAvailableOffersTable = OffersListScroll("TheirAvail") { if (it.type == TradeType.Gold) openGoldSelectionPopup(it, tradeLogic.currentTrade.theirOffers, tradeLogic.otherCivilization) else addOffer(it, tradeLogic.currentTrade.theirOffers, tradeLogic.currentTrade.ourOffers) } diff --git a/core/src/com/unciv/ui/trade/OffersListScroll.kt b/core/src/com/unciv/ui/trade/OffersListScroll.kt index 10de655ff4..79ab3d38d0 100644 --- a/core/src/com/unciv/ui/trade/OffersListScroll.kt +++ b/core/src/com/unciv/ui/trade/OffersListScroll.kt @@ -13,15 +13,23 @@ import com.unciv.ui.utils.* import kotlin.math.min import com.unciv.ui.utils.AutoScrollPane as ScrollPane -class OffersListScroll(val onOfferClicked: (TradeOffer) -> Unit) : ScrollPane(null) { +/** + * Widget for one fourth of an [OfferColumnsTable] - instantiated for ours/theirs × available/traded + * @param persistenceID Part of ID added to [ExpanderTab.persistenceID] to distinguish the four usecases + * @param onOfferClicked What to do when a tradeButton is clicked + */ +class OffersListScroll( + private val persistenceID: String, + private val onOfferClicked: (TradeOffer) -> Unit +) : ScrollPane(null) { val table = Table(CameraStageBaseScreen.skin).apply { defaults().pad(5f) } private val expanderTabs = HashMap() /** - * offersToDisplay - the offers which should be displayed as buttons - * otherOffers - the list of other side's offers to compare with whether these offers are unique + * @param offersToDisplay The offers which should be displayed as buttons + * @param otherOffers The list of other side's offers to compare with whether these offers are unique */ fun update(offersToDisplay:TradeOffersList, otherOffers: TradeOffersList) { table.clear() @@ -38,7 +46,7 @@ class OffersListScroll(val onOfferClicked: (TradeOffer) -> Unit) : ScrollPane(nu } val offersOfType = offersToDisplay.filter { it.type == offerType } if (labelName.isNotEmpty() && offersOfType.any()) { - expanderTabs[offerType] = ExpanderTab(labelName) { + expanderTabs[offerType] = ExpanderTab(labelName, persistenceID = "Trade.$persistenceID.$offerType") { it.defaults().pad(5f) } } diff --git a/core/src/com/unciv/ui/utils/ExpanderTab.kt b/core/src/com/unciv/ui/utils/ExpanderTab.kt index 83d5a43954..21abf75ffb 100644 --- a/core/src/com/unciv/ui/utils/ExpanderTab.kt +++ b/core/src/com/unciv/ui/utils/ExpanderTab.kt @@ -17,6 +17,8 @@ import com.unciv.UncivGame * @param icon Optional icon - please use [Image][com.badlogic.gdx.scenes.scene2d.ui.Image] or [IconCircleGroup] * @param defaultPad Padding between content and wrapper. Header padding is currently not modifiable. * @param expanderWidth If set initializes header width + * @param persistenceID If specified, the ExpanderTab will remember its open/closed state for the duration of one app run + * @param onChange If specified, this will be called after the visual change for a change in [isOpen] completes (e.g. to react to changed size) * @param initContent Optional lambda with [innerTable] as parameter, to help initialize content. */ class ExpanderTab( @@ -26,6 +28,8 @@ class ExpanderTab( startsOutOpened: Boolean = true, defaultPad: Float = 10f, expanderWidth: Float = 0f, + private val persistenceID: String? = null, + private val onChange: (() -> Unit)? = null, initContent: ((Table) -> Unit)? = null ): Table(CameraStageBaseScreen.skin) { private companion object { @@ -33,6 +37,8 @@ class ExpanderTab( const val arrowImage = "OtherIcons/BackArrow" val arrowColor = Color(1f,0.96f,0.75f,1f) const val animationDuration = 0.2f + + val persistedStates = HashMap() } private val header = Table(skin) // Header with label and icon, touchable to show/hide @@ -44,7 +50,8 @@ class ExpanderTab( val innerTable = Table() /** Indicates whether the contents are currently shown, changing this will animate the widget */ - var isOpen = startsOutOpened + // This works because a HashMap _could_ store an entry for the null key but we cannot actually store one when declaring as HashMap + var isOpen = persistedStates[persistenceID] ?: startsOutOpened private set(value) { if (value == field) return field = value @@ -81,10 +88,13 @@ class ExpanderTab( } private fun update(noAnimation: Boolean = false) { + if (persistenceID != null) + persistedStates[persistenceID] = isOpen if (noAnimation || !UncivGame.Current.settings.continuousRendering) { contentWrapper.clear() if (isOpen) contentWrapper.add(innerTable) headerIcon.rotation = if (isOpen) 90f else 180f + if (!noAnimation) onChange?.invoke() return } val action = object: FloatAction ( 90f, 180f, animationDuration, Interpolation.linear) { @@ -94,6 +104,7 @@ class ExpanderTab( if (this.isComplete) { contentWrapper.clear() if (isOpen) contentWrapper.add(innerTable) + onChange?.invoke() } } }.apply { isReverse = isOpen } diff --git a/core/src/com/unciv/ui/utils/ExtensionFunctions.kt b/core/src/com/unciv/ui/utils/ExtensionFunctions.kt index 5dd9a60518..8b57b7eebb 100644 --- a/core/src/com/unciv/ui/utils/ExtensionFunctions.kt +++ b/core/src/com/unciv/ui/utils/ExtensionFunctions.kt @@ -223,6 +223,14 @@ fun Label.setFontSize(size:Int): Label { return this } +/** [pack][WidgetGroup.pack] a [WidgetGroup] if its [needsLayout][WidgetGroup.needsLayout] is true. + * @return the receiver to allow chaining + */ +fun WidgetGroup.packIfNeeded(): WidgetGroup { + if (needsLayout()) pack() + return this +} + /** Get one random element of a given List. * * The probability for each element is proportional to the value of its corresponding element in the [weights] List.