diff --git a/core/src/com/unciv/ui/components/TabbedPager.kt b/core/src/com/unciv/ui/components/TabbedPager.kt index 52cdb960c2..b475dee52b 100644 --- a/core/src/com/unciv/ui/components/TabbedPager.kt +++ b/core/src/com/unciv/ui/components/TabbedPager.kt @@ -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). diff --git a/core/src/com/unciv/ui/components/UncivSlider.kt b/core/src/com/unciv/ui/components/UncivSlider.kt index ba013072e1..b0697ee428 100644 --- a/core/src/com/unciv/ui/components/UncivSlider.kt +++ b/core/src/com/unciv/ui/components/UncivSlider.kt @@ -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)) } diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditTab.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditTab.kt index 8fecc00fc9..be2c4dff46 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditTab.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditTab.kt @@ -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()) } diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt index 05d828067e..b46aec4f22 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt @@ -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 ) diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenCivRankings.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenCivRankings.kt index 68457a9ec8..e3cbec8496 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenCivRankings.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenCivRankings.kt @@ -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() } diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenGlobalVictory.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenGlobalVictory.kt index 11d51e1551..42d33040bc 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenGlobalVictory.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenGlobalVictory.kt @@ -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() diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenOurVictory.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenOurVictory.kt index bb1029ac5b..bd175a901b 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenOurVictory.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenOurVictory.kt @@ -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() diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenReplay.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenReplay.kt index ffe344fc9b..321813fa97 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenReplay.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreenReplay.kt @@ -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) {