From e109848e280cbc1b7fd9934beddc2f91fb2a20cf Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Mon, 12 Jun 2023 08:55:25 +0200 Subject: [PATCH] Nation picker gets an Icon View, keyboard selection, and fixed sort (#9553) * Nation picker gets an Icon View, keyboard selection, and fixed sort * Minor linting * Proper centering in the selection circle * Fix merge mistakes * Nation picker Icon View - reviews and layout tweaks --- .../com/unciv/models/metadata/GameSettings.kt | 4 + .../components/input/KeyShortcutDispatcher.kt | 2 + core/src/com/unciv/ui/images/ImageGetter.kt | 2 +- .../newgamescreen/NationPickerPopup.kt | 323 ++++++++++++++++++ .../ui/screens/newgamescreen/NationTable.kt | 4 +- .../newgamescreen/PlayerPickerTable.kt | 117 ------- 6 files changed, 331 insertions(+), 121 deletions(-) create mode 100644 core/src/com/unciv/ui/screens/newgamescreen/NationPickerPopup.kt diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 881dad824d..cbb0434864 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -121,6 +121,10 @@ class GameSettings { /** If on, selected notifications are drawn enlarged with wider padding */ var enlargeSelectedNotification = true + /** Whether the Nation Picker shows icons only or the horizontal "civBlocks" with leader/nation name */ + enum class NationPickerListMode { Icons, List } + var nationPickerListMode = NationPickerListMode.List + /** used to migrate from older versions of the settings */ var version: Int? = null diff --git a/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt b/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt index 2a889ce04e..e86bd2b0ad 100644 --- a/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt +++ b/core/src/com/unciv/ui/components/input/KeyShortcutDispatcher.kt @@ -14,6 +14,8 @@ open class KeyShortcutDispatcher { private data class ShortcutAction(val shortcut: KeyShortcut, val action: () -> Unit) private val shortcuts: MutableList = mutableListOf() + fun clear() = shortcuts.clear() + fun add(shortcut: KeyShortcut?, action: (() -> Unit)?) { if (action == null || shortcut == null) return shortcuts.removeIf { it.shortcut == shortcut } diff --git a/core/src/com/unciv/ui/images/ImageGetter.kt b/core/src/com/unciv/ui/images/ImageGetter.kt index 727d272034..104121566e 100644 --- a/core/src/com/unciv/ui/images/ImageGetter.kt +++ b/core/src/com/unciv/ui/images/ImageGetter.kt @@ -229,7 +229,7 @@ object ImageGetter { } fun getRandomNationPortrait(size: Float): Portrait { - return PortraitNation("Random", size) + return PortraitNation(Constants.random, size) } fun getUnitIcon(unitName: String, color: Color = Color.BLACK): Image { diff --git a/core/src/com/unciv/ui/screens/newgamescreen/NationPickerPopup.kt b/core/src/com/unciv/ui/screens/newgamescreen/NationPickerPopup.kt new file mode 100644 index 0000000000..d6ad42b47b --- /dev/null +++ b/core/src/com/unciv/ui/screens/newgamescreen/NationPickerPopup.kt @@ -0,0 +1,323 @@ +package com.unciv.ui.screens.newgamescreen + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.Touchable +import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction +import com.badlogic.gdx.scenes.scene2d.ui.Container +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup +import com.badlogic.gdx.utils.Align +import com.unciv.Constants +import com.unciv.GUI +import com.unciv.UncivGame +import com.unciv.logic.civilization.PlayerType +import com.unciv.models.metadata.GameSettings.NationPickerListMode +import com.unciv.models.metadata.Player +import com.unciv.models.ruleset.nation.Nation +import com.unciv.models.translations.tr +import com.unciv.ui.audio.MusicMood +import com.unciv.ui.audio.MusicTrackChooserFlags +import com.unciv.ui.components.AutoScrollPane +import com.unciv.ui.components.UncivTooltip.Companion.addTooltip +import com.unciv.ui.components.extensions.isNarrowerThan4to3 +import com.unciv.ui.components.extensions.toImageButton +import com.unciv.ui.components.input.KeyCharAndCode +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.input.onDoubleClick +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.images.Portrait +import com.unciv.ui.popups.Popup +import com.unciv.ui.screens.basescreen.BaseScreen +import kotlin.math.PI +import kotlin.math.cos + +internal class NationPickerPopup( + private val playerPicker: PlayerPickerTable, + private val player: Player, + private val noRandom: Boolean +) : Popup(playerPicker.previousScreen as BaseScreen, Scrollability.None) { + companion object { + // Note - innerTable has pad(20f) and defaults().pad(5f), so content bottomLeft is at x=25/y=25 + // These are used for the Close/OK buttons in the lower left/right corners: + const val buttonsCircleSize = 70f + const val buttonsIconSize = 50f + const val buttonsOffsetFromEdge = 5f + val buttonsBackColor: Color = Color.BLACK.cpy().apply { a = 0.67f } + // Icon view sizing + const val iconViewIconSize = 50f // Portrait lies and will be bigger than asked for (55f) + const val iconViewCellSize = 60f // Difference to the above is used for selection highlight + const val iconViewSpacing = 5f // Extra spacing between icons + const val iconViewPadTop = 18f // align top row with nation icon in detail pane - empiric + // Allow scrolling the bottom left icons _out_ from under the close/toggle view buttons + const val iconViewPadBottom = buttonsCircleSize + buttonsOffsetFromEdge - 25f + iconViewSpacing + const val iconViewPadHorz = iconViewSpacing / 2 // a little empiric + } + + private val previousScreen = playerPicker.previousScreen + private val ruleset = previousScreen.ruleset + private val settings = GUI.getSettings() + + // This Popup's body has two halves of same size, either side by side or arranged vertically + // depending on screen proportions - determine height for one of those + private val partHeight = stageToShowOn.height * (if (stageToShowOn.isNarrowerThan4to3()) 0.45f else 0.8f) + private val civBlocksWidth = playerPicker.civBlocksWidth + + private val nationListTable = Table() + private val nationListScroll = AutoScrollPane(nationListTable) + private val nationDetailsTable = Table() + private val nationDetailsScroll = AutoScrollPane(nationDetailsTable) + + private class SelectInfo( + val nation: Nation, + val scrollY: Float, + val widget: Container? = null // null = unused in List mode + ) + private var listMode: NationPickerListMode = settings.nationPickerListMode + private var selection: SelectInfo? = null + private val keySelectMap = mutableMapOf>() + private var lastKeyPressed = Char.MIN_VALUE + private var keyRoundRobin = 0 + + init { + nationListScroll.setOverscroll(false, false) + add(nationListScroll).size( civBlocksWidth + 10f, partHeight ) + // +10, because the nation table has a 5f pad, for a total of +10f + if (stageToShowOn.isNarrowerThan4to3()) row() + nationDetailsScroll.setOverscroll(false, false) + add(nationDetailsScroll).size(civBlocksWidth + 10f, partHeight) // Same here, see above + + updateNationListTable() + + clickBehindToClose = true + addActionIcons() + + nationDetailsTable.touchable = Touchable.enabled + nationDetailsTable.onClick { returnSelected() } + } + + /** Note - [newMode]==null toggles, but this is prepared for key shortcuts _setting_ a mode. + * Unused due to our key input stack not supporting Ctrl-Numbers yet, postponed. + */ + private fun toggleListMode(newMode: NationPickerListMode? = null) { + fun NationPickerListMode.toggle() = when (this) { + NationPickerListMode.Icons -> NationPickerListMode.List + NationPickerListMode.List -> NationPickerListMode.Icons + } + listMode = newMode ?: listMode.toggle() + settings.nationPickerListMode = listMode + updateNationListTable() + nationListScroll.updateVisualScroll() + } + + private fun String.toImageButton(overColor: Color) = + toImageButton(buttonsIconSize, buttonsCircleSize, buttonsBackColor, overColor) + + private fun addActionIcons() { + // Despite being a Popup we use our own buttons - floating circular ones + val closeButton = "OtherIcons/Close".toImageButton(Color.FIREBRICK) + closeButton.onActivation { close() } + closeButton.keyShortcuts.add(KeyCharAndCode.BACK) + closeButton.setPosition(buttonsOffsetFromEdge, buttonsOffsetFromEdge, Align.bottomLeft) + innerTable.addActor(closeButton) + + val okButton = "OtherIcons/Checkmark".toImageButton(Color.LIME) + okButton.onActivation { returnSelected() } + okButton.keyShortcuts.add(KeyCharAndCode.RETURN) + okButton.setPosition(innerTable.width - buttonsOffsetFromEdge, buttonsOffsetFromEdge, Align.bottomRight) + innerTable.addActor(okButton) + + val switchViewButton = "OtherIcons/NationSwap".toImageButton(Color.ROYAL) + switchViewButton.onActivation { toggleListMode() } + // No keyboard support yet - file manager conventions: Ctrl-1 Icons, Ctrl-2 List + switchViewButton.setPosition(2 * buttonsOffsetFromEdge + buttonsCircleSize, buttonsOffsetFromEdge, Align.bottomLeft) + innerTable.addActor(switchViewButton) + } + + private fun returnSelected() { + val selectedNation = selection?.nation?.name + ?: return + + UncivGame.Current.musicController.chooseTrack(selectedNation, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSelectNation) + + player.chosenCiv = selectedNation + close() + playerPicker.update() + } + + private data class NationIterationElement( + val nation: Nation, + val translatedName: String = nation.name.tr(hideIcons = true) + ) + + private fun updateNationListTable() { + nationListTable.clear() + keySelectMap.clear() + nationListTable.keyShortcuts.clear() + + // As for background... In List mode, the NationTable blocks come with a 5f horizontal padding, + // so the Icon mode background "jumps" to 5f wider - haven't found a fix! + if (listMode == NationPickerListMode.List) { + nationListTable.background = null + nationListTable.defaults().space(0f) + nationListTable.pad(0f) + } else { + nationListTable.background = BaseScreen.skinStrings.getUiBackground( + "NewGameScreen/NationTable/Background", + tintColor = Color.DARK_GRAY.cpy().apply { a = 0.75f } + ) + nationListTable.defaults().space(iconViewSpacing) + nationListTable.pad(iconViewPadTop, iconViewPadHorz, iconViewPadBottom, iconViewPadHorz) + } + + // These are available as closures to the factories below + var currentX = 0f + var currentY = 0f + + // Decide by listMode how each block is built - + // for each a factory producing an Actor and info on how to select it + fun getListModeNationActor(element: NationIterationElement): Pair { + val currentSelectInfo = SelectInfo(element.nation, currentY) + val nationTable = NationTable(element.nation, civBlocksWidth, 0f) // no need for min height + val cell = nationListTable.add(nationTable) + currentY += cell.padBottom + cell.prefHeight + cell.padTop + cell.row() + return nationTable to currentSelectInfo + } + + fun getIconsModeNationActor(element: NationIterationElement): Pair { + val nationIcon = ImageGetter.getNationPortrait(element.nation, iconViewIconSize) + nationIcon.addTooltip(element.translatedName, tipAlign = Align.center, hideIcons = true) + val nationGroup = Container(nationIcon).apply { + isTransform = false + touchable = Touchable.enabled + setRound(false) + center() + } + val currentSelectInfo = SelectInfo(element.nation, currentY, nationGroup) + if (currentX + iconViewCellSize > civBlocksWidth) { + nationListTable.row() + currentX = 0f + currentY += iconViewCellSize + } + nationListTable.add(nationGroup).size(iconViewCellSize) + currentX += iconViewCellSize + iconViewSpacing + return nationGroup to currentSelectInfo + } + + val nationActorFactory = when (listMode) { + NationPickerListMode.Icons -> ::getIconsModeNationActor + NationPickerListMode.List -> ::getListModeNationActor + } + + selection = null + var selectInfo: SelectInfo? = null + + for (element in getSortedNations()) { + val (nationActor, currentSelectInfo) = nationActorFactory(element) + + nationActor.onClick { + highlightNation(currentSelectInfo) + } + nationActor.onDoubleClick { + selection = currentSelectInfo + returnSelected() + } + + if (player.chosenCiv == element.nation.name) { + selectInfo = currentSelectInfo + } + + // Keyboard: Fist letter of each "word" - "The Ottomans" get T _and_ O + val keys = element.translatedName.split(' ').map { it.first() }.toSet() + for (key in keys) { + if (key in keySelectMap) { + keySelectMap[key]!! += currentSelectInfo + } else { + keySelectMap[key] = mutableListOf(currentSelectInfo) + nationListTable.keyShortcuts.add(key) { onKeyPress(key) } + } + } + } + + nationListScroll.layout() + pack() + if (selectInfo != null) highlightNation(selectInfo) + } + + private fun getSortedNations(): Sequence { + // Random and Spectator come first, both optional + val part1 = sequence { + if (!noRandom) { + val random = Nation().apply { + name = Constants.random + innerColor = listOf(255, 255, 255) + outerColor = listOf(0, 0, 0) + setTransients() + } + yield(NationIterationElement(random)) + } + val spectator = previousScreen.ruleset.nations[Constants.spectator] + if (spectator != null && player.playerType != PlayerType.AI) // only humans can spectate, sorry robots + yield(NationIterationElement(spectator)) + } + // Then what PlayerPickerTable says we should display - see its doc + val part2 = playerPicker.getAvailablePlayerCivs(player.chosenCiv) + .map { NationIterationElement(it) } + // Combine and Sort + return part1 + + part2.sortedWith( + compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.translatedName } + ) + } + + private fun onKeyPress(key: Char) { + // Keyboard is handled for the entire Table, not per Nation Actor to allow round-robin + // That is, "Germany, Greece, Gremlins" -> press "G" repeatedly to cycle through them. + val entries = keySelectMap[key] ?: return + keyRoundRobin = if (key != lastKeyPressed) 0 else (keyRoundRobin + 1) % entries.size + lastKeyPressed = key + highlightNation(entries[keyRoundRobin]) + } + + private fun highlightNation(selectInfo: SelectInfo) { + selection?.widget?.run { + clearActions() + background = null + } + + nationDetailsTable.clearChildren() // .clear() also clears listeners! + nationDetailsTable.add(NationTable(selectInfo.nation, civBlocksWidth, partHeight, ruleset)) + selection = selectInfo + + nationListScroll.scrollY = selectInfo.scrollY - + (nationListScroll.height - nationListTable.getRowHeight(0)) / 2 + + // Because in Icons mode it's much less clear _where_ the selected Nation is in the Grid - + // the scrollY centering is enough in List mode - the selection gets a thin border + // oscillating between the Nation's colours: + selectInfo.widget?.addAction(HighlightAction(selectInfo)) + } + + @Suppress("UsePropertyAccessSyntax") // setColor _is_ a field-by-field copy not a reference set + private class HighlightAction(selectInfo: SelectInfo) : TemporalAction(1.5f) { + private val innerColor = selectInfo.nation.getInnerColor() + private val outerColor = selectInfo.nation.getOuterColor() + private val widget = selectInfo.widget!! + private val tempColor = Color() + + override fun begin() { + widget.background = ImageGetter.getDrawable("OtherIcons/Circle") + .apply { setMinSize(iconViewCellSize, iconViewCellSize) } + } + override fun update(percent: Float) { + val t = (1.0 - cos(percent * PI * 2)) / 2 + tempColor.set(outerColor).lerp(innerColor, t.toFloat()) + widget.setColor(tempColor) // Luckily only affects background + } + override fun end() { + restart() + } + } +} diff --git a/core/src/com/unciv/ui/screens/newgamescreen/NationTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/NationTable.kt index 05a54907c9..0c8835ee9f 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/NationTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/NationTable.kt @@ -32,9 +32,7 @@ class NationTable(val nation: Nation, width: Float, minHeight: Float, ruleset: R titleTable.background = BaseScreen.skinStrings.getUiBackground( "NewGameScreen/NationTable/Title", tintColor = outerColor ) - val nationIndicator: Actor = - if (nation.name == Constants.random) ImageGetter.getRandomNationPortrait(50f) - else ImageGetter.getNationPortrait(nation, 50f) + val nationIndicator = ImageGetter.getNationPortrait(nation, 50f) // Works for Random too titleTable.add(nationIndicator).pad(10f).padLeft(0f) // left 0 for centering _with_ label val titleText = if (ruleset == null || nation.name == Constants.random || nation.name == Constants.spectator) diff --git a/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt index 1c81b5dcc4..8613b57de4 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt @@ -16,21 +16,16 @@ import com.unciv.models.metadata.Player import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.nation.Nation import com.unciv.models.translations.tr -import com.unciv.ui.audio.MusicMood -import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.UncivTextField import com.unciv.ui.components.WrappableLabel import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.isEnabled -import com.unciv.ui.components.extensions.isNarrowerThan4to3 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.input.onDoubleClick import com.unciv.ui.components.extensions.setFontColor import com.unciv.ui.components.extensions.surroundWithCircle -import com.unciv.ui.components.extensions.toImageButton import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.images.ImageGetter @@ -391,115 +386,3 @@ class FriendSelectionPopup( } } - -private class NationPickerPopup( - private val playerPicker: PlayerPickerTable, - private val player: Player, - noRandom: Boolean -) : Popup(playerPicker.previousScreen as BaseScreen) { - companion object { - // These are used for the Close/OK buttons in the lower left/right corners: - const val buttonsCircleSize = 70f - const val buttonsIconSize = 50f - const val buttonsOffsetFromEdge = 5f - val buttonsBackColor: Color = Color.BLACK.cpy().apply { a = 0.67f } - } - - private val previousScreen = playerPicker.previousScreen - private val ruleset = previousScreen.ruleset - // This Popup's body has two halves of same size, either side by side or arranged vertically - // depending on screen proportions - determine height for one of those - private val partHeight = stageToShowOn.height * (if (stageToShowOn.isNarrowerThan4to3()) 0.45f else 0.8f) - private val civBlocksWidth = playerPicker.civBlocksWidth - private val nationListTable = Table() - private val nationListScroll = ScrollPane(nationListTable) - private val nationDetailsTable = Table() - private val nationDetailsScroll = ScrollPane(nationDetailsTable) - private var selectedNation: Nation? = null - - init { - nationListScroll.setOverscroll(false, false) - add(nationListScroll).size( civBlocksWidth + 10f, partHeight ) - // +10, because the nation table has a 5f pad, for a total of +10f - if (stageToShowOn.isNarrowerThan4to3()) row() - nationDetailsScroll.setOverscroll(false, false) - add(nationDetailsScroll).size(civBlocksWidth + 10f, partHeight) // Same here, see above - - val nationSequence = sequence { - if (!noRandom) yield(Nation().apply { - name = Constants.random - innerColor = listOf(255, 255, 255) - outerColor = listOf(0, 0, 0) - setTransients() - }) - val spectator = previousScreen.ruleset.nations[Constants.spectator] - if (spectator != null && player.playerType != PlayerType.AI) // only humans can spectate, sorry robots - yield(spectator) - } + playerPicker.getAvailablePlayerCivs(player.chosenCiv) - .sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.name.tr() }) - val nations = nationSequence.toCollection(ArrayList(previousScreen.ruleset.nations.size)) - - var nationListScrollY = 0f - var currentY = 0f - for (nation in nations) { - if (player.chosenCiv == nation.name) - nationListScrollY = currentY - val nationTable = NationTable(nation, civBlocksWidth, 0f) // no need for min height - val cell = nationListTable.add(nationTable) - currentY += cell.padBottom + cell.prefHeight + cell.padTop - cell.row() - nationTable.onClick { - setNationDetails(nation) - } - nationTable.onDoubleClick { - selectedNation = nation - returnSelected() - } - if (player.chosenCiv == nation.name) - setNationDetails(nation) - } - - nationListScroll.layout() - pack() - if (nationListScrollY > 0f) { - // center the selected nation vertically, getRowHeight safe because nationListScrollY > 0f ensures at least 1 row - nationListScrollY -= (nationListScroll.height - nationListTable.getRowHeight(0)) / 2 - nationListScroll.scrollY = nationListScrollY.coerceIn(0f, nationListScroll.maxY) - } - - val closeButton = "OtherIcons/Close".toImageButton(Color.FIREBRICK) - closeButton.onActivation { close() } - closeButton.keyShortcuts.add(KeyCharAndCode.BACK) - closeButton.setPosition(buttonsOffsetFromEdge, buttonsOffsetFromEdge, Align.bottomLeft) - innerTable.addActor(closeButton) - clickBehindToClose = true - - val okButton = "OtherIcons/Checkmark".toImageButton(Color.LIME) - okButton.onClick { returnSelected() } - okButton.setPosition(innerTable.width - buttonsOffsetFromEdge, buttonsOffsetFromEdge, Align.bottomRight) - innerTable.addActor(okButton) - - nationDetailsTable.touchable = Touchable.enabled - nationDetailsTable.onClick { returnSelected() } - } - - private fun String.toImageButton(overColor: Color) = - toImageButton(buttonsIconSize, buttonsCircleSize, buttonsBackColor, overColor) - - private fun setNationDetails(nation: Nation) { - nationDetailsTable.clearChildren() // .clear() also clears listeners! - - nationDetailsTable.add(NationTable(nation, civBlocksWidth, partHeight, ruleset)) - selectedNation = nation - } - - private fun returnSelected() { - if (selectedNation == null) return - - UncivGame.Current.musicController.chooseTrack(selectedNation!!.name, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSelectNation) - - player.chosenCiv = selectedNation!!.name - close() - playerPicker.update() - } -}