diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index a04a87e146..30da232006 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -9,7 +9,7 @@ import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PopupAlert -import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.logic.map.mapunit.UnitTurnManager import com.unciv.logic.map.tile.Tile import com.unciv.logic.multiplayer.isUsersTurn import com.unciv.models.ruleset.Building @@ -752,13 +752,15 @@ class CityConstructions : IsPartOfGameInfoSerialization { } else true // we're just continuing the regular queue } - fun raisePriority(constructionQueueIndex: Int) { + fun raisePriority(constructionQueueIndex: Int): Int { constructionQueue.swap(constructionQueueIndex - 1, constructionQueueIndex) + return constructionQueueIndex - 1 } // Lowering == Highering next element in queue - fun lowerPriority(constructionQueueIndex: Int) { + fun lowerPriority(constructionQueueIndex: Int): Int { raisePriority(constructionQueueIndex + 1) + return constructionQueueIndex + 1 } private fun MutableList.swap(idx1: Int, idx2: Int) { @@ -778,7 +780,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { val tileForImprovement = getTileForImprovement(improvement.name) ?: return tileForImprovement.stopWorkingOnImprovement() // clears mark if (removeOnly) return - /**todo unify with [UnitActions.getImprovementConstructionActions] and [MapUnit.workOnImprovement] - this won't allow e.g. a building to place a road */ + /**todo unify with [UnitActions.getImprovementConstructionActions] and [UnitTurnManager.workOnImprovement] - this won't allow e.g. a building to place a road */ tileForImprovement.changeImprovement(improvement.name) city.civ.lastSeenImprovement[tileForImprovement.position] = improvement.name city.cityStats.update() @@ -794,11 +796,11 @@ class CityConstructions : IsPartOfGameInfoSerialization { */ fun removeCreateOneImprovementConstruction(improvement: String) { val ruleset = city.getRuleset() - val indexToRemove = constructionQueue.withIndex().mapNotNull { + val indexToRemove = constructionQueue.withIndex().firstNotNullOfOrNull { val construction = getConstruction(it.value) val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name it.index.takeIf { buildingImprovement == improvement } - }.firstOrNull() ?: return + } ?: return constructionQueue.removeAt(indexToRemove) diff --git a/core/src/com/unciv/logic/city/CityFocus.kt b/core/src/com/unciv/logic/city/CityFocus.kt index 8a8eb1e9dd..71a2def242 100644 --- a/core/src/com/unciv/logic/city/CityFocus.kt +++ b/core/src/com/unciv/logic/city/CityFocus.kt @@ -1,12 +1,29 @@ package com.unciv.logic.city import com.unciv.logic.IsPartOfGameInfoSerialization +import com.unciv.logic.automation.Automation +import com.unciv.logic.city.managers.CityPopulationManager import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats +import com.unciv.ui.components.input.KeyboardBinding +import com.unciv.ui.screens.cityscreen.CitizenManagementTable -// if tableEnabled == true, then Stat != null -enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) : - IsPartOfGameInfoSerialization { +/** + * Controls automatic worker-to-tile assignment + * @param label Display label, formatted for tr() + * @param tableEnabled Whether to show or hide in CityScreen's [CitizenManagementTable] + * @param stat Which stat the default [getStatMultiplier] emphasizes - unused if that is overridden w/o calling super + * @param binding Bindable keyboard key in UI - this is an override, by default matching enum names in [KeyboardBinding] are assigned automatically + * @see CityPopulationManager.autoAssignPopulation + * @see Automation.rankStatsForCityWork + */ +enum class CityFocus( + val label: String, + val tableEnabled: Boolean, + val stat: Stat? = null, + binding: KeyboardBinding? = null +) : IsPartOfGameInfoSerialization { + // region Enum values NoFocus("Default Focus", true, null) { override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see }, @@ -28,8 +45,16 @@ enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Sta } }, FaithFocus("[${Stat.Faith.name}] Focus", true, Stat.Faith), - HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness); - //GreatPersonFocus; + HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness), + //GreatPersonFocus + + ; + // endregion Enum values + + val binding: KeyboardBinding = + binding ?: + KeyboardBinding.values().firstOrNull { it.name == name } ?: + KeyboardBinding.None open fun getStatMultiplier(stat: Stat) = when (this.stat) { stat -> 3f @@ -42,7 +67,9 @@ enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Sta } } - fun safeValueOf(stat: Stat): CityFocus { - return values().firstOrNull { it.stat == stat } ?: NoFocus + companion object { + fun safeValueOf(stat: Stat): CityFocus { + return values().firstOrNull { it.stat == stat } ?: NoFocus + } } } diff --git a/core/src/com/unciv/ui/components/ExpanderTab.kt b/core/src/com/unciv/ui/components/ExpanderTab.kt index dfcee31ace..f46026d4c1 100644 --- a/core/src/com/unciv/ui/components/ExpanderTab.kt +++ b/core/src/com/unciv/ui/components/ExpanderTab.kt @@ -5,13 +5,17 @@ import com.badlogic.gdx.math.Interpolation import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.actions.FloatAction +import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane 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.ui.images.ImageGetter -import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.input.KeyboardBinding +import com.unciv.ui.components.input.keyShortcuts +import com.unciv.ui.components.input.onActivation +import com.unciv.ui.images.IconCircleGroup +import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen /** @@ -36,6 +40,7 @@ class ExpanderTab( headerPad: Float = 10f, expanderWidth: Float = 0f, private val persistenceID: String? = null, + toggleKey: KeyboardBinding = KeyboardBinding.None, private val onChange: (() -> Unit)? = null, initContent: ((Table) -> Unit)? = null ): Table(BaseScreen.skin) { @@ -81,7 +86,8 @@ class ExpanderTab( header.add(headerLabel) header.add(headerIcon).size(arrowSize).align(Align.center) header.touchable= Touchable.enabled - header.onClick { toggle() } + header.onActivation { toggle() } + header.keyShortcuts.add(toggleKey) // Using the onActivation parameter adds a tooltip, which often does not look too good if (expanderWidth != 0f) defaults().minWidth(expanderWidth) defaults().growX() @@ -126,9 +132,44 @@ class ExpanderTab( /** Toggle [isOpen], animated */ fun toggle() { isOpen = !isOpen + + // In the common case where the expander is hosted in a Table within a ScrollPane... + // try scrolling our header so it is visible (when toggled by keyboard) + if (parent is Table && parent.parent is ScrollPane) + tryAutoScroll(parent.parent as ScrollPane) + // But - our Actor.addBorder extension can ruin that, so cater for that special case too... + else if (testForBorderedTable()) + tryAutoScroll(parent.parent.parent as ScrollPane) } - /** Change header label text after initialization */ + private fun testForBorderedTable(): Boolean { + if (parent !is Table) return false + val borderTable = parent.parent as? Table ?: return false + if (parent.parent.parent !is ScrollPane) return false + return borderTable.cells.size == 1 && borderTable.background != null && borderTable.padTop == 2f + } + + private fun tryAutoScroll(scrollPane: ScrollPane) { + if (scrollPane.isScrollingDisabledY) return + + // As the "opening" is animated, and right now the animation has just started, + // a scroll-to-visible won't work, so limit it to showing the header for now. + val heightToShow = header.height + + // Coords as seen by "this" expander relative to parent and as seen by scrollPane may differ by the border size + // Also make area to show relative to top + val yToShow = this.y + this.height - heightToShow + + (if (scrollPane.actor == this.parent) 0f else parent.y) + + // If ever needed - how to check whether scrollTo would not need to scroll (without testing for heightToShow > scrollHeight) +// val relativeY = scrollPane.actor.height - yToShow - scrollPane.scrollY +// if (relativeY >= heightToShow && relativeY <= scrollPane.scrollHeight) return + + // scrollTo does the y axis inversion for us, and also will do nothing if the requested area is already fully visible + scrollPane.scrollTo(0f, yToShow, header.width, heightToShow) + } + + /** Change header label text after initialization (does not auto-translate) */ fun setText(text: String) { headerLabel.setText(text) } diff --git a/core/src/com/unciv/ui/components/input/KeyboardBinding.kt b/core/src/com/unciv/ui/components/input/KeyboardBinding.kt index 9472fd00e5..2d43939fc5 100644 --- a/core/src/com/unciv/ui/components/input/KeyboardBinding.kt +++ b/core/src/com/unciv/ui/components/input/KeyboardBinding.kt @@ -2,6 +2,7 @@ package com.unciv.ui.components.input import com.badlogic.gdx.Input import com.unciv.Constants +import com.unciv.models.stats.Stat private val unCamelCaseRegex = Regex("([A-Z])([A-Z])([a-z])|([a-z])([A-Z])") @@ -123,6 +124,38 @@ enum class KeyboardBinding( HideAdditionalActions(Category.UnitActions,"Back", Input.Keys.PAGE_UP), AddInCapital(Category.UnitActions, "Add in capital", 'g'), + // City Screen + AddConstruction(Category.CityScreen, "Add to or remove from queue", KeyCharAndCode.RETURN), + RaisePriority(Category.CityScreen, "Raise queue priority", Input.Keys.UP), + LowerPriority(Category.CityScreen, "Lower queue priority", Input.Keys.DOWN), + BuyConstruction(Category.CityScreen, 'b'), + BuyTile(Category.CityScreen, 't'), + BuildUnits(Category.CityScreen, "Buildable Units", 'u'), + BuildBuildings(Category.CityScreen, "Buildable Buildings", 'l'), + BuildWonders(Category.CityScreen, "Buildable Wonders", 'w'), + BuildNationalWonders(Category.CityScreen, "Buildable National Wonders", 'n'), + BuildOther(Category.CityScreen, "Other Constructions", 'o'), + NextCity(Category.CityScreen, Input.Keys.RIGHT), + PreviousCity(Category.CityScreen, Input.Keys.LEFT), + ShowStats(Category.CityScreen, 's'), + ShowStatDetails(Category.CityScreen, "Toggle Stat Details", Input.Keys.NUMPAD_ADD), + CitizenManagement(Category.CityScreen, 'c'), + GreatPeopleDetail(Category.CityScreen, 'g'), + SpecialistDetail(Category.CityScreen, 'p'), + ReligionDetail(Category.CityScreen, 'r'), + BuildingsDetail(Category.CityScreen, 'd'), + ResetCitizens(Category.CityScreen, KeyCharAndCode.ctrl('r')), + AvoidGrowth(Category.CityScreen, KeyCharAndCode.ctrl('a')), + // The following are automatically matched by enum name to CityFocus entries - if necessary override there + // Note on label: copied from CityFocus to ensure same translatable is used - without we'd get "Food Focus", not the same as "[Food] Focus" + NoFocus(Category.CityScreen, "Default Focus", KeyCharAndCode.ctrl('d')), + FoodFocus(Category.CityScreen, "[${Stat.Food.name}] Focus", KeyCharAndCode.ctrl('f')), + ProductionFocus(Category.CityScreen, "[${Stat.Production.name}] Focus", KeyCharAndCode.ctrl('p')), + GoldFocus(Category.CityScreen, "[${Stat.Gold.name}] Focus", KeyCharAndCode.ctrl('g')), + ScienceFocus(Category.CityScreen, "[${Stat.Science.name}] Focus", KeyCharAndCode.ctrl('s')), + CultureFocus(Category.CityScreen, "[${Stat.Culture.name}] Focus", KeyCharAndCode.ctrl('c')), + FaithFocus(Category.CityScreen, "[${Stat.Faith.name}] Focus", KeyCharAndCode.UNKNOWN), + // Popups Confirm(Category.Popups, "Confirm Dialog", 'y'), Cancel(Category.Popups, "Cancel Dialog", 'n'), @@ -144,6 +177,7 @@ enum class KeyboardBinding( // Conflict checking within group disabled, but any key assigned on WorldScreen is a problem override fun checkConflictsIn() = sequenceOf(WorldScreen) }, + CityScreen, Popups ; val label = unCamelCase(name) diff --git a/core/src/com/unciv/ui/screens/cityscreen/CitizenManagementTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CitizenManagementTable.kt index 774c0479de..5400d570ae 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CitizenManagementTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CitizenManagementTable.kt @@ -4,10 +4,11 @@ import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.Constants import com.unciv.logic.city.CityFocus -import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.ExpanderTab -import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.input.KeyboardBinding +import com.unciv.ui.components.input.onActivation +import com.unciv.ui.screens.basescreen.BaseScreen class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin) { val city = cityScreen.city @@ -24,7 +25,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin resetCell.add(resetLabel).pad(5f) if (cityScreen.canCityBeChanged()) { resetCell.touchable = Touchable.enabled - resetCell.onClick { + resetCell.onActivation(binding = KeyboardBinding.ResetCitizens) { city.reassignPopulation(true) cityScreen.update() } @@ -41,7 +42,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin avoidCell.add(avoidLabel).pad(5f) if (cityScreen.canCityBeChanged()) { avoidCell.touchable = Touchable.enabled - avoidCell.onClick { + avoidCell.onActivation(binding = KeyboardBinding.AvoidGrowth) { city.avoidGrowth = !city.avoidGrowth city.reassignPopulation() cityScreen.update() @@ -63,7 +64,10 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin cell.add(label).pad(5f) if (cityScreen.canCityBeChanged()) { cell.touchable = Touchable.enabled - cell.onClick { + // Note the binding here only works when visible, so the main one is on CityStatsTable.miniStatsTable + // If we bind both, both are executed - so only add the one here that re-applies the current focus + val binding = if (city.cityAIFocus == focus) focus.binding else KeyboardBinding.None + cell.onActivation(binding = binding) { city.cityAIFocus = focus city.reassignPopulation() cityScreen.update() @@ -88,6 +92,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin fontSize = Constants.defaultFontSize, persistenceID = "CityStatsTable.CitizenManagement", startsOutOpened = false, + toggleKey = KeyboardBinding.CitizenManagement, onChange = onChange ) { it.add(this) diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt index d3c9779d5f..dad50aea2b 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt @@ -34,13 +34,14 @@ import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.getConsumesAmountString import com.unciv.ui.components.extensions.isEnabled -import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.packIfNeeded 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.KeyboardBinding +import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.ConfirmPopup import com.unciv.ui.popups.Popup @@ -129,13 +130,16 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { } fun update(selectedConstruction: IConstruction?) { - updateButtons(selectedConstruction) + updateQueueAndButtons(selectedConstruction) + updateAvailableConstructions() + } + + private fun updateQueueAndButtons(construction: IConstruction?) { + updateButtons(construction) updateConstructionQueue() upperTable.pack() - // This should work when set once only in addActorsToStage, but it doesn't (table invisible - why?) + // Need to reposition when height changes as setPosition's alignment does not persist, it's just a readability shortcut to calculate bottomLeft upperTable.setPosition(posFromEdge, stageHeight - posFromEdge, Align.topLeft) - - updateAvailableConstructions() lowerTableScrollCell.maxHeight(stageHeight - upperTable.height - 2 * posFromEdge) } @@ -279,11 +283,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { availableConstructionsTable.apply { clear() defaults().left().bottom() - addCategory("Units", units, maxButtonWidth) - addCategory("Buildings", buildableBuildings, maxButtonWidth) - addCategory("Wonders", buildableWonders, maxButtonWidth) - addCategory("National Wonders", buildableNationalWonders, maxButtonWidth) - addCategory("Other", specialConstructions, maxButtonWidth) + addCategory("Units", units, maxButtonWidth, KeyboardBinding.BuildUnits) + addCategory("Buildings", buildableBuildings, maxButtonWidth, KeyboardBinding.BuildBuildings) + addCategory("Wonders", buildableWonders, maxButtonWidth, KeyboardBinding.BuildWonders) + addCategory("National Wonders", buildableNationalWonders, maxButtonWidth, KeyboardBinding.BuildNationalWonders) + addCategory("Other", specialConstructions, maxButtonWidth, KeyboardBinding.BuildOther) pack() } @@ -345,6 +349,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { cityScreen.selectConstruction(constructionName) selectedQueueEntry = constructionQueueIndex cityScreen.update() + ensureQueueEntryVisible() } return table } @@ -464,7 +469,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { if (isSelectedQueueEntry()) { button = "Remove from queue".toTextButton() - button.onClick { + button.onActivation(binding = KeyboardBinding.AddConstruction) { cityConstructions.removeFromQueue(selectedQueueEntry, false) cityScreen.clearSelection() selectedQueueEntry = -1 @@ -476,7 +481,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { || cannotAddConstructionToQueue(construction, city, cityConstructions)) { button.disable() } else { - button.onClick(UncivSound.Silent) { + button.onActivation(binding = KeyboardBinding.AddConstruction, sound = UncivSound.Silent) { addConstructionToQueue(construction, cityConstructions) } } @@ -542,13 +547,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { val constructionBuyCost = construction.getStatBuyCost(city, stat)!! button.setText("Buy".tr() + " " + constructionBuyCost + stat.character) - button.onActivation { + button.onActivation(binding = KeyboardBinding.BuyConstruction) { button.disable() buyButtonOnClick(construction, stat) } button.isEnabled = isConstructionPurchaseAllowed(construction, stat, constructionBuyCost) - button.keyShortcuts.add('B') - button.addTooltip('B') // The key binding is done in CityScreen constructor preferredBuyStat = stat // Not very intelligent, but the least common currency "wins" } @@ -651,34 +654,40 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { cityScreen.update() } - private fun getRaisePriorityButton(constructionQueueIndex: Int, name: String, city: City): Table { - val tab = Table() - tab.add(ImageGetter.getArrowImage(Align.top).apply { color = Color.BLACK }.surroundWithCircle(40f)) - tab.touchable = Touchable.enabled - tab.onClick { - tab.touchable = Touchable.disabled - city.cityConstructions.raisePriority(constructionQueueIndex) + private fun getMovePriorityButton( + arrowDirection: Int, + binding: KeyboardBinding, + constructionQueueIndex: Int, + name: String, + movePriority: (Int) -> Int + ): Table { + val button = Table() + button.add(ImageGetter.getArrowImage(arrowDirection).apply { color = Color.BLACK }.surroundWithCircle(40f)) + button.touchable = Touchable.enabled + // Don't bind the queue reordering keys here - those should affect only the selected entry, not all of them + button.onActivation { + button.touchable = Touchable.disabled + selectedQueueEntry = movePriority(constructionQueueIndex) + // No need to call entire cityScreen.update() as reordering doesn't influence Stat or Map, + // nor does it need an expensive rebuild of the available constructions. + // Selection display may need to update as I can click the button of a non-selected entry. cityScreen.selectConstruction(name) - selectedQueueEntry = constructionQueueIndex - 1 - cityScreen.update() + cityScreen.updateWithoutConstructionAndMap() + updateQueueAndButtons(cityScreen.selectedConstruction) + ensureQueueEntryVisible() // Not passing current button info - already outdated, our parent is already removed from the stage hierarchy and replaced } - return tab + if (selectedQueueEntry == constructionQueueIndex) { + button.keyShortcuts.add(binding) // This binds without automatic tooltip + button.addTooltip(binding) + } + return button } - private fun getLowerPriorityButton(constructionQueueIndex: Int, name: String, city: City): Table { - val tab = Table() - tab.add(ImageGetter.getArrowImage(Align.bottom).apply { color = Color.BLACK }.surroundWithCircle(40f)) - tab.touchable = Touchable.enabled - tab.onClick { - tab.touchable = Touchable.disabled - city.cityConstructions.lowerPriority(constructionQueueIndex) - cityScreen.selectConstruction(name) - selectedQueueEntry = constructionQueueIndex + 1 - cityScreen.update() - } + private fun getRaisePriorityButton(constructionQueueIndex: Int, name: String, city: City) = + getMovePriorityButton(Align.top, KeyboardBinding.RaisePriority, constructionQueueIndex, name, city.cityConstructions::raisePriority) - return tab - } + private fun getLowerPriorityButton(constructionQueueIndex: Int, name: String, city: City) = + getMovePriorityButton(Align.bottom, KeyboardBinding.LowerPriority, constructionQueueIndex, name, city.cityConstructions::lowerPriority) private fun getRemoveFromQueueButton(constructionQueueIndex: Int, city: City): Table { val tab = Table() @@ -705,12 +714,24 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { .pad(4f) } + private fun ensureQueueEntryVisible() { + // Ensure the selected queue entry stays visible, and if moved to the "current" top slot, that the header is visible too + // This uses knowledge about how we build constructionsQueueTable without re-evaluating that stuff: + // Every odd row is a separator, cells have no padding, and there's one header on top and another between selectedQueueEntries 0 and 1 + val button = constructionsQueueTable.cells[if (selectedQueueEntry == 0) 2 else 2 * selectedQueueEntry + 4].actor + val buttonOrHeader = if (selectedQueueEntry == 0) constructionsQueueTable.cells[0].actor else button + // The 4f includes the two separators on top/bottom of the entry/header (the y offset we'd need cancels out with constructionsQueueTable.y being 2f as well): + val height = buttonOrHeader.y + buttonOrHeader.height - button.y + 4f + // Alternatively, scrollTo(..., true, true) would keep the selection as centered as possible: + constructionsQueueScrollPane.scrollTo(2f, button.y, button.width, height) + } + private fun resizeAvailableConstructionsScrollPane() { availableConstructionsScrollPane.height = min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight) lowerTable.pack() } - private fun Table.addCategory(title: String, list: ArrayList, prefWidth: Float) { + private fun Table.addCategory(title: String, list: ArrayList
, prefWidth: Float, toggleKey: KeyboardBinding) { if (list.isEmpty()) return if (rows > 0) addSeparator() @@ -719,6 +740,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { defaultPad = 0f, expanderWidth = prefWidth, persistenceID = "CityConstruction.$title", + toggleKey = toggleKey, onChange = { resizeAvailableConstructionsScrollPane() } ) { for (table in list) { diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityReligionInfoTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityReligionInfoTable.kt index 7e3a295d21..e59d61c763 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityReligionInfoTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityReligionInfoTable.kt @@ -19,6 +19,7 @@ import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparatorVertical import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.input.KeyboardBinding class CityReligionInfoTable( private val religionManager: CityReligionManager, @@ -103,6 +104,7 @@ class CityReligionInfoTable( defaultPad = 0f, persistenceID = "CityStatsTable.Religion", startsOutOpened = false, + toggleKey = KeyboardBinding.ReligionDetail, onChange = onChange ) { defaults().center().pad(5f) diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt index 71710566cd..51c20b854f 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt @@ -27,6 +27,7 @@ import com.unciv.ui.components.extensions.packIfNeeded import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyShortcutDispatcherVeto +import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick @@ -139,8 +140,8 @@ class CityScreen( stage.addActor(exitCityButton) update() - globalShortcuts.add(Input.Keys.LEFT) { page(-1) } - globalShortcuts.add(Input.Keys.RIGHT) { page(1) } + globalShortcuts.add(KeyboardBinding.PreviousCity) { page(-1) } + globalShortcuts.add(KeyboardBinding.NextCity) { page(1) } } internal fun update() { @@ -150,6 +151,19 @@ class CityScreen( constructionsTable.isVisible = true constructionsTable.update(selectedConstruction) + updateWithoutConstructionAndMap() + + // 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() + } + } + + internal fun updateWithoutConstructionAndMap() { // Bottom right: Tile or selected construction info tileTable.update(selectedTile) tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) @@ -185,15 +199,6 @@ class CityScreen( // 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() - } } fun canCityBeChanged(): Boolean { diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityScreenTileTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityScreenTileTable.kt index 8ee9ef3ce5..0992fe80d4 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityScreenTileTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityScreenTileTable.kt @@ -16,6 +16,7 @@ import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton +import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen @@ -57,13 +58,11 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() { if (city.expansion.canBuyTile(selectedTile)) { val goldCostOfTile = city.expansion.getGoldCostOfTile(selectedTile) val buyTileButton = "Buy for [$goldCostOfTile] gold".toTextButton() - buyTileButton.onActivation { + buyTileButton.onActivation(binding = KeyboardBinding.BuyTile) { buyTileButton.disable() cityScreen.askToBuyTile(selectedTile) } - buyTileButton.keyShortcuts.add('T') buyTileButton.isEnabled = cityScreen.canChangeState && city.civ.hasStatToBuy(Stat.Gold, goldCostOfTile) - buyTileButton.addTooltip('T') // The key binding is done in CityScreen constructor innerTable.add(buyTileButton).padTop(5f).row() } diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityStatsTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityStatsTable.kt index 0d8fee4d75..c5b2f090a9 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityStatsTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityStatsTable.kt @@ -26,6 +26,7 @@ import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.toGroup import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton +import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onClick import com.unciv.ui.images.ImageGetter @@ -45,7 +46,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() { private val detailedStatsButton = "Stats".toTextButton().apply { labelCell.pad(10f) - onActivation { + onActivation(binding = KeyboardBinding.ShowStats) { DetailedStatsPopup(cityScreen).open() } } @@ -83,21 +84,19 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() { for ((stat, amount) in city.cityStats.currentCityStats) { if (stat == Stat.Faith && !city.civ.gameInfo.isReligionEnabled()) continue val icon = Table() - if (city.cityAIFocus.stat == stat) { + val focus = CityFocus.safeValueOf(stat) + val toggledFocus = if (focus == city.cityAIFocus) { icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = selected)) - if (cityScreen.canCityBeChanged()) { - icon.onClick { - city.cityAIFocus = CityFocus.NoFocus - city.reassignPopulation(); cityScreen.update() - } - } + CityFocus.NoFocus } else { icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR)) - if (cityScreen.canCityBeChanged()) { - icon.onClick { - city.cityAIFocus = city.cityAIFocus.safeValueOf(stat) - city.reassignPopulation(); cityScreen.update() - } + focus + } + if (cityScreen.canCityBeChanged()) { + icon.onActivation(binding = toggledFocus.binding) { + city.cityAIFocus = toggledFocus + city.reassignPopulation() + cityScreen.update() } } miniStatsTable.add(icon).size(27f).padRight(3f) @@ -247,7 +246,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() { otherBuildings.sortBy { it.name } val totalTable = Table() - lowerTable.addCategory("Buildings", totalTable, false) + lowerTable.addCategory("Buildings", totalTable, KeyboardBinding.BuildingsDetail, false) if (specialistBuildings.isNotEmpty()) { val specialistBuildingsTable = Table() @@ -327,13 +326,18 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() { destinationTable.add(button).pad(1f).padBottom(2f).padTop(2f).expandX().right().row() } - private fun Table.addCategory(category: String, showHideTable: Table, startsOpened: Boolean = true, innerPadding: Float = 10f) : ExpanderTab { + private fun Table.addCategory( + category: String, + showHideTable: Table, + toggleKey: KeyboardBinding, + startsOpened: Boolean = true + ) : ExpanderTab { val expanderTab = ExpanderTab( title = category, fontSize = Constants.defaultFontSize, persistenceID = "CityInfo.$category", startsOutOpened = startsOpened, - defaultPad = innerPadding, + toggleKey = toggleKey, onChange = { onContentResize() } ) { it.add(showHideTable).fillX().right() @@ -392,7 +396,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() { greatPeopleTable.add(ImageGetter.getConstructionPortrait(greatPersonName, 50f)).row() } - lowerTable.addCategory("Great People", greatPeopleTable) + lowerTable.addCategory("Great People", greatPeopleTable, KeyboardBinding.GreatPeopleDetail) } } diff --git a/core/src/com/unciv/ui/screens/cityscreen/DetailedStatsPopup.kt b/core/src/com/unciv/ui/screens/cityscreen/DetailedStatsPopup.kt index 254a269248..5f408bdf33 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/DetailedStatsPopup.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/DetailedStatsPopup.kt @@ -21,6 +21,7 @@ import com.unciv.ui.components.extensions.packIfNeeded 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.input.KeyboardBinding import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.popups.Popup import com.unciv.ui.screens.basescreen.BaseScreen @@ -176,14 +177,11 @@ class DetailedStatsPopup( val button = label .surroundWithCircle(25f, color = BaseScreen.skinStrings.skinConfig.baseColor) .surroundWithCircle(27f, false) - button.keyShortcuts.run { - add(Input.Keys.PLUS) - add(Input.Keys.NUMPAD_ADD) - } - button.onActivation { + button.onActivation(binding = KeyboardBinding.ShowStatDetails) { isDetailed = !isDetailed update() } + button.keyShortcuts.add(Input.Keys.PLUS) //todo Choose alternative (alt binding, remove, auto-equivalence, multikey bindings) return button } diff --git a/core/src/com/unciv/ui/screens/cityscreen/SpecialistAllocationTable.kt b/core/src/com/unciv/ui/screens/cityscreen/SpecialistAllocationTable.kt index ca446d6925..384180724f 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/SpecialistAllocationTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/SpecialistAllocationTable.kt @@ -12,6 +12,7 @@ import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.toGroup import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.onClick import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen @@ -141,6 +142,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base fontSize = Constants.defaultFontSize, persistenceID = "CityStatsTable.Specialists", startsOutOpened = true, + toggleKey = KeyboardBinding.SpecialistDetail, onChange = onChange ) { it.add(this) diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt index eb1caa757c..c174357f4a 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt @@ -52,13 +52,12 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { if (unitAction.type == UnitActionType.Promote && unitAction.action != null) actionButton.color = Color.GREEN.cpy().lerp(Color.WHITE, 0.5f) - actionButton.addTooltip(binding) actionButton.pack() if (unitAction.action == null) { actionButton.disable() } else { - actionButton.onActivation(unitAction.uncivSound) { + actionButton.onActivation(unitAction.uncivSound, binding) { unitAction.action.invoke() GUI.setUpdateWorldOnNextRender() // We keep the unit action/selection overlay from the previous unit open even when already selecting another unit @@ -70,7 +69,6 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { worldScreen.switchToNextUnit() } } - actionButton.keyShortcuts.add(binding) } return actionButton