Popups get the ability to scroll only the content without the buttons (#9513)

* Popups get the ability to scroll only the content without the buttons

* Centralize LoadingPopup

* Split non-WorldScreenMenuPopup classes off from that file

* Linting
This commit is contained in:
SomeTroglodyte 2023-06-04 07:41:58 +02:00 committed by GitHub
parent b9a916e081
commit 8a024bf9fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 213 additions and 92 deletions

View File

@ -47,7 +47,7 @@ open class FileChooser(
title: String?,
startFile: FileHandle? = null,
private val resultListener: ResultListener? = null
) : Popup(stageToShowOn, false) {
) : Popup(stageToShowOn, Scrollability.None) {
// config
var filter = FileFilter { true }
set(value) { field = value; resetList() }
@ -123,21 +123,20 @@ open class FileChooser(
init {
innerTable.top().left()
innerTable.touchable = Touchable.enabled
fileList.selection.setProgrammaticChangeEvents(false)
fileNameInput.setTextFieldListener { textField, _ -> result = textField.text }
if (title != null) {
addGoodSizedLabel(title).colspan(2).center().row()
innerTable.addSeparator(height = 1f)
addSeparator(height = 1f)
}
add(pathLabelWrapper).colspan(2).fillX().row()
innerTable.addSeparator(Color.GRAY, height = 1f)
addSeparator(Color.GRAY, height = 1f)
add(fileScroll).colspan(2).fill().row()
innerTable.addSeparator(height = 1f)
fileNameCell = innerTable.add().colspan(2).growX()
innerTable.row()
addSeparator(height = 1f)
fileNameCell = add().colspan(2).growX()
row()
addCloseButton("Cancel", KeyboardBinding.Cancel) {
reportResult(false)

View File

@ -24,10 +24,10 @@ class AuthPopup(stage: Stage, authSuccessful: ((Boolean) -> Unit)? = null)
authSuccessful?.invoke(true)
close()
} catch (_: Exception) {
innerTable.clear()
clear()
addGoodSizedLabel("Authentication failed").colspan(2).row()
add(passwordField).colspan(2).growX().pad(16f, 0f, 16f, 0f).row()
addCloseButton(style=negativeButtonStyle) { authSuccessful?.invoke(false) }.growX().padRight(8f)
addCloseButton(style = negativeButtonStyle) { authSuccessful?.invoke(false) }.growX().padRight(8f)
add(button).growX().padLeft(8f)
return@onClick
}
@ -35,7 +35,7 @@ class AuthPopup(stage: Stage, authSuccessful: ((Boolean) -> Unit)? = null)
addGoodSizedLabel("Please enter your server password").colspan(2).row()
add(passwordField).colspan(2).growX().pad(16f, 0f, 16f, 0f).row()
addCloseButton(style=negativeButtonStyle) { authSuccessful?.invoke(false) }.growX().padRight(8f)
addCloseButton(style = negativeButtonStyle) { authSuccessful?.invoke(false) }.growX().padRight(8f)
add(button).growX().padLeft(8f)
}
}

View File

@ -0,0 +1,19 @@
package com.unciv.ui.popups
import com.unciv.Constants
import com.unciv.ui.screens.LoadingScreen
import com.unciv.ui.screens.basescreen.BaseScreen
/**
* Mini popup just displays "Loading..." and opens itself.
*
* Not to be confused with [LoadingScreen], which tries to preserve background as screenshot.
* That screen will use this once the screenshot is on-screen, though.
*/
class LoadingPopup(screen: BaseScreen) : Popup(screen, Scrollability.None) {
init {
addGoodSizedLabel(Constants.loading)
open(true)
}
}

View File

@ -9,10 +9,12 @@ import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
@ -33,27 +35,80 @@ import com.unciv.ui.screens.basescreen.UncivStage
/**
* Base class for all Popups, i.e. Tables that get rendered in the middle of a screen and on top of everything else
*
* @property stageToShowOn The stage that will be used for [open], measurements or finding other instances
* @param scrollable Controls how content can scroll if too large - see [Scrollability]
* @param maxSizePercentage Causes [topTable] to limit its height - useful if `scrollable` is on. Will be multiplied by stageToShowOn.height.
*/
@Suppress("MemberVisibilityCanBePrivate")
open class Popup(
val stageToShowOn: Stage,
scrollable: Boolean = true
scrollable: Scrollability = Scrollability.WithoutButtons,
maxSizePercentage: Float = 0.9f
): Table(BaseScreen.skin) {
constructor(screen: BaseScreen) : this(screen.stage)
constructor(
screen: BaseScreen,
scrollable: Scrollability = Scrollability.WithoutButtons,
maxSizePercentage: Float = 0.9f
) : this(screen.stage, scrollable, maxSizePercentage)
// This exists to differentiate the actual popup (the inner table)
// from the 'screen blocking' part of the popup (which covers the entire screen)
/** Controls how content may scroll.
*
* With scrolling enabled, the ScrollPane can be accessed via [getScrollPane].
* @property None No scrolling
* @property All Entire content wrapped in an [AutoScrollPane] so it can scroll if larger than maximum dimensions
* @property WithoutButtons content separated into scrollable upper part and static lower part containing the buttons
*/
enum class Scrollability { None, All, WithoutButtons }
private val maxPopupWidth = stageToShowOn.width * maxSizePercentage
private val maxPopupHeight = stageToShowOn.height * maxSizePercentage
/** This exists to differentiate the actual popup (this table)
* from the 'screen blocking' part of the popup (which covers the entire screen).
*
* Note you seldom need to interact directly with it, Popup has many Table method proxies
* that pass through, like [add], [row], [defaults], [addSeparator] or [clear].
*/
/* Hierarchy:
Scrollability.None:
* Stage
* this@Popup (fills parent, catches click-behind)
* innerTable (entire Popup content, smaller, limited by maxSizePercentage)
* topTable and bottomTable _reference_ innerTable
Scrollability.All:
* Stage
* this@Popup (fills parent, catches click-behind)
* ScrollPane (anonymous)
* innerTable (entire Popup content, smaller, limited by maxSizePercentage)
* topTable and bottomTable _reference_ innerTable
Scrollability.WithoutButtons:
* Stage
* this@Popup (fills parent, catches click-behind)
* innerTable (entire Popup content, smaller, limited by maxSizePercentage)
* ScrollPane (anonymous)
* topTable
* bottomTable
*/
protected val innerTable = Table(BaseScreen.skin)
/** This contains most of the Popup content (except the closing buttons which go in [bottomTable]) */
private val topTable: Table
private val topTableCell: Cell<WidgetGroup>
/** This contains the bottom row buttons and does not participate in scrolling */
protected val bottomTable: Table
/** Callbacks that will be called whenever this Popup is shown */
val showListeners = mutableListOf<() -> Unit>()
/** Callbacks that will be called whenever this Popup is closed, no matter how (e.g. no distinction OK/Cancel) */
val closeListeners = mutableListOf<() -> Unit>()
/** [EventBus] is used to receive [UncivStage.VisibleAreaChanged] */
protected val events = EventBus.EventReceiver()
/** Enables/disables closing by clicking/trapping outside [innerTable].
/** Enables/disables closing by clicking/tapping outside [innerTable].
*
* Automatically set when [addCloseButton] is called but may be changed back or enabled without such a button.
*/
@ -69,15 +124,43 @@ open class Popup(
background = BaseScreen.skinStrings.getUiBackground(
"General/Popup/Background",
tintColor = Color.GRAY.cpy().apply { a = 0.5f })
//todo topTable and bottomTable _could_ be separately skinnable - but would need care so rounded edges work
innerTable.background = BaseScreen.skinStrings.getUiBackground(
"General/Popup/InnerTable",
tintColor = BaseScreen.skinStrings.skinConfig.baseColor.darken(0.5f)
)
innerTable.touchable = Touchable.enabled
innerTable.pad(20f)
innerTable.defaults().pad(5f)
fun wrapInScrollPane(table: Table) = AutoScrollPane(table, BaseScreen.skin)
.apply { setOverscroll(false, false) }
when (scrollable) {
Scrollability.None -> {
topTable = innerTable
bottomTable = innerTable
topTableCell = super.add(innerTable)
}
Scrollability.All -> {
topTable = innerTable
bottomTable = innerTable
topTableCell = super.add(wrapInScrollPane(innerTable))
}
Scrollability.WithoutButtons -> {
topTable = Table(BaseScreen.skin)
topTable.pad(20f).padBottom(0f)
topTable.defaults().fillX().pad(5f)
bottomTable = Table(BaseScreen.skin)
topTableCell = innerTable.add(wrapInScrollPane(topTable))
innerTable.defaults().fillX()
innerTable.row()
innerTable.add(bottomTable)
super.add(innerTable)
}
}
super.add(if (scrollable) AutoScrollPane(innerTable, BaseScreen.skin) else innerTable)
bottomTable.pad(20f)
bottomTable.defaults().pad(5f)
topTableCell.maxSize(maxPopupWidth, maxPopupHeight)
isVisible = false
touchable = Touchable.enabled
@ -86,12 +169,20 @@ open class Popup(
super.setFillParent(true)
}
private fun recalculateInnerTableMaxHeight() {
if (topTable === bottomTable) return
topTableCell.maxHeight(maxPopupHeight - bottomTable.prefHeight)
innerTable.invalidate()
}
/**
* Displays the Popup on the screen. If another popup is already open, this one will display after the other has
* closed. Use [force] = true if you want to open this popup above the other one anyway.
*/
fun open(force: Boolean = false) {
stageToShowOn.addActor(this)
recalculateInnerTableMaxHeight()
innerTable.pack()
pack()
center(stageToShowOn)
@ -140,13 +231,26 @@ open class Popup(
}
}
/* All additions to the popup are to the inner table - we shouldn't care that there's an inner table at all */
final override fun <T : Actor?> add(actor: T): Cell<T> = innerTable.add(actor)
override fun row(): Cell<Actor> = innerTable.row()
override fun defaults(): Cell<Actor> = innerTable.defaults()
fun addSeparator() = innerTable.addSeparator()
/* All additions to the popup are to the inner table - we shouldn't care that there's an inner table at all.
Note the Kdoc mentions innerTable when under Scrollability.WithoutButtons it's actually topTable,
but metioning that distinction seems overkill. innerTable has the clearer Kdoc for "where the Actors go".
*/
/** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [innerTable] */
final override fun <T : Actor?> add(actor: T): Cell<T> = topTable.add(actor)
/** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [innerTable] */
final override fun add(): Cell<Actor?> = topTable.add()
/** Popup proxy redirects [row][com.badlogic.gdx.scenes.scene2d.ui.Table.row] to [innerTable] */
override fun row(): Cell<Actor> = topTable.row()
/** Popup proxy redirects [defaults][com.badlogic.gdx.scenes.scene2d.ui.Table.defaults] to [innerTable] */
override fun defaults(): Cell<Actor> = topTable.defaults()
/** Popup proxy redirects [addSeparator][com.unciv.ui.components.extensions.addSeparator] to [innerTable] */
fun addSeparator(color: Color = Color.WHITE, colSpan: Int = 0, height: Float = 2f) =
topTable.addSeparator(color, colSpan, height)
/** Proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.clear] to clear content:
* [innerTable] or if [Scrollability.WithoutButtons] was used [topTable] and [bottomTable] */
override fun clear() {
innerTable.clear()
topTable.clear()
bottomTable.clear()
clickBehindToClose = false
onCloseCallback = null
}
@ -181,7 +285,7 @@ open class Popup(
val button = text.toTextButton(style)
button.onActivation { action() }
button.keyShortcuts.add(key)
return add(button)
return bottomTable.add(button)
}
fun addButton(text: String, key: Char, style: TextButtonStyle? = null, action: () -> Unit)
= addButton(text, KeyCharAndCode(key), style, action).apply { row() }
@ -250,10 +354,10 @@ open class Popup(
* Make their width equal by setting minWidth of one cell to actor width of the other.
*/
fun equalizeLastTwoButtonWidths() {
val n = innerTable.cells.size
val n = bottomTable.cells.size
if (n < 2) throw UnsupportedOperationException()
val cell1 = innerTable.cells[n-2]
val cell2 = innerTable.cells[n-1]
val cell1 = bottomTable.cells[n-2]
val cell2 = bottomTable.cells[n-1]
if (cell1.actor !is Button || cell2.actor !is Button) throw UnsupportedOperationException()
cell1.minWidth(cell2.actor.width).uniformX()
cell2.minWidth(cell1.actor.width).uniformX()
@ -268,7 +372,6 @@ open class Popup(
clear()
addGoodSizedLabel(newText)
if (withCloseButton) {
row()
addCloseButton()
}
}
@ -285,6 +388,9 @@ open class Popup(
if (stageToShowOn.setKeyboardFocus(value))
(value as? TextField)?.selectAll()
}
/** Gets the ScrollPane the content is wrapped in (only if Popup was instantiated with scrollable=true) */
fun getScrollPane() = topTable.parent as? ScrollPane
}

View File

@ -29,7 +29,7 @@ class OptionsPopup(
screen: BaseScreen,
private val selectPage: Int = defaultPage,
private val onClose: () -> Unit = {}
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = false ) {
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = Scrollability.None) {
val game = screen.game
val settings = screen.game.settings

View File

@ -5,18 +5,16 @@ import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.unciv.Constants
import com.unciv.ui.images.ImageWithCustomSize
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.popups
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.popups.LoadingPopup
/** A loading screen that creates a screenshot of the current screen and adds a "Loading..." popup on top of that */
class LoadingScreen(
previousScreen: BaseScreen? = null
) : BaseScreen() {
val screenshot: Texture
private val screenshot: Texture
init {
screenshot = takeScreenshot(previousScreen)
val image = ImageWithCustomSize(
@ -34,9 +32,7 @@ class LoadingScreen(
stage.addAction(Actions.sequence(
Actions.delay(1000f),
Actions.run {
val popup = Popup(stage)
popup.add(Constants.loading.toLabel())
popup.open()
LoadingPopup(this)
}
))
}
@ -44,7 +40,7 @@ class LoadingScreen(
private fun takeScreenshot(previousScreen: BaseScreen?): Texture {
if (previousScreen != null) {
for (popup in previousScreen.popups) popup.isVisible = false
previousScreen.render(Gdx.graphics.getDeltaTime())
previousScreen.render(Gdx.graphics.deltaTime)
}
val pixmap = Pixmap.createFromFrameBuffer(0, 0, Gdx.graphics.backBufferWidth, Gdx.graphics.backBufferHeight)
val screenshot = Texture(pixmap)

View File

@ -29,7 +29,7 @@ import kotlin.math.max
class DetailedStatsPopup(
private val cityScreen: CityScreen
) : Popup(stageToShowOn = cityScreen.stage, scrollable = false) {
) : Popup(cityScreen, Scrollability.None) {
private val headerTable = Table()
private val totalTable = Table()

View File

@ -279,7 +279,7 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
QuickSave.autoLoadGame(this)
} else {
GUI.resetToWorldScreen()
GUI.getWorldScreen().popups.filterIsInstance(WorldScreenMenuPopup::class.java).forEach(Popup::close)
GUI.getWorldScreen().popups.filterIsInstance<WorldScreenMenuPopup>().forEach(Popup::close)
ImageGetter.ruleset = game.gameInfo!!.ruleset
}
} else {

View File

@ -22,6 +22,7 @@ import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.popups.LoadingPopup
import com.unciv.utils.Concurrency
import com.unciv.utils.Log
import kotlinx.coroutines.CoroutineScope
@ -105,10 +106,7 @@ class MapEditorLoadTab(
var needPopup = true // loadMap can fail faster than postRunnable runs
Concurrency.runOnGLThread {
if (!needPopup) return@runOnGLThread
popup = Popup(editorScreen).apply {
addGoodSizedLabel(Constants.loading)
open()
}
popup = LoadingPopup(editorScreen)
}
try {
val map = MapSaver.loadMap(chosenMap!!)

View File

@ -51,7 +51,7 @@ class UnitUpgradeMenu(
private val unit: MapUnit,
private val unitAction: UpgradeUnitAction,
private val onButtonClicked: () -> Unit
) : Popup(stage, scrollable = false) {
) : Popup(stage, Scrollability.None) {
private val container: Container<Table>
private val allUpgradableUnits: Sequence<MapUnit>
private val animationDuration = 0.33f

View File

@ -25,6 +25,7 @@ import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.popups.LoadingPopup
import com.unciv.utils.Log
import com.unciv.utils.Concurrency
import com.unciv.utils.launchOnGLThread
@ -116,9 +117,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
private fun onLoadGame() {
if (selectedSave == null) return
val loadingPopup = Popup( this)
loadingPopup.addGoodSizedLabel(Constants.loading)
loadingPopup.open()
val loadingPopup = LoadingPopup(this)
Concurrency.run(loadGame) {
try {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread

View File

@ -1,11 +1,10 @@
package com.unciv.ui.screens.savescreens
import com.unciv.Constants
import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.LoadingPopup
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.utils.Concurrency
@ -55,9 +54,7 @@ object QuickSave {
}
fun autoLoadGame(screen: MainMenuScreen) {
val loadingPopup = Popup(screen)
loadingPopup.addGoodSizedLabel(Constants.loading)
loadingPopup.open()
val loadingPopup = LoadingPopup(screen)
Concurrency.run("autoLoadGame") {
// Load game from file to class on separate thread to avoid ANR...
fun outOfMemory() {

View File

@ -0,0 +1,26 @@
package com.unciv.ui.screens.worldscreen.mainmenu
import com.badlogic.gdx.Gdx
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.worldscreen.WorldScreen
class WorldScreenCommunityPopup(val worldScreen: WorldScreen) : Popup(worldScreen, scrollable = Scrollability.All) {
init {
addButton("Discord") {
Gdx.net.openURI("https://discord.gg/bjrB4Xw")
close()
}.row()
addButton("Github") {
Gdx.net.openURI("https://github.com/yairm210/Unciv")
close()
}.row()
addButton("Reddit") {
Gdx.net.openURI("https://www.reddit.com/r/Unciv/")
close()
}.row()
addCloseButton()
}
}

View File

@ -1,18 +1,15 @@
package com.unciv.ui.screens.worldscreen.mainmenu
import com.badlogic.gdx.Gdx
import com.unciv.UncivGame
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.newgamescreen.NewGameScreen
import com.unciv.ui.popups.options.addMusicControls
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.savescreens.LoadGameScreen
import com.unciv.ui.screens.savescreens.SaveGameScreen
import com.unciv.ui.screens.victoryscreen.VictoryScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, scrollable = Scrollability.All) {
init {
defaults().fillX()
@ -54,42 +51,9 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
}.row()
addButton("Music") {
close()
WorldScreenMusicButton(worldScreen).open(force = true)
WorldScreenMusicPopup(worldScreen).open(force = true)
}.row()
addCloseButton()
pack()
}
}
class WorldScreenCommunityPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
init {
defaults().fillX()
addButton("Discord") {
Gdx.net.openURI("https://discord.gg/bjrB4Xw")
close()
}.row()
addButton("Github") {
Gdx.net.openURI("https://github.com/yairm210/Unciv")
close()
}.row()
addButton("Reddit") {
Gdx.net.openURI("https://www.reddit.com/r/Unciv/")
close()
}.row()
addCloseButton()
}
}
class WorldScreenMusicButton(val worldScreen: WorldScreen) : Popup(worldScreen) {
init {
val musicController = UncivGame.Current.musicController
val settings = UncivGame.Current.settings
defaults().fillX()
addMusicControls(this, settings, musicController)
addCloseButton().colspan(2)
}
}

View File

@ -0,0 +1,17 @@
package com.unciv.ui.screens.worldscreen.mainmenu
import com.unciv.UncivGame
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.options.addMusicControls
import com.unciv.ui.screens.worldscreen.WorldScreen
class WorldScreenMusicPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
init {
val musicController = UncivGame.Current.musicController
val settings = UncivGame.Current.settings
defaults().fillX()
addMusicControls(this, settings, musicController)
addCloseButton().colspan(2)
}
}