Victory screen replay gets a Slider (#9116)

* VictoryScreen tweaks for narrow portrait, UncivSlider update

* VictoryScreen Replay Slider
This commit is contained in:
SomeTroglodyte
2023-04-04 21:06:22 +02:00
committed by GitHub
parent ad080b4dc6
commit 910778418a
8 changed files with 119 additions and 33 deletions

View File

@ -541,6 +541,7 @@ open class TabbedPager(
* @param insertBefore -1 to add at the end, or index of existing page to insert this before it.
* @param secret Marks page as 'secret'. A password is asked once per [TabbedPager] and if it does not match the has passed in the constructor the page and all subsequent secret pages are dropped.
* @param disabled Initial disabled state. Disabled pages cannot be selected even with [selectPage], their button is dimmed.
* @param scrollAlign Used only once on first page activation - sets the content ScrollPane's scrollX/scrollY so your content (which must have valid width/height at the time) aligns as specified to the pager's content area.
* @param shortcutKey Optional keyboard key to associate.
* @param syncScroll If on, the ScrollPanes for [content] and [fixed content][IPageExtensions.getFixedContent] will synchronize horizontally.
* @return The new page's index or -1 if it could not be immediately added (secret).

View File

@ -32,7 +32,7 @@ import kotlin.math.sign
/**
* Modified Gdx [Slider]
*
* Has +/- buttons at the end for easier single steps
* Optionally has +/- buttons at the end for easier single steps
* Shows a timed tip with the actual value every time it changes
* Disables listeners of any ScrollPanes this is nested in while dragging
*
@ -43,8 +43,12 @@ import kotlin.math.sign
* @param max Initializes [Slider.max]
* @param step Initializes [Slider.stepSize]
* @param vertical Initializes [Slider.vertical]
* @param plusMinus Enable +/- buttons
* @param onChange Optional lambda gets called with the current value on change
* @param plusMinus Enable +/- buttons - note they will also snap to [setSnapToValues].
* @param initial Initializes [value]
* @param sound Plays _only_ on user dragging (+/- always play the normal Click sound). Consider using [UncivSound.Silent] for sliders with many steps.
* @param tipType None disables the tooltip, Auto animates it on change, Permanent leaves it on screen after initial fade-in.
* @param getTipText Formats a value for the tooltip. Default formats as numeric, precision depends on [stepSize]. You can also use [UncivSlider::formatPercent][formatPercent].
* @param onChange Optional lambda gets called with the current value on a user change (not when setting value programmatically).
*/
class UncivSlider (
min: Float,
@ -54,10 +58,12 @@ class UncivSlider (
plusMinus: Boolean = true,
initial: Float,
sound: UncivSound = UncivSound.Slider,
private var permanentTip: Boolean = true,
private val tipType: TipType = TipType.Permanent,
private val getTipText: ((Float) -> String)? = null,
onChange: ((Float) -> Unit)? = null
private val onChange: ((Float) -> Unit)? = null
): Table(BaseScreen.skin) {
enum class TipType { None, Auto, Permanent }
companion object {
/** Can be passed directly to the [getTipText] constructor parameter */
fun formatPercent(value: Float): String {
@ -88,6 +94,7 @@ class UncivSlider (
private var snapThreshold: Float = 0f
// Compatibility with default Slider
@Suppress("unused") // Part of the Slider API
val minValue: Float
get() = slider.minValue
@Suppress("unused") // Part of the Slider API
@ -96,7 +103,9 @@ class UncivSlider (
var value: Float
get() = slider.value
set(newValue) {
blockListener = true
slider.value = newValue
blockListener = false
valueChanged()
}
var stepSize: Float
@ -116,6 +125,7 @@ class UncivSlider (
slider.isDisabled = value
setPlusMinusEnabled()
}
@Suppress("unused") // Part of the Slider API
/** Sets the range of this slider. The slider's current value is clamped to the range. */
fun setRange(min: Float, max: Float) {
slider.setRange(min, max)
@ -133,16 +143,13 @@ class UncivSlider (
// Detect changes in isDragging
private var hasFocus = false
// Help value set not to trigger change listener events
private var blockListener = false
init {
tipLabel.setOrigin(Align.center)
tipContainer.touchable = Touchable.disabled
/** Prevents hiding the value tooltip over the slider knob */
if(permanentTip)
tipHideTask.cancel()
stepChanged() // Initialize tip formatting
if (plusMinus) {
@ -158,7 +165,7 @@ class UncivSlider (
if (vertical) row()
} else minusButton = null
add(slider).pad(padding).fill()
add(slider).pad(padding).fillY().growX()
if (plusMinus) {
if (vertical) row()
@ -179,6 +186,7 @@ class UncivSlider (
// Add the listener late so the setting of the initial value is silent
slider.addListener(object : ChangeListener() {
override fun changed(event: ChangeEvent?, actor: Actor?) {
if (blockListener) return
if (slider.isDragging != hasFocus) {
hasFocus = slider.isDragging
if (hasFocus)
@ -203,6 +211,7 @@ class UncivSlider (
Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT)
) {
value += delta
onChange?.invoke(value)
return
}
var bestDiff = -1f
@ -219,19 +228,28 @@ class UncivSlider (
bestIndex += delta.sign.toInt()
if (bestIndex !in snapToValues!!.indices) return
value = snapToValues!![bestIndex]
onChange?.invoke(value)
}
// Visual feedback
private fun valueChanged() {
if (getTipText == null)
tipLabel.setText(tipFormat.format(slider.value))
else
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // warning wrong, without !! won't compile
tipLabel.setText(getTipText!!(slider.value))
if (!tipHideTask.isScheduled) showTip()
tipHideTask.cancel()
if (!permanentTip)
Timer.schedule(tipHideTask, hideDelay)
when {
tipType == TipType.None -> Unit
getTipText == null ->
tipLabel.setText(tipFormat.format(slider.value))
else ->
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // warning wrong, without !! won't compile
tipLabel.setText(getTipText!!(slider.value))
}
when(tipType) {
TipType.None -> Unit
TipType.Auto -> {
if (!tipHideTask.isScheduled) showTip()
tipHideTask.cancel()
Timer.schedule(tipHideTask, hideDelay)
}
TipType.Permanent -> showTip()
}
setPlusMinusEnabled()
}
@ -251,7 +269,7 @@ class UncivSlider (
stepSize > 0.0099f -> "%.2f"
else -> "%.3f"
}
if (getTipText == null)
if (tipType != TipType.None && getTipText == null)
tipLabel.setText(tipFormat.format(slider.value))
}

View File

@ -90,7 +90,11 @@ class MapEditorEditTab(
defaults().pad(10f).left()
add(brushLabel)
brushCell = add().padLeft(0f)
brushSlider = UncivSlider(1f,6f,1f, initial = 1f, getTipText = { getBrushTip(it).tr() }, permanentTip = false) {
brushSlider = UncivSlider(1f,6f,1f,
initial = 1f,
getTipText = { getBrushTip(it).tr() },
tipType = UncivSlider.TipType.Auto
) {
brushSize = if (it > 5f) -1 else it.toInt()
brushLabel.setText("Brush ([${getBrushTip(it, true)}]):".tr())
}

View File

@ -25,7 +25,6 @@ import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
//TODO someoneHasWon should look at gameInfo.victoryData
//TODO replay slider
class VictoryScreen(
private val worldScreen: WorldScreen,
@ -44,8 +43,6 @@ class VictoryScreen(
val key: Char,
val iconName: String = "",
val caption: String? = null,
val align: Int = Align.topLeft,
val syncScroll: Boolean = true,
val allowAsSecret: Boolean = false
) {
OurStatus('O', "StatIcons/Specialist", caption = "Our status") {
@ -63,7 +60,7 @@ class VictoryScreen(
override fun getContent(worldScreen: WorldScreen) = VictoryScreenCivRankings(worldScreen)
override fun isHidden(playerCiv: Civilization) = UncivGame.Current.settings.useDemographics
},
Replay('P', "OtherIcons/Load", align = Align.top, syncScroll = false, allowAsSecret = true) {
Replay('P', "OtherIcons/Load", allowAsSecret = true) {
override fun getContent(worldScreen: WorldScreen) = VictoryScreenReplay(worldScreen)
override fun isHidden(playerCiv: Civilization) =
!playerCiv.isSpectator() && playerCiv.gameInfo.victoryData == null && playerCiv.isAlive()
@ -75,7 +72,7 @@ class VictoryScreen(
init {
//**************** Set up the tabs ****************
splitPane.setFirstWidget(tabs)
val iconSize = Constants.defaultFontSize.toFloat()
val iconSize = Constants.headingFontSize.toFloat()
for (tab in VictoryTabs.values()) {
val tabHidden = tab.isHidden(playerCiv)
@ -86,7 +83,7 @@ class VictoryScreen(
tab.caption ?: tab.name,
tab.getContent(worldScreen),
icon, iconSize,
scrollAlign = tab.align, syncScroll = tab.syncScroll,
scrollAlign = Align.topLeft,
shortcutKey = KeyCharAndCode(tab.key),
secret = tabHidden && tab.allowAsSecret
)

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.extensions.addSeparator
@ -15,6 +16,8 @@ class VictoryScreenCivRankings(
private val header = Table()
init {
align(Align.topLeft)
header.align(Align.topLeft)
defaults().pad(10f)
val majorCivs = worldScreen.gameInfo.civilizations.filter { it.isMajorCiv() }

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Victory
import com.unciv.ui.components.TabbedPager
@ -16,6 +17,8 @@ class VictoryScreenGlobalVictory(
private val header = Table()
init {
align(Align.top)
val gameInfo = worldScreen.gameInfo
val majorCivs = gameInfo.civilizations.asSequence().filter { it.isMajorCiv() }
val victoriesToShow = gameInfo.getEnabledVictories()

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Victory
import com.unciv.ui.components.TabbedPager
@ -16,6 +17,8 @@ class VictoryScreenOurVictory(
private val header = Table()
init {
align(Align.top)
val gameInfo = worldScreen.gameInfo
val victoriesToShow = gameInfo.getEnabledVictories()

View File

@ -1,10 +1,17 @@
package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.badlogic.gdx.utils.Timer
import com.unciv.models.UncivSound
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.setSize
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
@ -13,20 +20,62 @@ class VictoryScreenReplay(
) : Table(BaseScreen.skin), TabbedPager.IPageExtensions {
private val gameInfo = worldScreen.gameInfo
private val finalTurn = gameInfo.turns
private var replayTimer : Timer.Task? = null
private val yearLabel = "".toLabel()
private val replayMap = ReplayMap(gameInfo.tileMap)
private val header = Table()
private val yearLabel = "".toLabel()
private val slider: UncivSlider
private val playImage = ImageGetter.getImage("OtherIcons/ForwardArrow")
private val pauseImage = ImageGetter.getImage("OtherIcons/Pause")
private val playPauseButton = Container(pauseImage)
init {
header.add(yearLabel).pad(10f)
// yearLabel should be OK with 80f, Label("4000 BC").prefWidth is nearly 78f.
// The 190f is twice (that plus space=15f). The minimum 120f should never happen, just in case.
val firstTurn = gameInfo.historyStartTurn
val maxSliderPercent = if (worldScreen.isPortrait()) 0.75f else 0.5f
val sliderWidth = ((finalTurn - firstTurn) * 15f + 60f)
.coerceAtMost(worldScreen.stage.width * maxSliderPercent)
.coerceAtMost(worldScreen.stage.width - 190f)
.coerceAtLeast(120f)
slider = UncivSlider(
firstTurn.toFloat(), finalTurn.toFloat(), 1f,
initial = firstTurn.toFloat(),
sound = UncivSound.Silent,
tipType = UncivSlider.TipType.None,
onChange = this::sliderChanged
)
playImage.setSize(24f)
pauseImage.setSize(24f)
playPauseButton.apply {
// I decided against `align(Align.left)`: since the button is so much smaller than the year label, perfect symmetry is impossible
setSize(26f, 26f)
onClick(::togglePause)
}
yearLabel.setAlignment(Align.right)
yearLabel.onClick {
updateReplayTable(gameInfo.historyStartTurn)
restartTimer()
}
header.defaults().space(15f).fillX().padTop(15f)
header.add(yearLabel).minWidth(80f).right()
header.add(slider).width(sliderWidth)
header.add(playPauseButton).minWidth(80f)
add(replayMap).pad(10f)
}
private fun togglePause() {
if (replayTimer == null) restartTimer() else resetTimer()
}
private fun restartTimer() {
replayTimer?.cancel()
val firstTurn = gameInfo.historyStartTurn
val finalTurn = gameInfo.turns
val firstTurn = slider.value.toInt()
replayTimer = Timer.schedule(
object : Timer.Task() {
private var nextTurn = firstTurn
@ -39,22 +88,30 @@ class VictoryScreenReplay(
// End at the last turn.
finalTurn - firstTurn
)
playPauseButton.actor = pauseImage
}
private fun resetTimer() {
replayTimer?.cancel()
replayTimer = null
playPauseButton.actor = playImage
}
private fun sliderChanged(value: Float) {
resetTimer()
updateReplayTable(value.toInt())
}
private fun updateReplayTable(turn: Int) {
val finalTurn = gameInfo.turns
val year = gameInfo.getYear(turn - finalTurn)
yearLabel.setText(
YearTextUtil.toYearText(
year, gameInfo.currentPlayerCiv.isLongCountDisplay()
)
)
slider.value = turn.toFloat()
replayMap.update(turn)
if (turn == finalTurn) resetTimer()
}
override fun activated(index: Int, caption: String, pager: TabbedPager) {