mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-07 00:41:39 +07:00
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:
@ -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 =
|
||||||
|
@ -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('-', ' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user