Mod manager portrait and auto scroll (#5138)

* Mod manager portrait mode

* Mod manager portrait and auto scroll

- MM switches to stacked expanders in portrait.
- Use AutoScrollPanes.
- Disable the enter/leave listener of AutoScrollPane in Pickers which disable the default ScrollPane to roll their own - helps all such pickers.
- No expander open/close persistence on purpose.
- PickerScreen a bit cleaned 'cuz I needed to understand something.
- Marked mods from the kill-list that are already installed.
- Button sync now OK when counterpart missing (deselects other column).

* Mod manager portrait - template
This commit is contained in:
SomeTroglodyte
2021-09-09 06:24:00 +02:00
committed by GitHub
parent ffdc289611
commit bebfe92fb1
3 changed files with 94 additions and 25 deletions

View File

@ -1176,6 +1176,7 @@ Are you SURE you want to delete this mod? =
Updated = Updated =
Current mods = Current mods =
Downloadable mods = Downloadable mods =
Mod info and options =
Next page = Next page =
Open Github page = Open Github page =
Permanent audiovisual mod = Permanent audiovisual mod =

View File

@ -20,6 +20,7 @@ import com.unciv.ui.utils.UncivDateFormat.parseDate
import com.unciv.ui.worldscreen.mainmenu.Github import com.unciv.ui.worldscreen.mainmenu.Github
import java.util.* import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.max
/** /**
* The Mod Management Screen - called only from [MainMenuScreen] * The Mod Management Screen - called only from [MainMenuScreen]
@ -29,9 +30,9 @@ import kotlin.concurrent.thread
class ModManagementScreen: PickerScreen(disableScroll = true) { class ModManagementScreen: PickerScreen(disableScroll = true) {
private val modTable = Table().apply { defaults().pad(10f) } private val modTable = Table().apply { defaults().pad(10f) }
private val scrollInstalledMods = ScrollPane(modTable) private val scrollInstalledMods = AutoScrollPane(modTable)
private val downloadTable = Table().apply { defaults().pad(10f) } private val downloadTable = Table().apply { defaults().pad(10f) }
private val scrollOnlineMods = ScrollPane(downloadTable) private val scrollOnlineMods = AutoScrollPane(downloadTable)
private val modActionTable = Table().apply { defaults().pad(10f) } private val modActionTable = Table().apply { defaults().pad(10f) }
val amountPerPage = 30 val amountPerPage = 30
@ -41,6 +42,10 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
private var selectedModName = "" private var selectedModName = ""
private var selectedAuthor = "" private var selectedAuthor = ""
private val deprecationLabel: WrappableLabel
private val deprecationCell: Cell<WrappableLabel>
private val modDescriptionLabel: WrappableLabel
// keep running count of mods fetched from online search for comparison to total count as reported by GitHub // keep running count of mods fetched from online search for comparison to total count as reported by GitHub
private var downloadModCount = 0 private var downloadModCount = 0
@ -50,8 +55,9 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
private fun showModDescription(modName: String) { private fun showModDescription(modName: String) {
val online = modDescriptionsOnline[modName] ?: "" val online = modDescriptionsOnline[modName] ?: ""
val installed = modDescriptionsInstalled[modName] ?: "" val installed = modDescriptionsInstalled[modName] ?: ""
val separator = if(online.isEmpty() || installed.isEmpty()) "" else "\n" val separator = if (online.isEmpty() || installed.isEmpty()) "" else "\n"
descriptionLabel.setText(online + separator + installed) deprecationCell.setActor(if (modName in modsToHideNames) deprecationLabel else null)
modDescriptionLabel.setText(online + separator + installed)
} }
// Enable syncing entries in 'installed' and 'repo search ScrollPanes // Enable syncing entries in 'installed' and 'repo search ScrollPanes
@ -60,7 +66,6 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
private val onlineScrollIndex = HashMap<String,ScrollToEntry>(30) private val onlineScrollIndex = HashMap<String,ScrollToEntry>(30)
private var onlineScrollCurrentY = -1f private var onlineScrollCurrentY = -1f
// cleanup - background processing needs to be stopped on exit and memory freed // cleanup - background processing needs to be stopped on exit and memory freed
private var runningSearchThread: Thread? = null private var runningSearchThread: Thread? = null
private var stopBackgroundTasks = false private var stopBackgroundTasks = false
@ -116,8 +121,50 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
closeButton.onClick(closeAction) closeButton.onClick(closeAction)
onBackButtonClicked(closeAction) onBackButtonClicked(closeAction)
val labelWidth = max(stage.width / 2f - 60f,60f)
deprecationLabel = WrappableLabel("Deprecated until update conforms to current requirements", labelWidth, Color.FIREBRICK)
deprecationLabel.wrap = true
modDescriptionLabel = WrappableLabel("", labelWidth)
modDescriptionLabel.wrap = true
// Replace the PickerScreen's descriptionLabel
val labelWrapper = Table()
labelWrapper.defaults().top().left().growX()
val labelScroll = descriptionLabel.parent as ScrollPane
descriptionLabel.remove()
deprecationCell = labelWrapper.add(deprecationLabel).padBottom(10f)
deprecationLabel.remove()
labelWrapper.row()
labelWrapper.add(modDescriptionLabel).row()
labelScroll.actor = labelWrapper
refreshInstalledModTable() refreshInstalledModTable()
if (isNarrowerThan4to3()) initPortrait()
else initLandscape()
reloadOnlineMods()
}
private fun initPortrait() {
topTable.defaults().top().pad(0f)
topTable.add(ExpanderTab("Current mods", expanderWidth = stage.width) {
it.add(scrollInstalledMods).growX()
}).top().growX().row()
topTable.add(ExpanderTab("Downloadable mods", expanderWidth = stage.width) {
it.add(scrollOnlineMods).growX()
}).top().padTop(10f).growX().row()
topTable.add().expandY().row() // helps with top() being ignored
topTable.add(ExpanderTab("Mod info and options", expanderWidth = stage.width) {
it.add(modActionTable).growX()
}).bottom().padTop(10f).growX().row()
}
private fun initLandscape() {
// Header row // Header row
topTable.add().expandX() // empty cols left and right for separator topTable.add().expandX() // empty cols left and right for separator
topTable.add("Current mods".toLabel()).pad(5f).minWidth(200f).padLeft(25f) topTable.add("Current mods".toLabel()).pad(5f).minWidth(200f).padLeft(25f)
@ -134,7 +181,6 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
topTable.add() // skip empty first column topTable.add() // skip empty first column
topTable.add(scrollInstalledMods) topTable.add(scrollInstalledMods)
reloadOnlineMods()
topTable.add(scrollOnlineMods) topTable.add(scrollOnlineMods)
topTable.add(modActionTable) topTable.add(modActionTable)
@ -177,11 +223,11 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
for (repo in repoSearch.items) { for (repo in repoSearch.items) {
if (stopBackgroundTasks) return@postRunnable if (stopBackgroundTasks) return@postRunnable
repo.name = repo.name.replace('-', ' ') repo.name = repo.name.replace('-', ' ')
// Mods we have manually decided to remove for instability are removed here // Mods we have manually decided to remove for instability are removed here
// If at some later point these mods are updated, we should definitely remove // If at some later point these mods are updated, we should definitely remove
// this piece of code. This is a band-aid, not a full solution. // this piece of code. This is a band-aid, not a full solution.
if (repo.html_url in modsToHide) continue if (repo.html_url in modsToHideAsUrl) continue
modDescriptionsOnline[repo.name] = modDescriptionsOnline[repo.name] =
(repo.description ?: "-{No description provided}-".tr()) + (repo.description ?: "-{No description provided}-".tr()) +
@ -267,13 +313,14 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
lastSelectedButton?.color = Color.WHITE lastSelectedButton?.color = Color.WHITE
button.color = Color.BLUE button.color = Color.BLUE
lastSelectedButton = button lastSelectedButton = button
if (lastSelectedButton == lastSyncMarkedButton) lastSyncMarkedButton = null if (lastSelectedButton != lastSyncMarkedButton)
lastSyncMarkedButton?.color = Color.WHITE
lastSyncMarkedButton = null
// look for sync-able same mod in other list // look for sync-able same mod in other list
val pos = index[name] ?: return val pos = index[name] ?: return
// scroll into view // scroll into view
scroll.scrollY = (pos.y + (pos.height - scroll.height) / 2).coerceIn(0f, scroll.maxY) scroll.scrollY = (pos.y + (pos.height - scroll.height) / 2).coerceIn(0f, scroll.maxY)
// and color it so it's easier to find. ROYAL and SLATE too dark. // and color it so it's easier to find. ROYAL and SLATE too dark.
lastSyncMarkedButton?.color = Color.WHITE
pos.button.color = Color.valueOf("7499ab") // about halfway between royal and sky pos.button.color = Color.valueOf("7499ab") // about halfway between royal and sky
lastSyncMarkedButton = pos.button lastSyncMarkedButton = pos.button
} }
@ -508,9 +555,17 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
game.setScreen(ModManagementScreen()) game.setScreen(ModManagementScreen())
} }
} }
companion object { companion object {
private val blockedModsFile = FileHandle("jsons/ManuallyBlockedMods.json") val modsToHideAsUrl = run {
val modsToHide = JsonParser().getFromJson(Array<String>::class.java, blockedModsFile) val blockedModsFile = FileHandle("jsons/ManuallyBlockedMods.json")
JsonParser().getFromJson(Array<String>::class.java, blockedModsFile)
}
val modsToHideNames = run {
val regex = Regex(""".*/([^/]+)/?$""")
modsToHideAsUrl.map { url ->
regex.replace(url) { it.groups[1]!!.value }.replace('-', ' ')
}
}
} }
} }

View File

@ -7,21 +7,28 @@ import com.unciv.ui.utils.*
import com.unciv.ui.utils.AutoScrollPane as ScrollPane import com.unciv.ui.utils.AutoScrollPane as ScrollPane
open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen() { open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen() {
/** The close button on the lower left of [bottomTable], see [setDefaultCloseAction] */
internal var closeButton: TextButton = Constants.close.toTextButton() protected var closeButton: TextButton = Constants.close.toTextButton()
/** A scrollable wrapped Label you can use to show descriptions in the [bottomTable], starts empty */
protected var descriptionLabel: Label protected var descriptionLabel: Label
/** A wrapper containing [rightSideButton]. You can add buttons, they will be arranged vertically */
protected var rightSideGroup = VerticalGroup() protected var rightSideGroup = VerticalGroup()
/** A button on the lower right of [bottomTable] you can use for a "OK"-type action, starts disabled */
protected var rightSideButton: TextButton protected var rightSideButton: TextButton
private val screenSplit = 0.85f private val screenSplit = 0.85f
private val maxBottomTableHeight = 150f // about 7 lines of normal text private val maxBottomTableHeight = 150f // about 7 lines of normal text
/** /**
* The table displaying the choices from which to pick (usually). * The table displaying the choices from which to pick (usually).
* Also the element which most of the screen realestate is devoted to displaying. * Also the element which most of the screen real estate is devoted to displaying.
*/ */
protected var topTable: Table protected var topTable: Table
/** Holds the [Close button][closeButton], a [description label][descriptionLabel] and an [action button][rightSideButton] */
protected var bottomTable:Table = Table() protected var bottomTable:Table = Table()
internal var splitPane: SplitPane /** A fixed SplitPane holds [scrollPane] and [bottomTable] */
protected var splitPane: SplitPane
/** A ScrollPane scrolling [topTable], disabled by the disableScroll parameter */
protected var scrollPane: ScrollPane protected var scrollPane: ScrollPane
init { init {
@ -29,7 +36,7 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen(
descriptionLabel = "".toLabel() descriptionLabel = "".toLabel()
descriptionLabel.wrap = true descriptionLabel.wrap = true
val labelScroll = ScrollPane(descriptionLabel,skin) val labelScroll = ScrollPane(descriptionLabel, skin)
bottomTable.add(labelScroll).pad(5f).fill().expand() bottomTable.add(labelScroll).pad(5f).fill().expand()
rightSideButton = "".toTextButton() rightSideButton = "".toTextButton()
@ -42,7 +49,8 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen(
topTable = Table() topTable = Table()
scrollPane = ScrollPane(topTable) scrollPane = ScrollPane(topTable)
scrollPane.setScrollingDisabled(disableScroll, disableScroll) scrollPane.setScrollingDisabled(disableScroll, disableScroll) // lock scrollPane
if (disableScroll) scrollPane.clearListeners() // remove focus capture of AutoScrollPane too
scrollPane.setSize(stage.width, stage.height - bottomTable.height) scrollPane.setSize(stage.width, stage.height - bottomTable.height)
splitPane = SplitPane(scrollPane, bottomTable, true, skin) splitPane = SplitPane(scrollPane, bottomTable, true, skin)
@ -51,6 +59,10 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen(
stage.addActor(splitPane) stage.addActor(splitPane)
} }
/**
* Initializes the [Close button][closeButton]'s action (and the Back/ESC handler)
* to return to the [previousScreen] if specified, or else to the world screen.
*/
fun setDefaultCloseAction(previousScreen: CameraStageBaseScreen?=null) { fun setDefaultCloseAction(previousScreen: CameraStageBaseScreen?=null) {
val closeAction = { val closeAction = {
if (previousScreen != null) game.setScreen(previousScreen) if (previousScreen != null) game.setScreen(previousScreen)
@ -61,18 +73,19 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen(
onBackButtonClicked(closeAction) onBackButtonClicked(closeAction)
} }
fun setRightSideButtonEnabled(bool: Boolean) { /** Enables the [rightSideButton]. See [pick] for a way to set the text. */
if (bool) rightSideButton.enable() fun setRightSideButtonEnabled(enabled: Boolean) {
else rightSideButton.disable() rightSideButton.isEnabled = enabled
} }
/** Sets the text of the [rightSideButton] and enables it if it's the player's turn */
protected fun pick(rightButtonText: String) { protected fun pick(rightButtonText: String) {
if (UncivGame.Current.worldScreen.isPlayersTurn) rightSideButton.enable() if (UncivGame.Current.worldScreen.isPlayersTurn) rightSideButton.enable()
rightSideButton.setText(rightButtonText) rightSideButton.setText(rightButtonText)
} }
fun removeRightSideClickListeners(){ /** Remove listeners from [rightSideButton] to prepare giving it a new onClick */
rightSideButton.listeners.filter { it != rightSideButton.clickListener } fun removeRightSideClickListeners() {
.forEach { rightSideButton.removeListener(it) } rightSideButton.clearListeners()
} }
} }