Key bindings CityScreen (#9828)

* CityScreen keyboard bindings - ExpanderTab update

* CityScreen keyboard bindings - Linting

* CityScreen keyboard bindings - Main Keys

* CityScreen keyboard bindings - Queue

* CityScreen keyboard bindings - Fix Expander scroll-to
This commit is contained in:
SomeTroglodyte 2023-07-30 16:40:05 +02:00 committed by GitHub
parent a5e1f6d800
commit 52e756e9fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 239 additions and 100 deletions

View File

@ -9,7 +9,7 @@ import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PopupAlert 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.map.tile.Tile
import com.unciv.logic.multiplayer.isUsersTurn import com.unciv.logic.multiplayer.isUsersTurn
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
@ -752,13 +752,15 @@ class CityConstructions : IsPartOfGameInfoSerialization {
} else true // we're just continuing the regular queue } else true // we're just continuing the regular queue
} }
fun raisePriority(constructionQueueIndex: Int) { fun raisePriority(constructionQueueIndex: Int): Int {
constructionQueue.swap(constructionQueueIndex - 1, constructionQueueIndex) constructionQueue.swap(constructionQueueIndex - 1, constructionQueueIndex)
return constructionQueueIndex - 1
} }
// Lowering == Highering next element in queue // Lowering == Highering next element in queue
fun lowerPriority(constructionQueueIndex: Int) { fun lowerPriority(constructionQueueIndex: Int): Int {
raisePriority(constructionQueueIndex + 1) raisePriority(constructionQueueIndex + 1)
return constructionQueueIndex + 1
} }
private fun MutableList<String>.swap(idx1: Int, idx2: Int) { private fun MutableList<String>.swap(idx1: Int, idx2: Int) {
@ -778,7 +780,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
val tileForImprovement = getTileForImprovement(improvement.name) ?: return val tileForImprovement = getTileForImprovement(improvement.name) ?: return
tileForImprovement.stopWorkingOnImprovement() // clears mark tileForImprovement.stopWorkingOnImprovement() // clears mark
if (removeOnly) return 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) tileForImprovement.changeImprovement(improvement.name)
city.civ.lastSeenImprovement[tileForImprovement.position] = improvement.name city.civ.lastSeenImprovement[tileForImprovement.position] = improvement.name
city.cityStats.update() city.cityStats.update()
@ -794,11 +796,11 @@ class CityConstructions : IsPartOfGameInfoSerialization {
*/ */
fun removeCreateOneImprovementConstruction(improvement: String) { fun removeCreateOneImprovementConstruction(improvement: String) {
val ruleset = city.getRuleset() val ruleset = city.getRuleset()
val indexToRemove = constructionQueue.withIndex().mapNotNull { val indexToRemove = constructionQueue.withIndex().firstNotNullOfOrNull {
val construction = getConstruction(it.value) val construction = getConstruction(it.value)
val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name
it.index.takeIf { buildingImprovement == improvement } it.index.takeIf { buildingImprovement == improvement }
}.firstOrNull() ?: return } ?: return
constructionQueue.removeAt(indexToRemove) constructionQueue.removeAt(indexToRemove)

View File

@ -1,12 +1,29 @@
package com.unciv.logic.city package com.unciv.logic.city
import com.unciv.logic.IsPartOfGameInfoSerialization 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.Stat
import com.unciv.models.stats.Stats 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) : * Controls automatic worker-to-tile assignment
IsPartOfGameInfoSerialization { * @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) { NoFocus("Default Focus", true, null) {
override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see 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), FaithFocus("[${Stat.Faith.name}] Focus", true, Stat.Faith),
HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness); HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness),
//GreatPersonFocus; //GreatPersonFocus
;
// endregion Enum values
val binding: KeyboardBinding =
binding ?:
KeyboardBinding.values().firstOrNull { it.name == name } ?:
KeyboardBinding.None
open fun getStatMultiplier(stat: Stat) = when (this.stat) { open fun getStatMultiplier(stat: Stat) = when (this.stat) {
stat -> 3f stat -> 3f
@ -42,7 +67,9 @@ enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Sta
} }
} }
fun safeValueOf(stat: Stat): CityFocus { companion object {
return values().firstOrNull { it.stat == stat } ?: NoFocus fun safeValueOf(stat: Stat): CityFocus {
return values().firstOrNull { it.stat == stat } ?: NoFocus
}
} }
} }

View File

@ -5,13 +5,17 @@ import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction 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.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel 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 import com.unciv.ui.screens.basescreen.BaseScreen
/** /**
@ -36,6 +40,7 @@ class ExpanderTab(
headerPad: Float = 10f, headerPad: Float = 10f,
expanderWidth: Float = 0f, expanderWidth: Float = 0f,
private val persistenceID: String? = null, private val persistenceID: String? = null,
toggleKey: KeyboardBinding = KeyboardBinding.None,
private val onChange: (() -> Unit)? = null, private val onChange: (() -> Unit)? = null,
initContent: ((Table) -> Unit)? = null initContent: ((Table) -> Unit)? = null
): Table(BaseScreen.skin) { ): Table(BaseScreen.skin) {
@ -81,7 +86,8 @@ class ExpanderTab(
header.add(headerLabel) header.add(headerLabel)
header.add(headerIcon).size(arrowSize).align(Align.center) header.add(headerIcon).size(arrowSize).align(Align.center)
header.touchable= Touchable.enabled 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) if (expanderWidth != 0f)
defaults().minWidth(expanderWidth) defaults().minWidth(expanderWidth)
defaults().growX() defaults().growX()
@ -126,9 +132,44 @@ class ExpanderTab(
/** Toggle [isOpen], animated */ /** Toggle [isOpen], animated */
fun toggle() { fun toggle() {
isOpen = !isOpen 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) { fun setText(text: String) {
headerLabel.setText(text) headerLabel.setText(text)
} }

View File

@ -2,6 +2,7 @@ package com.unciv.ui.components.input
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.unciv.Constants import com.unciv.Constants
import com.unciv.models.stats.Stat
private val unCamelCaseRegex = Regex("([A-Z])([A-Z])([a-z])|([a-z])([A-Z])") 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), HideAdditionalActions(Category.UnitActions,"Back", Input.Keys.PAGE_UP),
AddInCapital(Category.UnitActions, "Add in capital", 'g'), 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 // Popups
Confirm(Category.Popups, "Confirm Dialog", 'y'), Confirm(Category.Popups, "Confirm Dialog", 'y'),
Cancel(Category.Popups, "Cancel Dialog", 'n'), 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 // Conflict checking within group disabled, but any key assigned on WorldScreen is a problem
override fun checkConflictsIn() = sequenceOf(WorldScreen) override fun checkConflictsIn() = sequenceOf(WorldScreen)
}, },
CityScreen,
Popups Popups
; ;
val label = unCamelCase(name) val label = unCamelCase(name)

View File

@ -4,10 +4,11 @@ import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.city.CityFocus import com.unciv.logic.city.CityFocus
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.ExpanderTab 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.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) { class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin) {
val city = cityScreen.city val city = cityScreen.city
@ -24,7 +25,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin
resetCell.add(resetLabel).pad(5f) resetCell.add(resetLabel).pad(5f)
if (cityScreen.canCityBeChanged()) { if (cityScreen.canCityBeChanged()) {
resetCell.touchable = Touchable.enabled resetCell.touchable = Touchable.enabled
resetCell.onClick { resetCell.onActivation(binding = KeyboardBinding.ResetCitizens) {
city.reassignPopulation(true) city.reassignPopulation(true)
cityScreen.update() cityScreen.update()
} }
@ -41,7 +42,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin
avoidCell.add(avoidLabel).pad(5f) avoidCell.add(avoidLabel).pad(5f)
if (cityScreen.canCityBeChanged()) { if (cityScreen.canCityBeChanged()) {
avoidCell.touchable = Touchable.enabled avoidCell.touchable = Touchable.enabled
avoidCell.onClick { avoidCell.onActivation(binding = KeyboardBinding.AvoidGrowth) {
city.avoidGrowth = !city.avoidGrowth city.avoidGrowth = !city.avoidGrowth
city.reassignPopulation() city.reassignPopulation()
cityScreen.update() cityScreen.update()
@ -63,7 +64,10 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin
cell.add(label).pad(5f) cell.add(label).pad(5f)
if (cityScreen.canCityBeChanged()) { if (cityScreen.canCityBeChanged()) {
cell.touchable = Touchable.enabled 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.cityAIFocus = focus
city.reassignPopulation() city.reassignPopulation()
cityScreen.update() cityScreen.update()
@ -88,6 +92,7 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin
fontSize = Constants.defaultFontSize, fontSize = Constants.defaultFontSize,
persistenceID = "CityStatsTable.CitizenManagement", persistenceID = "CityStatsTable.CitizenManagement",
startsOutOpened = false, startsOutOpened = false,
toggleKey = KeyboardBinding.CitizenManagement,
onChange = onChange onChange = onChange
) { ) {
it.add(this) it.add(this)

View File

@ -34,13 +34,14 @@ import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.getConsumesAmountString import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.extensions.isEnabled 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.onActivation
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.packIfNeeded import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton 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.images.ImageGetter
import com.unciv.ui.popups.ConfirmPopup import com.unciv.ui.popups.ConfirmPopup
import com.unciv.ui.popups.Popup import com.unciv.ui.popups.Popup
@ -129,13 +130,16 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
} }
fun update(selectedConstruction: IConstruction?) { fun update(selectedConstruction: IConstruction?) {
updateButtons(selectedConstruction) updateQueueAndButtons(selectedConstruction)
updateAvailableConstructions()
}
private fun updateQueueAndButtons(construction: IConstruction?) {
updateButtons(construction)
updateConstructionQueue() updateConstructionQueue()
upperTable.pack() 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) upperTable.setPosition(posFromEdge, stageHeight - posFromEdge, Align.topLeft)
updateAvailableConstructions()
lowerTableScrollCell.maxHeight(stageHeight - upperTable.height - 2 * posFromEdge) lowerTableScrollCell.maxHeight(stageHeight - upperTable.height - 2 * posFromEdge)
} }
@ -279,11 +283,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
availableConstructionsTable.apply { availableConstructionsTable.apply {
clear() clear()
defaults().left().bottom() defaults().left().bottom()
addCategory("Units", units, maxButtonWidth) addCategory("Units", units, maxButtonWidth, KeyboardBinding.BuildUnits)
addCategory("Buildings", buildableBuildings, maxButtonWidth) addCategory("Buildings", buildableBuildings, maxButtonWidth, KeyboardBinding.BuildBuildings)
addCategory("Wonders", buildableWonders, maxButtonWidth) addCategory("Wonders", buildableWonders, maxButtonWidth, KeyboardBinding.BuildWonders)
addCategory("National Wonders", buildableNationalWonders, maxButtonWidth) addCategory("National Wonders", buildableNationalWonders, maxButtonWidth, KeyboardBinding.BuildNationalWonders)
addCategory("Other", specialConstructions, maxButtonWidth) addCategory("Other", specialConstructions, maxButtonWidth, KeyboardBinding.BuildOther)
pack() pack()
} }
@ -345,6 +349,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
cityScreen.selectConstruction(constructionName) cityScreen.selectConstruction(constructionName)
selectedQueueEntry = constructionQueueIndex selectedQueueEntry = constructionQueueIndex
cityScreen.update() cityScreen.update()
ensureQueueEntryVisible()
} }
return table return table
} }
@ -464,7 +469,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
if (isSelectedQueueEntry()) { if (isSelectedQueueEntry()) {
button = "Remove from queue".toTextButton() button = "Remove from queue".toTextButton()
button.onClick { button.onActivation(binding = KeyboardBinding.AddConstruction) {
cityConstructions.removeFromQueue(selectedQueueEntry, false) cityConstructions.removeFromQueue(selectedQueueEntry, false)
cityScreen.clearSelection() cityScreen.clearSelection()
selectedQueueEntry = -1 selectedQueueEntry = -1
@ -476,7 +481,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
|| cannotAddConstructionToQueue(construction, city, cityConstructions)) { || cannotAddConstructionToQueue(construction, city, cityConstructions)) {
button.disable() button.disable()
} else { } else {
button.onClick(UncivSound.Silent) { button.onActivation(binding = KeyboardBinding.AddConstruction, sound = UncivSound.Silent) {
addConstructionToQueue(construction, cityConstructions) addConstructionToQueue(construction, cityConstructions)
} }
} }
@ -542,13 +547,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
val constructionBuyCost = construction.getStatBuyCost(city, stat)!! val constructionBuyCost = construction.getStatBuyCost(city, stat)!!
button.setText("Buy".tr() + " " + constructionBuyCost + stat.character) button.setText("Buy".tr() + " " + constructionBuyCost + stat.character)
button.onActivation { button.onActivation(binding = KeyboardBinding.BuyConstruction) {
button.disable() button.disable()
buyButtonOnClick(construction, stat) buyButtonOnClick(construction, stat)
} }
button.isEnabled = isConstructionPurchaseAllowed(construction, stat, constructionBuyCost) 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" preferredBuyStat = stat // Not very intelligent, but the least common currency "wins"
} }
@ -651,34 +654,40 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
cityScreen.update() cityScreen.update()
} }
private fun getRaisePriorityButton(constructionQueueIndex: Int, name: String, city: City): Table { private fun getMovePriorityButton(
val tab = Table() arrowDirection: Int,
tab.add(ImageGetter.getArrowImage(Align.top).apply { color = Color.BLACK }.surroundWithCircle(40f)) binding: KeyboardBinding,
tab.touchable = Touchable.enabled constructionQueueIndex: Int,
tab.onClick { name: String,
tab.touchable = Touchable.disabled movePriority: (Int) -> Int
city.cityConstructions.raisePriority(constructionQueueIndex) ): 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) cityScreen.selectConstruction(name)
selectedQueueEntry = constructionQueueIndex - 1 cityScreen.updateWithoutConstructionAndMap()
cityScreen.update() 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 { private fun getRaisePriorityButton(constructionQueueIndex: Int, name: String, city: City) =
val tab = Table() getMovePriorityButton(Align.top, KeyboardBinding.RaisePriority, constructionQueueIndex, name, city.cityConstructions::raisePriority)
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()
}
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 { private fun getRemoveFromQueueButton(constructionQueueIndex: Int, city: City): Table {
val tab = Table() val tab = Table()
@ -705,12 +714,24 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
.pad(4f) .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() { private fun resizeAvailableConstructionsScrollPane() {
availableConstructionsScrollPane.height = min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight) availableConstructionsScrollPane.height = min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight)
lowerTable.pack() lowerTable.pack()
} }
private fun Table.addCategory(title: String, list: ArrayList<Table>, prefWidth: Float) { private fun Table.addCategory(title: String, list: ArrayList<Table>, prefWidth: Float, toggleKey: KeyboardBinding) {
if (list.isEmpty()) return if (list.isEmpty()) return
if (rows > 0) addSeparator() if (rows > 0) addSeparator()
@ -719,6 +740,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
defaultPad = 0f, defaultPad = 0f,
expanderWidth = prefWidth, expanderWidth = prefWidth,
persistenceID = "CityConstruction.$title", persistenceID = "CityConstruction.$title",
toggleKey = toggleKey,
onChange = { resizeAvailableConstructionsScrollPane() } onChange = { resizeAvailableConstructionsScrollPane() }
) { ) {
for (table in list) { for (table in list) {

View File

@ -19,6 +19,7 @@ import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.addSeparatorVertical import com.unciv.ui.components.extensions.addSeparatorVertical
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyboardBinding
class CityReligionInfoTable( class CityReligionInfoTable(
private val religionManager: CityReligionManager, private val religionManager: CityReligionManager,
@ -103,6 +104,7 @@ class CityReligionInfoTable(
defaultPad = 0f, defaultPad = 0f,
persistenceID = "CityStatsTable.Religion", persistenceID = "CityStatsTable.Religion",
startsOutOpened = false, startsOutOpened = false,
toggleKey = KeyboardBinding.ReligionDetail,
onChange = onChange onChange = onChange
) { ) {
defaults().center().pad(5f) defaults().center().pad(5f)

View File

@ -27,6 +27,7 @@ import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto 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.keyShortcuts
import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
@ -139,8 +140,8 @@ class CityScreen(
stage.addActor(exitCityButton) stage.addActor(exitCityButton)
update() update()
globalShortcuts.add(Input.Keys.LEFT) { page(-1) } globalShortcuts.add(KeyboardBinding.PreviousCity) { page(-1) }
globalShortcuts.add(Input.Keys.RIGHT) { page(1) } globalShortcuts.add(KeyboardBinding.NextCity) { page(1) }
} }
internal fun update() { internal fun update() {
@ -150,6 +151,19 @@ class CityScreen(
constructionsTable.isVisible = true constructionsTable.isVisible = true
constructionsTable.update(selectedConstruction) 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 // Bottom right: Tile or selected construction info
tileTable.update(selectedTile) tileTable.update(selectedTile)
tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight)
@ -185,15 +199,6 @@ class CityScreen(
// Top center: Annex/Raze button // Top center: Annex/Raze button
updateAnnexAndRazeCityButton() 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 { fun canCityBeChanged(): Boolean {

View File

@ -16,6 +16,7 @@ import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
@ -57,13 +58,11 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
if (city.expansion.canBuyTile(selectedTile)) { if (city.expansion.canBuyTile(selectedTile)) {
val goldCostOfTile = city.expansion.getGoldCostOfTile(selectedTile) val goldCostOfTile = city.expansion.getGoldCostOfTile(selectedTile)
val buyTileButton = "Buy for [$goldCostOfTile] gold".toTextButton() val buyTileButton = "Buy for [$goldCostOfTile] gold".toTextButton()
buyTileButton.onActivation { buyTileButton.onActivation(binding = KeyboardBinding.BuyTile) {
buyTileButton.disable() buyTileButton.disable()
cityScreen.askToBuyTile(selectedTile) cityScreen.askToBuyTile(selectedTile)
} }
buyTileButton.keyShortcuts.add('T')
buyTileButton.isEnabled = cityScreen.canChangeState && city.civ.hasStatToBuy(Stat.Gold, goldCostOfTile) 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() innerTable.add(buyTileButton).padTop(5f).row()
} }

View File

@ -26,6 +26,7 @@ import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toGroup import com.unciv.ui.components.extensions.toGroup
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton 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.onActivation
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
@ -45,7 +46,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
private val detailedStatsButton = "Stats".toTextButton().apply { private val detailedStatsButton = "Stats".toTextButton().apply {
labelCell.pad(10f) labelCell.pad(10f)
onActivation { onActivation(binding = KeyboardBinding.ShowStats) {
DetailedStatsPopup(cityScreen).open() DetailedStatsPopup(cityScreen).open()
} }
} }
@ -83,21 +84,19 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
for ((stat, amount) in city.cityStats.currentCityStats) { for ((stat, amount) in city.cityStats.currentCityStats) {
if (stat == Stat.Faith && !city.civ.gameInfo.isReligionEnabled()) continue if (stat == Stat.Faith && !city.civ.gameInfo.isReligionEnabled()) continue
val icon = Table() 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)) icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = selected))
if (cityScreen.canCityBeChanged()) { CityFocus.NoFocus
icon.onClick {
city.cityAIFocus = CityFocus.NoFocus
city.reassignPopulation(); cityScreen.update()
}
}
} else { } else {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR)) icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR))
if (cityScreen.canCityBeChanged()) { focus
icon.onClick { }
city.cityAIFocus = city.cityAIFocus.safeValueOf(stat) if (cityScreen.canCityBeChanged()) {
city.reassignPopulation(); cityScreen.update() icon.onActivation(binding = toggledFocus.binding) {
} city.cityAIFocus = toggledFocus
city.reassignPopulation()
cityScreen.update()
} }
} }
miniStatsTable.add(icon).size(27f).padRight(3f) miniStatsTable.add(icon).size(27f).padRight(3f)
@ -247,7 +246,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
otherBuildings.sortBy { it.name } otherBuildings.sortBy { it.name }
val totalTable = Table() val totalTable = Table()
lowerTable.addCategory("Buildings", totalTable, false) lowerTable.addCategory("Buildings", totalTable, KeyboardBinding.BuildingsDetail, false)
if (specialistBuildings.isNotEmpty()) { if (specialistBuildings.isNotEmpty()) {
val specialistBuildingsTable = Table() 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() 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( val expanderTab = ExpanderTab(
title = category, title = category,
fontSize = Constants.defaultFontSize, fontSize = Constants.defaultFontSize,
persistenceID = "CityInfo.$category", persistenceID = "CityInfo.$category",
startsOutOpened = startsOpened, startsOutOpened = startsOpened,
defaultPad = innerPadding, toggleKey = toggleKey,
onChange = { onContentResize() } onChange = { onContentResize() }
) { ) {
it.add(showHideTable).fillX().right() it.add(showHideTable).fillX().right()
@ -392,7 +396,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
greatPeopleTable.add(ImageGetter.getConstructionPortrait(greatPersonName, 50f)).row() greatPeopleTable.add(ImageGetter.getConstructionPortrait(greatPersonName, 50f)).row()
} }
lowerTable.addCategory("Great People", greatPeopleTable) lowerTable.addCategory("Great People", greatPeopleTable, KeyboardBinding.GreatPeopleDetail)
} }
} }

View File

@ -21,6 +21,7 @@ import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.components.extensions.pad import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.IconCircleGroup
import com.unciv.ui.popups.Popup import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
@ -176,14 +177,11 @@ class DetailedStatsPopup(
val button = label val button = label
.surroundWithCircle(25f, color = BaseScreen.skinStrings.skinConfig.baseColor) .surroundWithCircle(25f, color = BaseScreen.skinStrings.skinConfig.baseColor)
.surroundWithCircle(27f, false) .surroundWithCircle(27f, false)
button.keyShortcuts.run { button.onActivation(binding = KeyboardBinding.ShowStatDetails) {
add(Input.Keys.PLUS)
add(Input.Keys.NUMPAD_ADD)
}
button.onActivation {
isDetailed = !isDetailed isDetailed = !isDetailed
update() update()
} }
button.keyShortcuts.add(Input.Keys.PLUS) //todo Choose alternative (alt binding, remove, auto-equivalence, multikey bindings)
return button return button
} }

View File

@ -12,6 +12,7 @@ import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toGroup import com.unciv.ui.components.extensions.toGroup
import com.unciv.ui.components.extensions.toLabel 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.components.input.onClick
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
@ -141,6 +142,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base
fontSize = Constants.defaultFontSize, fontSize = Constants.defaultFontSize,
persistenceID = "CityStatsTable.Specialists", persistenceID = "CityStatsTable.Specialists",
startsOutOpened = true, startsOutOpened = true,
toggleKey = KeyboardBinding.SpecialistDetail,
onChange = onChange onChange = onChange
) { ) {
it.add(this) it.add(this)

View File

@ -52,13 +52,12 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
if (unitAction.type == UnitActionType.Promote && unitAction.action != null) if (unitAction.type == UnitActionType.Promote && unitAction.action != null)
actionButton.color = Color.GREEN.cpy().lerp(Color.WHITE, 0.5f) actionButton.color = Color.GREEN.cpy().lerp(Color.WHITE, 0.5f)
actionButton.addTooltip(binding)
actionButton.pack() actionButton.pack()
if (unitAction.action == null) { if (unitAction.action == null) {
actionButton.disable() actionButton.disable()
} else { } else {
actionButton.onActivation(unitAction.uncivSound) { actionButton.onActivation(unitAction.uncivSound, binding) {
unitAction.action.invoke() unitAction.action.invoke()
GUI.setUpdateWorldOnNextRender() GUI.setUpdateWorldOnNextRender()
// We keep the unit action/selection overlay from the previous unit open even when already selecting another unit // 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() worldScreen.switchToNextUnit()
} }
} }
actionButton.keyShortcuts.add(binding)
} }
return actionButton return actionButton