From bebfe92fb17a45815d98aa36ca2f9f3f239abef5 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 9 Sep 2021 06:24:00 +0200 Subject: [PATCH] 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 --- .../jsons/translations/template.properties | 1 + .../ui/pickerscreens/ModManagementScreen.kt | 81 ++++++++++++++++--- .../unciv/ui/pickerscreens/PickerScreen.kt | 37 ++++++--- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index e8a3ffc4e1..22523d7aff 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1176,6 +1176,7 @@ Are you SURE you want to delete this mod? = Updated = Current mods = Downloadable mods = +Mod info and options = Next page = Open Github page = Permanent audiovisual mod = diff --git a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt index 5d7841f8ea..77b2bdb197 100644 --- a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt @@ -20,6 +20,7 @@ import com.unciv.ui.utils.UncivDateFormat.parseDate import com.unciv.ui.worldscreen.mainmenu.Github import java.util.* import kotlin.concurrent.thread +import kotlin.math.max /** * The Mod Management Screen - called only from [MainMenuScreen] @@ -29,9 +30,9 @@ import kotlin.concurrent.thread class ModManagementScreen: PickerScreen(disableScroll = true) { 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 scrollOnlineMods = ScrollPane(downloadTable) + private val scrollOnlineMods = AutoScrollPane(downloadTable) private val modActionTable = Table().apply { defaults().pad(10f) } val amountPerPage = 30 @@ -41,6 +42,10 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { private var selectedModName = "" private var selectedAuthor = "" + private val deprecationLabel: WrappableLabel + private val deprecationCell: Cell + private val modDescriptionLabel: WrappableLabel + // keep running count of mods fetched from online search for comparison to total count as reported by GitHub private var downloadModCount = 0 @@ -50,8 +55,9 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { private fun showModDescription(modName: String) { val online = modDescriptionsOnline[modName] ?: "" val installed = modDescriptionsInstalled[modName] ?: "" - val separator = if(online.isEmpty() || installed.isEmpty()) "" else "\n" - descriptionLabel.setText(online + separator + installed) + val separator = if (online.isEmpty() || installed.isEmpty()) "" else "\n" + deprecationCell.setActor(if (modName in modsToHideNames) deprecationLabel else null) + modDescriptionLabel.setText(online + separator + installed) } // Enable syncing entries in 'installed' and 'repo search ScrollPanes @@ -60,7 +66,6 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { private val onlineScrollIndex = HashMap(30) private var onlineScrollCurrentY = -1f - // cleanup - background processing needs to be stopped on exit and memory freed private var runningSearchThread: Thread? = null private var stopBackgroundTasks = false @@ -116,8 +121,50 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { closeButton.onClick(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() + 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 topTable.add().expandX() // empty cols left and right for separator 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(scrollInstalledMods) - reloadOnlineMods() topTable.add(scrollOnlineMods) topTable.add(modActionTable) @@ -177,11 +223,11 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { for (repo in repoSearch.items) { if (stopBackgroundTasks) return@postRunnable repo.name = repo.name.replace('-', ' ') - + // 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 // 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] = (repo.description ?: "-{No description provided}-".tr()) + @@ -267,13 +313,14 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { lastSelectedButton?.color = Color.WHITE button.color = Color.BLUE 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 val pos = index[name] ?: return // scroll into view 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. - lastSyncMarkedButton?.color = Color.WHITE pos.button.color = Color.valueOf("7499ab") // about halfway between royal and sky lastSyncMarkedButton = pos.button } @@ -508,9 +555,17 @@ class ModManagementScreen: PickerScreen(disableScroll = true) { game.setScreen(ModManagementScreen()) } } - + companion object { - private val blockedModsFile = FileHandle("jsons/ManuallyBlockedMods.json") - val modsToHide = JsonParser().getFromJson(Array::class.java, blockedModsFile) + val modsToHideAsUrl = run { + val blockedModsFile = FileHandle("jsons/ManuallyBlockedMods.json") + JsonParser().getFromJson(Array::class.java, blockedModsFile) + } + val modsToHideNames = run { + val regex = Regex(""".*/([^/]+)/?$""") + modsToHideAsUrl.map { url -> + regex.replace(url) { it.groups[1]!!.value }.replace('-', ' ') + } + } } } diff --git a/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt index b436d9a9e6..8202a23e12 100644 --- a/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt @@ -7,21 +7,28 @@ import com.unciv.ui.utils.* import com.unciv.ui.utils.AutoScrollPane as ScrollPane open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen() { - - internal var closeButton: TextButton = Constants.close.toTextButton() + /** The close button on the lower left of [bottomTable], see [setDefaultCloseAction] */ + 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 + /** A wrapper containing [rightSideButton]. You can add buttons, they will be arranged vertically */ 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 + private val screenSplit = 0.85f private val maxBottomTableHeight = 150f // about 7 lines of normal text /** * 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 + /** Holds the [Close button][closeButton], a [description label][descriptionLabel] and an [action button][rightSideButton] */ 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 init { @@ -29,7 +36,7 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen( descriptionLabel = "".toLabel() descriptionLabel.wrap = true - val labelScroll = ScrollPane(descriptionLabel,skin) + val labelScroll = ScrollPane(descriptionLabel, skin) bottomTable.add(labelScroll).pad(5f).fill().expand() rightSideButton = "".toTextButton() @@ -42,7 +49,8 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen( topTable = Table() 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) splitPane = SplitPane(scrollPane, bottomTable, true, skin) @@ -51,6 +59,10 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen( 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) { val closeAction = { if (previousScreen != null) game.setScreen(previousScreen) @@ -61,18 +73,19 @@ open class PickerScreen(disableScroll: Boolean = false) : CameraStageBaseScreen( onBackButtonClicked(closeAction) } - fun setRightSideButtonEnabled(bool: Boolean) { - if (bool) rightSideButton.enable() - else rightSideButton.disable() + /** Enables the [rightSideButton]. See [pick] for a way to set the text. */ + fun setRightSideButtonEnabled(enabled: Boolean) { + rightSideButton.isEnabled = enabled } + /** Sets the text of the [rightSideButton] and enables it if it's the player's turn */ protected fun pick(rightButtonText: String) { if (UncivGame.Current.worldScreen.isPlayersTurn) rightSideButton.enable() rightSideButton.setText(rightButtonText) } - fun removeRightSideClickListeners(){ - rightSideButton.listeners.filter { it != rightSideButton.clickListener } - .forEach { rightSideButton.removeListener(it) } + /** Remove listeners from [rightSideButton] to prepare giving it a new onClick */ + fun removeRightSideClickListeners() { + rightSideButton.clearListeners() } }